260 lines
12 KiB
PHP
260 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Cawangan;
|
|
use App\Gadai;
|
|
use App\ShorakaTransaction;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
/** Temporary new financing only. Existing/refinancing contracts retain their database path. */
|
|
class ShorakaNewGadaiService
|
|
{
|
|
const TTL = 604800; // Seven days; loss/expiry requires reconciliation, never resubmission.
|
|
|
|
private function cache()
|
|
{
|
|
return Cache::store(config('services.shoraka.pending_cache_store', 'file'));
|
|
}
|
|
|
|
private function key(string $branch, string $reference): string
|
|
{
|
|
return 'shoraka:new:' . hash('sha256', $branch . ':' . $reference);
|
|
}
|
|
|
|
public function pending(string $branch, string $reference): ?array
|
|
{
|
|
return $this->cache()->get($this->key($branch, $reference));
|
|
}
|
|
|
|
private function put(string $branch, string $reference, array $data): void
|
|
{
|
|
if (!$this->cache()->put($this->key($branch, $reference), $data, self::TTL)) {
|
|
abort(503, 'Maklumat sementara tidak dapat disimpan.');
|
|
}
|
|
}
|
|
|
|
// File cache and these locks must be shared by every API worker serving a branch.
|
|
public function locked(string $key, callable $callback)
|
|
{
|
|
$directory = storage_path('framework/cache/shoraka-locks');
|
|
if (!is_dir($directory) && !@mkdir($directory, 0700, true) && !is_dir($directory)) {
|
|
abort(503, 'Kunci transaksi tidak tersedia.');
|
|
}
|
|
$handle = fopen($directory . '/' . hash('sha256', $key) . '.lock', 'c');
|
|
if (!$handle) {
|
|
abort(503, 'Kunci transaksi tidak tersedia.');
|
|
}
|
|
if (!flock($handle, LOCK_EX | LOCK_NB)) {
|
|
fclose($handle);
|
|
abort(409, 'Transaksi sedang diproses. Sila tunggu.');
|
|
}
|
|
try {
|
|
return $callback();
|
|
} finally {
|
|
flock($handle, LOCK_UN);
|
|
fclose($handle);
|
|
}
|
|
}
|
|
|
|
public function reserve(string $branch, array $customer): Gadai
|
|
{
|
|
return $this->locked('reserve:' . $branch, function () use ($branch, $customer) {
|
|
$cawangan = Cawangan::on('mysql7')->where('kodcaw', $branch)->firstOrFail();
|
|
$date = Carbon::now()->toDateString();
|
|
$counter = 'shoraka:serial:' . $branch . ':' . $date;
|
|
$latest = Gadai::on($cawangan->mysql)->where('t_gadai', $date)->max('sirig');
|
|
$highest = max((int) $latest, (int) $this->cache()->get($counter, 0));
|
|
$releasedKey = 'shoraka:released:' . $branch . ':' . $date;
|
|
$released = $this->cache()->get($releasedKey, []);
|
|
sort($released, SORT_NUMERIC);
|
|
$serial = null;
|
|
while ($released) {
|
|
$candidate = (int) array_shift($released);
|
|
$candidateReference = strtoupper($branch) . Carbon::parse($date)->format('ymd') . '-' . sprintf('%04d', $candidate);
|
|
if (!ShorakaTransaction::where('norujukan', $candidateReference)->exists()
|
|
&& !Gadai::on($cawangan->mysql)->where('norujukan', $candidateReference)->exists()
|
|
&& !$this->pending($branch, $candidateReference)) {
|
|
$serial = $candidate;
|
|
$reference = $candidateReference;
|
|
break;
|
|
}
|
|
}
|
|
if (!$this->cache()->forever($releasedKey, $released)) {
|
|
abort(503, 'Nombor rujukan dibatalkan tidak dapat dikemas kini.');
|
|
}
|
|
if ($serial === null) {
|
|
$serial = $highest;
|
|
do {
|
|
++$serial;
|
|
$reference = strtoupper($branch) . Carbon::parse($date)->format('ymd') . '-' . sprintf('%04d', $serial);
|
|
} while (ShorakaTransaction::where('norujukan', $reference)->exists()
|
|
|| Gadai::on($cawangan->mysql)->where('norujukan', $reference)->exists()
|
|
|| $this->pending($branch, $reference));
|
|
}
|
|
if (!$this->cache()->forever($counter, max($highest, $serial))) {
|
|
abort(503, 'Nombor rujukan tidak dapat ditempah.');
|
|
}
|
|
$data = array_merge($customer, [
|
|
'kodcaw' => $branch, 'norujukan' => $reference, 'sirig' => $serial,
|
|
't_gadai' => $date, 'masa' => Carbon::now()->toTimeString(), 'sttebus' => '',
|
|
'cara_terima' => 'TUNAI', 'cetak' => 0, 'tagserah' => 0, 'tagkomuditi' => 0,
|
|
]);
|
|
if ($cawangan->version === 'v2') {
|
|
$data['tagbaru'] = 1;
|
|
}
|
|
$this->put($branch, $reference, ['gadai' => $data, 'items' => [], 'next_item' => 1, 'revision' => 0, 'prepared' => false]);
|
|
return $this->find($branch, $reference);
|
|
});
|
|
}
|
|
|
|
public function abandon(string $branch, string $reference): void
|
|
{
|
|
// Use the reservation lock as well so release and allocation cannot race.
|
|
$this->locked('reserve:' . $branch, function () use ($branch, $reference) {
|
|
$this->locked($branch . ':' . $reference, function () use ($branch, $reference) {
|
|
$pending = $this->mutable($branch, $reference);
|
|
$cawangan = Cawangan::on('mysql7')->where('kodcaw', $branch)->firstOrFail();
|
|
if (Gadai::on($cawangan->mysql)->where('norujukan', $reference)->exists()) {
|
|
abort(409, 'Gadaian telah disimpan dan nombor rujukan tidak boleh digunakan semula.');
|
|
}
|
|
$releasedKey = 'shoraka:released:' . $branch . ':' . $pending['gadai']['t_gadai'];
|
|
$released = $this->cache()->get($releasedKey, []);
|
|
$released[] = (int) $pending['gadai']['sirig'];
|
|
if (!$this->cache()->forever($releasedKey, array_values(array_unique($released)))) {
|
|
abort(503, 'Nombor rujukan tidak dapat dilepaskan.');
|
|
}
|
|
if (!$this->cache()->forget($this->key($branch, $reference))) {
|
|
abort(503, 'Maklumat sementara tidak dapat dibatalkan.');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
public function latestSerial(string $branch, string $date): int
|
|
{
|
|
return (int) $this->cache()->get('shoraka:serial:' . $branch . ':' . $date, 0);
|
|
}
|
|
|
|
public function claim(string $branch, string $reference, array $payload, callable $create)
|
|
{
|
|
return $this->locked($branch . ':' . $reference, function () use ($branch, $reference, $payload, $create) {
|
|
$pending = $this->mutable($branch, $reference);
|
|
if (!$pending['prepared']
|
|
|| number_format((float) ($pending['gadai']['pinjaman'] ?? 0), 2, '.', '') !== $payload['order_amount']
|
|
|| number_format((float) ($pending['gadai']['hargajualan'] ?? 0), 2, '.', '') !== $payload['murabaha_amount']) {
|
|
abort(409, 'Maklumat gadaian berubah. Sila sahkan semula sebelum penghantaran.');
|
|
}
|
|
return $create(); // The unique transaction reference freezes further edits.
|
|
});
|
|
}
|
|
|
|
public function find(string $branch, string $reference)
|
|
{
|
|
$cawangan = Cawangan::on('mysql7')->where('kodcaw', $branch)->firstOrFail();
|
|
$saved = Gadai::on($cawangan->mysql)->where('kodcaw', $branch)->where('norujukan', $reference)->first();
|
|
if ($saved) {
|
|
return $saved;
|
|
}
|
|
$pending = $this->pending($branch, $reference);
|
|
if (!$pending) {
|
|
return null;
|
|
}
|
|
$model = (new Gadai())->setConnection($cawangan->mysql);
|
|
$model->setRawAttributes($pending['gadai']);
|
|
return $model;
|
|
}
|
|
|
|
private function mutable(string $branch, string $reference): array
|
|
{
|
|
if (ShorakaTransaction::where('norujukan', $reference)->exists()) {
|
|
abort(409, 'Kontrak telah dihantar ke Shoraka dan tidak boleh diubah.');
|
|
}
|
|
$pending = $this->pending($branch, $reference);
|
|
if (!$pending) {
|
|
abort(409, 'Maklumat sementara tamat. Semakan diperlukan.');
|
|
}
|
|
return $pending;
|
|
}
|
|
|
|
public function prepare(string $branch, string $reference, array $fields, int $revision): void
|
|
{
|
|
$this->locked($branch . ':' . $reference, function () use ($branch, $reference, $fields, $revision) {
|
|
$pending = $this->mutable($branch, $reference);
|
|
if (($pending['revision'] ?? 0) !== $revision) {
|
|
abort(409, 'Maklumat marhun berubah semasa pengiraan. Sila sahkan semula.');
|
|
}
|
|
$pending['gadai'] = array_merge($pending['gadai'], $fields);
|
|
$pending['prepared'] = true;
|
|
$this->put($branch, $reference, $pending);
|
|
});
|
|
}
|
|
|
|
public function items(string $branch, string $reference)
|
|
{
|
|
return collect(array_values($this->pending($branch, $reference)['items'] ?? []));
|
|
}
|
|
|
|
public function changeItem(string $branch, string $reference, array $fields, $id = null, bool $delete = false)
|
|
{
|
|
return $this->locked($branch . ':' . $reference, function () use ($branch, $reference, $fields, $id, $delete) {
|
|
$pending = $this->mutable($branch, $reference);
|
|
if ($id !== null && !isset($pending['items'][$id])) {
|
|
abort(404, 'Marhun tidak dijumpai.');
|
|
}
|
|
if ($id === null) {
|
|
$id = $pending['next_item']++;
|
|
}
|
|
$item = array_merge($pending['items'][$id] ?? [], $fields, ['recno' => (int) $id]);
|
|
if ($delete) {
|
|
unset($pending['items'][$id]);
|
|
} else {
|
|
$pending['items'][$id] = $item;
|
|
}
|
|
$pending['revision'] = ($pending['revision'] ?? 0) + 1;
|
|
$pending['prepared'] = false; // Reprice the contract after any collateral edit.
|
|
$this->put($branch, $reference, $pending);
|
|
return $item;
|
|
});
|
|
}
|
|
|
|
public function promote(ShorakaTransaction $order): void
|
|
{
|
|
if ($order->status !== 'Completed' || !$this->pending($order->kodcaw, $order->norujukan)) {
|
|
return;
|
|
}
|
|
$this->locked($order->kodcaw . ':' . $order->norujukan, function () use ($order) {
|
|
$pending = $this->pending($order->kodcaw, $order->norujukan);
|
|
$data = $pending['gadai'];
|
|
if (!$pending['prepared']
|
|
|| number_format((float) ($data['pinjaman'] ?? 0), 2, '.', '') !== $order->order_amount
|
|
|| number_format((float) ($data['hargajualan'] ?? 0), 2, '.', '') !== $order->murabaha_amount) {
|
|
abort(409, 'Amaun gadaian tidak sepadan dengan Shoraka.');
|
|
}
|
|
$branch = Cawangan::on('mysql7')->where('kodcaw', $order->kodcaw)->firstOrFail();
|
|
$connection = (new Gadai())->setConnection($branch->mysql)->getConnection();
|
|
$connection->transaction(function () use ($connection, $pending, $data, $order) {
|
|
$existing = $connection->table('gadai')->where('norujukan', $order->norujukan)->lockForUpdate()->first();
|
|
if ($existing) {
|
|
if ($existing->kodcaw !== $order->kodcaw
|
|
|| number_format((float) $existing->pinjaman, 2, '.', '') !== $order->order_amount
|
|
|| number_format((float) $existing->hargajualan, 2, '.', '') !== $order->murabaha_amount) {
|
|
abort(409, 'Rujukan gadaian bertindih. Semakan diperlukan.');
|
|
}
|
|
return;
|
|
}
|
|
$connection->table('gadai')->insert(array_merge($data, ['tagkomuditi' => 1]));
|
|
foreach ($pending['items'] as $item) {
|
|
unset($item['recno']);
|
|
$connection->table('marhun')->insert($item);
|
|
}
|
|
});
|
|
\App\Audit::firstOrCreate([
|
|
'kodcaw' => $order->kodcaw, 'norujukan' => $order->norujukan, 'actions' => 'Daftar Gadai',
|
|
], ['users' => $data['teller'] ?? '', 'new_data' => json_encode($data)]);
|
|
// Retain the cache briefly for idempotent reconciliation after a lost response.
|
|
});
|
|
}
|
|
}
|