diff --git a/app/Http/Controllers/GadaiController.php b/app/Http/Controllers/GadaiController.php index bba2619..9d795c6 100644 --- a/app/Http/Controllers/GadaiController.php +++ b/app/Http/Controllers/GadaiController.php @@ -55,6 +55,10 @@ class GadaiController extends Controller $cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$cawangan_code)->first(); $gadaiTransaction = Gadai::on($cawangan['mysql'])->where('t_gadai','=',$date)->orderBy('sirig', 'desc')->first(); + $serial = app(\App\Services\ShorakaNewGadaiService::class)->latestSerial($cawangan_code, $date); + if ($serial > (int) ($gadaiTransaction->sirig ?? 0)) { + $gadaiTransaction = ['sirig' => $serial]; + } }catch(\Exception $e){ Log::debug($e); Log::debug($cawangan_code); @@ -111,7 +115,7 @@ class GadaiController extends Controller $gadai_all = []; foreach($cawangan_all as $index=>$cawangan){ - $gadai = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->get(); + $gadai = collect(array_filter([app(\App\Services\ShorakaNewGadaiService::class)->find($cawangan->kodcaw, $norujukan)])); if(sizeof($gadai) != 0){ array_push($gadai_all,$gadai); } @@ -130,7 +134,7 @@ class GadaiController extends Controller $gadai_all = []; foreach($cawangan_all as $index=>$cawangan){ - $gadai = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->get(); + $gadai = collect(array_filter([app(\App\Services\ShorakaNewGadaiService::class)->find($cawangan->kodcaw, $norujukan)])); if(sizeof($gadai) != 0){ array_push($gadai_all,$gadai); } @@ -152,13 +156,22 @@ class GadaiController extends Controller public function showOneGadaibyRujukanAndCawangan($kodcaw,$norujukan){ $cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$kodcaw)->first(); - $gadai = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->get(); + $gadai = collect(array_filter([app(\App\Services\ShorakaNewGadaiService::class)->find($cawangan->kodcaw, $norujukan)])); return response()->json($gadai); } public function create($kodcaw,Request $request){ + if ($request->boolean('shoraka_new')) { + $this->validate($request, ['nokp' => 'required|string', 'nopelanggan' => 'required']); + return response()->json(app(\App\Services\ShorakaNewGadaiService::class)->reserve( + $kodcaw, $request->only(['nokp', 'nopelanggan']) + ), 201); + } + if (app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, (string) $request->norujukan)) { + abort(409, 'Nombor rujukan sedang digunakan. Sila dapatkan nombor baharu.'); + } $master = new MasterServices($kodcaw); $response = $master->createGadai($request); @@ -228,14 +241,18 @@ class GadaiController extends Controller } - public function daftarGadaiMarhun($norujukan,Request $request){ - if (\App\ShorakaTransaction::where('norujukan', $request->norujukan)->exists()) { + public function daftarGadaiMarhun($kodcaw, $norujukan, Request $request) + { + if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists()) { abort(409, 'Kontrak telah dihantar ke Shoraka dan tidak boleh diubah.'); } - // dd("ggwp"); - $master = new MasterServices($request->kodcaw); - $response = $master->createMarhunGadai($request->norujukan,$request); - + $temporary = app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, $norujukan); + if ($temporary && (string) $temporary['gadai']['nokp'] !== (string) $request->nokp) { + abort(409, 'Pelanggan tidak sepadan dengan borang gadaian sementara.'); + } + $request->merge(['kodcaw' => $kodcaw, 'norujukan' => $norujukan]); + $master = new MasterServices($kodcaw); + $response = $master->createMarhunGadai($norujukan, $request); return response()->json($response, 200); } @@ -249,7 +266,13 @@ class GadaiController extends Controller public function checkNorujukan(Request $request){ $cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$request->kodcaw)->first(); - $gadai = Gadai::on($cawangan['mysql'])->where([['norujukan','=',$request->norujukan],['nokp','=',$request->nokp]])->first(); + $gadai = app(\App\Services\ShorakaNewGadaiService::class)->find($request->kodcaw, $request->norujukan); + if (!$gadai && \App\ShorakaTransaction::where('norujukan', $request->norujukan)->exists()) { + abort(409, 'Transaksi Shoraka wujud tetapi maklumat sementara tiada. Hubungi operasi; jangan daftar semula.'); + } + if ($gadai && $gadai->nokp !== $request->nokp) { + $gadai = null; + } return response()->json($gadai); } @@ -372,6 +395,16 @@ class GadaiController extends Controller } public function batalGadai($kodcaw, $norujukan,Request $request){ + $temporary = app(\App\Services\ShorakaNewGadaiService::class); + $order = \App\ShorakaTransaction::where('norujukan', $norujukan)->first(); + if ($order && !empty($order->request_payload['_new_gadai'])) { + abort(409, 'Transaksi Shoraka telah dihantar. Semakan operasi diperlukan sebelum pembatalan.'); + } + if ($temporary->pending($kodcaw, $norujukan)) { + $temporary->abandon($kodcaw, $norujukan); + return response('Batal Berjaya', 200); + } + $current = Carbon::now(); $current = new Carbon(); @@ -426,7 +459,8 @@ class GadaiController extends Controller } public function cetakSAG($kodcaw,$norujukan,Request $request){ - if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists()) { + if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists() + || app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, $norujukan)) { app(\App\Services\ShorakaGadaiService::class)->assertCompleted($kodcaw, $norujukan); } @@ -454,7 +488,8 @@ class GadaiController extends Controller } public function serahMarhun($kodcaw,$norujukan){ - if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists()) { + if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists() + || app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, $norujukan)) { app(\App\Services\ShorakaGadaiService::class)->assertCompleted($kodcaw, $norujukan); } diff --git a/app/Http/Controllers/MarhunController.php b/app/Http/Controllers/MarhunController.php index cf5d893..2df9169 100644 --- a/app/Http/Controllers/MarhunController.php +++ b/app/Http/Controllers/MarhunController.php @@ -26,6 +26,9 @@ class MarhunController extends Controller $marhun_all = []; foreach($cawangan_all as $index=>$cawangan){ $marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->get(); + if ($marhun->isEmpty()) { + $marhun = app(\App\Services\ShorakaNewGadaiService::class)->items($cawangan->kodcaw, $norujukan); + } if(sizeof($marhun) != 0){ array_push($marhun_all,$marhun); } @@ -40,6 +43,9 @@ class MarhunController extends Controller $marhun = Marhun::on($cawangan['mysql'])->where('norujukan',$request->norujukan)->distinct()->pluck('karat')->toArray(); + if (app(\App\Services\ShorakaNewGadaiService::class)->pending($request->kodcaw, $request->norujukan)) { + $marhun = app(\App\Services\ShorakaNewGadaiService::class)->items($request->kodcaw, $request->norujukan)->pluck('karat')->unique()->all(); + } $mutu = HargaEmas::on('mysql7')->whereIn('karat',$marhun)->pluck('keterangan')->toArray(); return response()->json($mutu); @@ -59,6 +65,11 @@ class MarhunController extends Controller public function create($kodcaw,Request $request) { + $order = \App\ShorakaTransaction::where('norujukan', $request->norujukan)->first(); + if ($order && !empty($order->request_payload['_new_gadai'])) { + abort(409, 'Kontrak Shoraka telah dihantar dan tidak boleh diubah.'); + } + $current = Carbon::now(); $current = new Carbon(); @@ -96,6 +107,10 @@ class MarhunController extends Controller }; $cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$kodcaw)->first(); + $temporary = app(\App\Services\ShorakaNewGadaiService::class); + if ($temporary->pending($kodcaw, $request->norujukan)) { + return response()->json($temporary->changeItem($kodcaw, $request->norujukan, $marhunToCreate), 201); + } $marhun = Marhun::on($cawangan['mysql'])->create($marhunToCreate); $audit = new Audit(); @@ -127,6 +142,17 @@ class MarhunController extends Controller public function deletemarhun($kodcaw,$norujukan,Request $request) { + $order = \App\ShorakaTransaction::where('norujukan', $norujukan)->first(); + if ($order && !empty($order->request_payload['_new_gadai'])) { + abort(409, 'Kontrak Shoraka telah dihantar dan tidak boleh diubah.'); + } + + $temporary = app(\App\Services\ShorakaNewGadaiService::class); + if ($temporary->pending($kodcaw, $norujukan)) { + $temporary->changeItem($kodcaw, $norujukan, [], $request->recno, true); + return response('Deleted Successfully', 200); + } + $current = Carbon::now(); $current = new Carbon(); @@ -152,6 +178,11 @@ class MarhunController extends Controller public function editmarhun($kodcaw,$norujukan,Request $request) { + $order = \App\ShorakaTransaction::where('norujukan', $norujukan)->first(); + if ($order && !empty($order->request_payload['_new_gadai'])) { + abort(409, 'Kontrak Shoraka telah dihantar dan tidak boleh diubah.'); + } + $current = Carbon::now(); $current = new Carbon(); @@ -159,6 +190,17 @@ class MarhunController extends Controller $harga = HargaEmas::on('mysql7')->where('karat',$request->karat)->first(); $nilai_marhun = $request->berat * $harga['harga']; + $temporary = app(\App\Services\ShorakaNewGadaiService::class); + if ($temporary->pending($kodcaw, $norujukan)) { + $temporary->changeItem($kodcaw, $norujukan, [ + 'karat' => $request->karat, 'marhun' => $request->marhun, 'nota' => $request->nota, + 'tag_permata' => $request->tag_permata, + 'berat_batu_permata' => $request->berat_batu_permata ?: null, + 'berat' => $request->berat, 'harga' => $harga['harga'], 'n_marhun' => $nilai_marhun, + 'bil' => $request->kuantiti, + ], $request->recno); + return response()->json(true); + } $cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$kodcaw)->first(); // Lock the parent first so concurrent edits to this SAG are serialized. @@ -257,6 +299,9 @@ class MarhunController extends Controller $marhuns = Marhun::on($cawangan['mysql'])->where('norujukan',$request->norujukan)->get(); + if ($marhuns->isEmpty()) { + $marhuns = app(\App\Services\ShorakaNewGadaiService::class)->items($request->kodcaw, $request->norujukan); + } $totalnilaimarhun = 0; foreach($marhuns as $index=>$marhun){ @@ -270,6 +315,11 @@ class MarhunController extends Controller public function getMarhunCaw($kodcaw,$norujukan) { + $temporary = app(\App\Services\ShorakaNewGadaiService::class); + if ($temporary->pending($kodcaw, $norujukan)) { + return response()->json($temporary->items($kodcaw, $norujukan)->sum('n_marhun')); + } + $cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$kodcaw)->first(); $data = Marhun::on($cawangan['mysql']) ->where('norujukan','=', $norujukan) diff --git a/app/Http/Controllers/ShorakaController.php b/app/Http/Controllers/ShorakaController.php index 3b9ae02..4f19c01 100644 --- a/app/Http/Controllers/ShorakaController.php +++ b/app/Http/Controllers/ShorakaController.php @@ -48,7 +48,7 @@ class ShorakaController extends Controller try { // The unique norujukan index is the atomic claim for concurrent submissions. - $transaction = ShorakaTransaction::create([ + $attributes = [ 'norujukan' => $validated['norujukan'], 'kodcaw' => $request->attributes->get('shoraka_kodcaw'), 'status' => 'PENDING', @@ -58,9 +58,15 @@ class ShorakaController extends Controller 'ownership_doc_no' => $payload['ownership_doc_no'], 'order_amount' => $payload['order_amount'], 'murabaha_amount' => $payload['murabaha_amount'], - 'request_payload' => $payload, + 'request_payload' => $request->attributes->get('shoraka_new') + ? array_merge($payload, ['_new_gadai' => true]) : $payload, 'attempt_count' => 1, - ]); + ]; + $create = function () use ($attributes) { return ShorakaTransaction::create($attributes); }; + $transaction = $request->attributes->get('shoraka_new') + ? app(\App\Services\ShorakaNewGadaiService::class)->claim( + $request->attributes->get('shoraka_kodcaw'), $validated['norujukan'], $payload, $create + ) : $create(); } catch (QueryException $exception) { $existing = ShorakaTransaction::where('norujukan', $validated['norujukan'])->first(); if ($existing) { diff --git a/app/Http/Controllers/ShorakaGadaiController.php b/app/Http/Controllers/ShorakaGadaiController.php index c25383b..990be02 100644 --- a/app/Http/Controllers/ShorakaGadaiController.php +++ b/app/Http/Controllers/ShorakaGadaiController.php @@ -25,7 +25,8 @@ class ShorakaGadaiController extends ShorakaController 'order_amount' => $gadai->pinjaman, 'murabaha_amount' => $gadai->hargajualan, 'order' => $order ? $this->orderData($order) : null, 'details' => $order ? $order->response_payload : null, - 'can_complete' => $order && $order->status === 'Completed' + 'simulation' => $order && strpos((string) $order->order_id, 'SIM-') === 0, + 'can_complete' => $gadai->exists && $order && $order->status === 'Completed' && number_format((float) $gadai->pinjaman, 2, '.', '') === $order->order_amount && number_format((float) $gadai->hargajualan, 2, '.', '') === $order->murabaha_amount, ], @@ -44,8 +45,16 @@ class ShorakaGadaiController extends ShorakaController || $gadai->cetak || $gadai->sttebus) { abort(409, 'Gadaian lama atau telah selesai tidak boleh dihantar sebagai order baharu.'); } + $temporary = app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, $norujukan); + if ($temporary && $gadai->exists) { + abort(409, 'Rujukan sementara bertindih dengan gadaian sedia ada. Semakan operasi diperlukan.'); + } + if ($temporary && !$temporary['prepared']) { + abort(409, 'Sila sahkan semula amaun gadaian selepas perubahan marhun.'); + } $request = new Request($gadaiService->payload($gadai)); $request->attributes->set('shoraka_kodcaw', $kodcaw); + $request->attributes->set('shoraka_new', !$gadai->exists); return $this->submit($request, $shoraka); } diff --git a/app/Services/ShorakaGadaiService.php b/app/Services/ShorakaGadaiService.php index f091b3e..027ebd9 100644 --- a/app/Services/ShorakaGadaiService.php +++ b/app/Services/ShorakaGadaiService.php @@ -13,7 +13,8 @@ class ShorakaGadaiService public function gadai(string $kodcaw, string $norujukan): Gadai { $branch = Cawangan::on('mysql7')->where('kodcaw', $kodcaw)->firstOrFail(); - return Gadai::on($branch->mysql)->where('norujukan', $norujukan)->where('kodcaw', $kodcaw)->firstOrFail(); + return app(ShorakaNewGadaiService::class)->find($kodcaw, $norujukan) + ?? abort(404, 'Maklumat gadaian tidak tersedia. Semakan diperlukan.'); } public function payload(Gadai $gadai): array @@ -52,7 +53,11 @@ class ShorakaGadaiService if (!$order->kodcaw) { return; } + app(ShorakaNewGadaiService::class)->promote($order); $gadai = $this->gadai($order->kodcaw, $order->norujukan); + if (!$gadai->exists) { + return; + } $sameAmounts = number_format((float) $gadai->pinjaman, 2, '.', '') === $order->order_amount && number_format((float) $gadai->hargajualan, 2, '.', '') === $order->murabaha_amount; $gadai->getConnection()->table('gadai')->where('norujukan', $order->norujukan)->update([ @@ -64,7 +69,7 @@ class ShorakaGadaiService { $gadai = $this->gadai($kodcaw, $norujukan); $order = $this->transaction($kodcaw, $norujukan); - if (!$order || $order->status !== 'Completed' + if (!$gadai->exists || !$order || $order->status !== 'Completed' || number_format((float) $gadai->pinjaman, 2, '.', '') !== $order->order_amount || number_format((float) $gadai->hargajualan, 2, '.', '') !== $order->murabaha_amount) { abort(409, 'Pembiayaan dan SAG ditahan sehingga transaksi Shoraka selesai dan amaun sepadan.'); diff --git a/app/Services/ShorakaNewGadaiService.php b/app/Services/ShorakaNewGadaiService.php new file mode 100644 index 0000000..b7602d4 --- /dev/null +++ b/app/Services/ShorakaNewGadaiService.php @@ -0,0 +1,223 @@ +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'); + $serial = max((int) $latest, (int) $this->cache()->get($counter, 0)); + do { + ++$serial; + $reference = strtoupper($branch) . Carbon::now()->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, $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 + { + $this->locked($branch . ':' . $reference, function () use ($branch, $reference) { + $this->mutable($branch, $reference); + $this->cache()->forget($this->key($branch, $reference)); + }); + } + + 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. + }); + } +} diff --git a/app/Services/ShorakaService.php b/app/Services/ShorakaService.php index 4e294f2..dec33cd 100644 --- a/app/Services/ShorakaService.php +++ b/app/Services/ShorakaService.php @@ -32,11 +32,50 @@ class ShorakaService public function configured(): bool { + if ($this->simulationRequested()) { + return $this->simulationAllowed() && in_array($this->simulationMode(), ['failed', 'pending'], true); + } return $this->apiId !== '' && $this->secretKey !== ''; } + protected function simulationAllowed(): bool + { + return app()->environment('local', 'testing'); + } + + protected function simulationMode(): string + { + return strtolower(trim((string) config('services.shoraka.simulation', 'off'))); + } + + protected function simulationRequested(): bool + { + return !in_array($this->simulationMode(), ['', 'off'], true); + } + + private function simulatedResponse(string $orderId): array + { + if (!$this->simulationAllowed()) { + throw new RuntimeException('Shoraka simulation is restricted to local/testing.'); + } + if (strpos($orderId, 'SIM-FAILED-') === 0) { + return ['orderId' => $orderId, 'status' => 'Cancelled']; + } + if (strpos($orderId, 'SIM-PENDING-') === 0) { + return ['orderId' => $orderId, 'status' => 'Active']; + } + throw new RuntimeException('Invalid simulation order ID.'); + } + public function submitOrder(array $data): array { + if ($this->simulationRequested()) { + if (!$this->simulationAllowed() || !in_array($this->simulationMode(), ['failed', 'pending'], true)) { + throw new RuntimeException('Shoraka simulation configuration is not allowed.'); + } + return $this->simulatedResponse('SIM-' . strtoupper($this->simulationMode()) . '-' . bin2hex(random_bytes(12))); + } + $payload = ['api_id' => $this->apiId]; foreach ([ 'product_type', 'commodity_type', 'ownership', 'value_date', @@ -61,6 +100,13 @@ class ShorakaService public function orderStatus(string $orderId): array { + if (strpos($orderId, 'SIM-') === 0) { + return $this->simulatedResponse($orderId); // Never send simulated references to the real provider. + } + if ($this->simulationRequested()) { + throw new RuntimeException('Real order status is unavailable while simulation is enabled.'); + } + return $this->orderResponse($this->request()->post( $this->baseUrl . '/api/orderstatus', $this->orderPayload($orderId) ), $orderId); @@ -68,6 +114,10 @@ class ShorakaService public function certificate(string $orderId): string { + if ($this->simulationRequested() || strpos($orderId, 'SIM-') === 0) { + throw new RuntimeException('Certificates are unavailable for simulation.'); + } + // Section 5 specifies POST, despite GET in the document history. $response = $this->request()->accept('application/pdf')->post( $this->baseUrl . '/api/certificate', $this->orderPayload($orderId) @@ -83,6 +133,10 @@ class ShorakaService public function validCallback(array $payload): bool { + if ($this->simulationRequested() || strpos((string) ($payload['orderId'] ?? ''), 'SIM-') === 0) { + return false; + } + if (!$this->configured() || !isset($payload['apiId'], $payload['signature']) || !is_string($payload['apiId']) diff --git a/app/Support/v1/GadaiV1.php b/app/Support/v1/GadaiV1.php index a35e5d9..fb094c9 100644 --- a/app/Support/v1/GadaiV1.php +++ b/app/Support/v1/GadaiV1.php @@ -73,10 +73,13 @@ class GadaiV1 $old_data = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->first(); } - $bil_marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->count(); + $temporary = app(\App\Services\ShorakaNewGadaiService::class); + $temporaryData = $temporary->pending($request->kodcaw, $norujukan); + $isTemporary = $temporaryData !== null; + $bil_marhun = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->count() : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->count()); - $total_nilai_marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('n_marhun'); - $total_berat = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('berat'); + $total_nilai_marhun = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('n_marhun') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('n_marhun')); + $total_berat = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('berat') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('berat')); $kadar_raw = KadarUpah::where('min','<=',$total_nilai_marhun)->where('max','>=',$total_nilai_marhun)->first(); $tawarruqservices = new Calculation(); @@ -88,9 +91,7 @@ class GadaiV1 $hargajualan = $request->pinjaman + $upah12; - $gadai = Gadai::on($cawangan['mysql']) - ->where('norujukan','=',$norujukan) - ->update(array( + $gadaiFields = array( 'nilaimarhun' => $total_nilai_marhun, 'berat'=> $total_berat, 'pinjaman' => $request->pinjaman, @@ -115,7 +116,12 @@ class GadaiV1 'upahdibayar' => 0.00, 'tempohbayar' => 0.00, 'lelong_tawarruq' => 1 - )); + ); + if ($isTemporary) { + $temporary->prepare($request->kodcaw, $norujukan, $gadaiFields, $temporaryData['revision'] ?? 0); + return response()->json(true); + } + $gadai = Gadai::on($cawangan['mysql'])->where('norujukan', $norujukan)->update($gadaiFields); $new_data = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->first(); diff --git a/app/Support/v1/Penebusan.php b/app/Support/v1/Penebusan.php index 08af1c5..e0a864a 100644 --- a/app/Support/v1/Penebusan.php +++ b/app/Support/v1/Penebusan.php @@ -946,7 +946,8 @@ class Penebusan $gadaiTransaction = Gadai::on($cawangan['mysql'])->where('t_gadai','=',$current->toDateString())->orderBy('sirig', 'desc')->first(); $gadaiTransaction = optional($gadaiTransaction)->toArray(); - $new_sirig = ($gadaiTransaction) ? $gadaiTransaction['sirig'] + 1 : 1; + $new_sirig = max((int) ($gadaiTransaction['sirig'] ?? 0), + app(\App\Services\ShorakaNewGadaiService::class)->latestSerial($kodcaw, $current->toDateString())) + 1; $curent_year = substr($current->year,2,2); $curent_month = sprintf("%02d", $current->month); $current_day = sprintf("%02d", $current->day); diff --git a/app/Support/v2/GadaiV2.php b/app/Support/v2/GadaiV2.php index 7c17ca5..fa40cf5 100644 --- a/app/Support/v2/GadaiV2.php +++ b/app/Support/v2/GadaiV2.php @@ -107,10 +107,13 @@ class GadaiV2 $old_data = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->first(); } - $bil_marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('bil'); + $temporary = app(\App\Services\ShorakaNewGadaiService::class); + $temporaryData = $temporary->pending($request->kodcaw, $norujukan); + $isTemporary = $temporaryData !== null; + $bil_marhun = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('bil') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('bil')); - $total_nilai_marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('n_marhun'); - $total_berat = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('berat'); + $total_nilai_marhun = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('n_marhun') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('n_marhun')); + $total_berat = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('berat') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('berat')); $kadar_raw = $this->getKadarUpahAnggota($request->nokp, $total_nilai_marhun, $cawangan['group_id']); @@ -190,6 +193,10 @@ if (is_null($kadar_raw)) { } } + if ($isTemporary) { + $temporary->prepare($request->kodcaw, $norujukan, $gadaiFields, $temporaryData['revision'] ?? 0); + return 'true'; + } $gadai = Gadai::on($cawangan['mysql']) ->where('norujukan','=',$norujukan) ->update($gadaiFields); diff --git a/app/Support/v2/Penebusan.php b/app/Support/v2/Penebusan.php index 5a3f71b..c3a47cc 100644 --- a/app/Support/v2/Penebusan.php +++ b/app/Support/v2/Penebusan.php @@ -1224,7 +1224,8 @@ class Penebusan $gadaiTransaction = Gadai::on($cawangan['mysql'])->where('t_gadai','=',$current->toDateString())->orderBy('sirig', 'desc')->first(); $gadaiTransaction = optional($gadaiTransaction)->toArray(); - $new_sirig = ($gadaiTransaction) ? $gadaiTransaction['sirig'] + 1 : 1; + $new_sirig = max((int) ($gadaiTransaction['sirig'] ?? 0), + app(\App\Services\ShorakaNewGadaiService::class)->latestSerial($kodcaw, $current->toDateString())) + 1; $curent_year = substr($current->year,2,2); $curent_month = sprintf("%02d", $current->month); $current_day = sprintf("%02d", $current->day); diff --git a/config/services.php b/config/services.php index cabce3f..d0db227 100644 --- a/config/services.php +++ b/config/services.php @@ -3,6 +3,8 @@ return [ 'shoraka' => [ + // Local UI tests only: off, failed or pending. Success is deliberately excluded. + 'simulation' => env('SHORAKA_SIMULATION', 'off'), 'base_url' => env( 'SHORAKA_BASE_URL', 'https://uat2.shorakagroup.com'