diff --git a/app/Http/Controllers/AliranTunaiController.php b/app/Http/Controllers/AliranTunaiController.php index 06b0820a..e6d616be 100644 --- a/app/Http/Controllers/AliranTunaiController.php +++ b/app/Http/Controllers/AliranTunaiController.php @@ -125,10 +125,10 @@ class AliranTunaiController extends Controller $auth_user = Auth::user(); - $gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan_gadai)->json(); + $gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan_gadai)->throw()->json(); $gadai_detail = $gadai[0]; - $getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $teller])->json(); + $getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $teller])->throw()->json(); $keluar = $getTunai2['keluar']; $bakiakhir = $getTunai2['baki']; @@ -143,7 +143,7 @@ class AliranTunaiController extends Controller 'keluar' => $keluar, 'masuk' => $masuk, 'bakiakhir' => $bakiakhir - ]); + ])->throw(); return response()->json($updateTunai,201); @@ -153,13 +153,13 @@ class AliranTunaiController extends Controller $auth_user = Auth::user(); - $gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->json(); + $gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->throw()->json(); $gadai_detail = $gadai[0]; - $tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->json(); + $tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->throw()->json(); $tebus_detail = $tebus[0]; - $getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->json(); + $getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->throw()->json(); $keluar=$getTunai2['keluar']; $baki=$getTunai2['baki']; @@ -181,7 +181,7 @@ class AliranTunaiController extends Controller 'keluar'=>$keluar, 'masuk'=>$masuk, 'bakiakhir'=>$bakiakhir - ]); + ])->throw(); }else{ @@ -199,7 +199,7 @@ class AliranTunaiController extends Controller 'keluar'=>$keluar, 'masuk'=>$masuk, 'bakiakhir'=>$bakiakhir - ]); + ])->throw(); } } @@ -208,13 +208,13 @@ class AliranTunaiController extends Controller $auth_user = Auth::user(); - $gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->json(); + $gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->throw()->json(); $gadai_detail = $gadai[0]; - $tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->json(); + $tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->throw()->json(); $tebus_detail = $tebus[0]; - $getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->json(); + $getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->throw()->json(); $keluar=$getTunai2['keluar']; $baki=$getTunai2['baki']; @@ -228,7 +228,7 @@ class AliranTunaiController extends Controller 'keluar'=>$keluar, 'masuk'=>$masuk, 'bakiakhir'=>$bakiakhir - ]); + ])->throw(); } public function alirantunaiUpahBknTawarruq(Request $request){ diff --git a/app/Http/Controllers/GadaianController.php b/app/Http/Controllers/GadaianController.php index 128855f2..3b92ff49 100644 --- a/app/Http/Controllers/GadaianController.php +++ b/app/Http/Controllers/GadaianController.php @@ -285,6 +285,11 @@ class GadaianController extends Controller public function store(Request $request) { $auth_user = Auth::user(); + $shoraka = app(\App\Services\ShorakaFinancingService::class); + if ($shoraka->local($request->norujukan_gadai)) { + return $shoraka->pendingResponse($request, $request->norujukan_gadai); + } + $shoraka->ensureReady(); $caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI')); $pinjamanAmt = round((float) $request->pinjaman, 2); @@ -342,16 +347,20 @@ class GadaianController extends Controller 'nokp' => $request->nokp, 'kadar_rebate' => $this->parseMoney($request->kadar_rebate), 'rebate' => $this->parseMoney($request->rebate), - ])->json(); + ])->throw()->json(); - $komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/komuditi/transaksi', [ - 'unit_komuditi' => round($request->pinjaman, 2), - 'norujukan' => $request->norujukan_gadai, - 'kodcaw' => $auth_user['cawangan'], - 'teller' => $auth_user['name'] + return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [ + 'cara_terima' => $caraTerima, + 'pinjaman' => $pinjamanAmt, ]); + } + public function completeShorakaFinancing(Request $request) + { + $auth_user = Auth::user(); + $caraTerima = $request->cara_terima; + $pinjamanAmt = round((float) $request->pinjaman, 2); if ($caraTerima === 'ONLINE') { $online = Http::withOptions(['verify' => config('app.api_verify')]) ->asForm() @@ -374,7 +383,7 @@ class GadaianController extends Controller $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal daftar pembiayaan online. Sila cuba lagi.'; - return redirect()->back()->with('error', $msg); + throw new \RuntimeException($msg); } $this->notifyPcPembiayaanOnlinePending($auth_user, $request, $pinjamanAmt); @@ -594,6 +603,7 @@ class GadaianController extends Controller public function cetakSAG(Request $request) { + app(\App\Services\ShorakaFinancingService::class)->printData($request->norujukan_modal); $auth_user = Auth::user(); $gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/cetak/' . $auth_user['cawangan'] . '/' . $request->norujukan_modal); @@ -614,6 +624,7 @@ class GadaianController extends Controller } foreach ($arraynorujukan as $index => $norujukan) { + app(\App\Services\ShorakaFinancingService::class)->printData($norujukan); $gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/serah/' . $auth_user['cawangan'] . '/' . $norujukan); if ($gadai->failed()) { @@ -671,6 +682,12 @@ class GadaianController extends Controller // $pembayaran_segera = "https://spare.arrahn.com.my/pembayaransegera"; $pembayaran_segera = "https://pay.arrahn.com.my"; + $shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']); + if ($shorakaPrint) { + $komoditi = $shorakaPrint['komoditi']; + $product = $shorakaPrint['product']; + $kuantiti = $shorakaPrint['kuantiti']; + } else { $komoditi = Http::withOptions(['verify' => config('app.api_verify')]) ->get($api_url . '/api/komuditi/check') ->json(); @@ -678,6 +695,7 @@ class GadaianController extends Controller ->get($api_url . '/api/komuditi/description', ['kodproduk' => $komoditi['kodproduct']]) ->json(); $kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai']; + } if ($gadai_detail['tagbaru'] == 1) { $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')]) diff --git a/app/Http/Controllers/PembiayaanOnlineController.php b/app/Http/Controllers/PembiayaanOnlineController.php index 18ea958c..1971c53b 100644 --- a/app/Http/Controllers/PembiayaanOnlineController.php +++ b/app/Http/Controllers/PembiayaanOnlineController.php @@ -522,6 +522,12 @@ class PembiayaanOnlineController extends Controller $pembayaran_segera = 'https://pay.arrahn.com.my'; + $shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']); + if ($shorakaPrint) { + $komoditi = $shorakaPrint['komoditi']; + $product = $shorakaPrint['product']; + $kuantiti = $shorakaPrint['kuantiti']; + } else { $komoditi = $this->api()->get($api_url . '/api/komuditi/check')->json(); $product = $this->api() ->get($api_url . '/api/komuditi/description', [ @@ -531,6 +537,7 @@ class PembiayaanOnlineController extends Controller $nilai = is_array($komoditi) ? (float) ($komoditi['nilai'] ?? 0) : 0; $kuantiti = $nilai > 0 ? ((float) ($gadai_detail['pinjaman'] ?? 0) / $nilai) : 0; + } if (($gadai_detail['tagbaru'] ?? 0) == 1) { $cawangan_info = $this->api() diff --git a/app/Http/Controllers/PenebusanController.php b/app/Http/Controllers/PenebusanController.php index 32ef7f7f..c5f5e446 100644 --- a/app/Http/Controllers/PenebusanController.php +++ b/app/Http/Controllers/PenebusanController.php @@ -271,6 +271,7 @@ class PenebusanController extends Controller public function tambahpinjaman(Request $request) { + app(\App\Services\ShorakaFinancingService::class)->ensureReady(); $api_url = config('api.url'); $current = Carbon::now(); $current = new Carbon(); @@ -405,33 +406,16 @@ class PenebusanController extends Controller 'upah12' => $keuntungan12bulan, 'hargajualan' => $hargajualan, 'kuantiti_marhun' => $request->kuantiti_marhun - ])->json(); - - $komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/komuditi/transaksi',[ - 'unit_komuditi' => round($request->pinjaman, 2), - 'norujukan' => $norujukanbaru, - 'kodcaw' => $auth_user['cawangan'], - 'teller' => $auth_user['name'] - ]); - - //////////////////////////// - ////Update History Gadaian/// - //////////////////////////// - - $updatehistorygadaian = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/gadai/updatehistory/'. $auth_user['cawangan'] .'/' . $norujukanbaru,[ - 'historygadaian' => $request->norujukan, - 'jenistebus' => $request->jenistebus, - ])->json(); - - - event(new TebusNotify('lulus',Auth::user()->cawangan)); - event(new GadaiNotify('lulus',Auth::user()->cawangan)); - $this->AliranTunaiController->alirantunaiTebus($request); + ])->throw()->json(); + return app(\App\Services\ShorakaFinancingService::class)->hold( + $request, $norujukanbaru, 'refinance', ['update_history' => true] + ); } public function tambahpinjaman_latest(Request $request) { + app(\App\Services\ShorakaFinancingService::class)->ensureReady(); $api_url = config('api.url'); $current = Carbon::now(); $current = new Carbon(); @@ -475,17 +459,18 @@ class PenebusanController extends Controller ])->json(); - if($updatetebus['result'] == 'success'){ - event(new TebusNotify('lulus',Auth::user()->cawangan)); - event(new GadaiNotify('lulus',Auth::user()->cawangan)); - $this->AliranTunaiController->alirantunaiTebus($request); + if (($updatetebus['result'] ?? null) === 'success') { + abort_unless(!empty($updatetebus['norujukan_baru']), 502, 'Rujukan gadaian baharu tidak diterima.'); + return app(\App\Services\ShorakaFinancingService::class)->hold( + $request, $updatetebus['norujukan_baru'], 'refinance' + ); } - return response()->json($updatetebus['result'],200); - + return response()->json($updatetebus['result'] ?? 'error', 200); } public function tebusautotawarruq(Request $request) { + app(\App\Services\ShorakaFinancingService::class)->ensureReady(); $auth_user = Auth::user(); //////////////////////// @@ -515,17 +500,18 @@ class PenebusanController extends Controller "noic" => $request->noic, "jenisAT" => $request->jenisAT, ])->json(); - if($updatetebus['result'] == 'success'){ - $alirantunai = new AliranTunaiController(); - event(new TebusNotify('lulus',Auth::user()->cawangan)); - event(new GadaiNotify('lulus',Auth::user()->cawangan)); - $alirantunai->alirantunaiTebusAutoTawarruq($request); + if (($updatetebus['result'] ?? null) === 'success') { + abort_unless(!empty($updatetebus['norujukan_baru']), 502, 'Rujukan gadaian baharu tidak diterima.'); + return app(\App\Services\ShorakaFinancingService::class)->hold( + $request, $updatetebus['norujukan_baru'], 'autotawarruq' + ); } - return response()->json($updatetebus['result'],200); + return response()->json($updatetebus['result'] ?? 'error', 200); } public function tebusseparuh(Request $request) { + app(\App\Services\ShorakaFinancingService::class)->ensureReady(); $api_url = config('api.url'); $current = Carbon::now(); $current = new Carbon(); @@ -666,39 +652,13 @@ class PenebusanController extends Controller 'upah12' => $keuntungan12bulan, 'hargajualan' => $hargajualan, 'kuantiti_marhun' => $request->kuantiti_marhun - ])->json(); + ])->throw()->json(); - $komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/komuditi/transaksi',[ - 'unit_komuditi' => round($request->pinjaman, 2), - 'norujukan' => $norujukanbaru, - 'kodcaw' => $auth_user['cawangan'], - 'teller' => $auth_user['name'] - ]); - - //////////////////////////// - ////Update History Gadaian/// - //////////////////////////// - - $updatehistorygadaian = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/gadai/updatehistory/'. $auth_user['cawangan'] .'/' . $norujukanbaru,[ - 'historygadaian' => $request->norujukan, - 'jenistebus' => $request->jenistebus, - ])->json(); - - - $alirantunai = new AliranTunaiController(); - event(new TebusNotify('lulus',Auth::user()->cawangan)); - event(new GadaiNotify('lulus',Auth::user()->cawangan)); - - $alirantunai->alirantunaiTebus($request); - - - return response()->json($updatemarhun,200); - + return app(\App\Services\ShorakaFinancingService::class)->hold( + $request, $norujukanbaru, 'refinance', ['update_history' => true] + ); } - - //Wakil Function - public function getfirstWakil() { $auth_user = Auth::user(); diff --git a/app/Http/Controllers/ShorakaController.php b/app/Http/Controllers/ShorakaController.php new file mode 100644 index 00000000..3c6dfb31 --- /dev/null +++ b/app/Http/Controllers/ShorakaController.php @@ -0,0 +1,73 @@ +cawangan) + ->orderByDesc('id')->limit(100)->get(); + $cawangan_info = ['details_caw' => Auth::user()->cawangan, 'kodcaw' => Auth::user()->cawangan]; + return view('shoraka.index', compact('records', 'cawangan_info')); + } + + public function show(string $norujukan, ShorakaFinancingService $service) + { + $record = $service->local($norujukan); + $data = null; + $error = null; + try { + $data = $service->api()->get($service->url($norujukan))->throw()->json()['data']; + } catch (Throwable $exception) { + $error = 'Maklumat Shoraka tidak dapat diambil. Bayaran dan SAG masih ditahan.'; + } + $cawangan_info = ['details_caw' => Auth::user()->cawangan, 'kodcaw' => Auth::user()->cawangan]; + return view('shoraka.show', compact('norujukan', 'record', 'data', 'error', 'cawangan_info')); + } + + public function submit(string $norujukan, ShorakaFinancingService $service) + { + $record = $service->local($norujukan); + abort_unless($record && $record->teller === Auth::user()->name, 403); + $response = $service->api()->post($service->url($norujukan, '/submit')); + return redirect()->route('shoraka.show', compact('norujukan')) + ->with('shoraka_message', $response->successful() + ? 'Order dihantar. Semak status sebelum menyelesaikan pembiayaan.' + : (($response->json()['message'] ?? null) ?: 'Order belum berjaya. Semak status sebelum cuba semula.')); + } + + public function refresh(string $norujukan, ShorakaFinancingService $service) + { + try { + $response = $service->api()->post($service->url($norujukan, '/refresh')); + $message = $response->successful() ? 'Status dikemas kini.' + : (($response->json()['message'] ?? null) ?: 'Status tidak dapat dikemas kini.'); + } catch (Throwable $exception) { + $message = 'Sambungan terganggu. Jangan hantar order semula.'; + } + return redirect()->route('shoraka.show', compact('norujukan'))->with('shoraka_message', $message); + } + + public function finish(Request $request, string $norujukan, ShorakaFinancingService $service) + { + return $service->finish($request, $norujukan); + } + + public function certificate(string $norujukan, ShorakaFinancingService $service) + { + $response = $service->api()->get($service->url($norujukan, '/certificate'))->throw(); + abort_unless(substr($response->body(), 0, 5) === '%PDF-', 502); + return response($response->body(), 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="sijil-shoraka.pdf"', + 'Cache-Control' => 'private, no-store', + ]); + } +} diff --git a/app/Services/ShorakaFinancingService.php b/app/Services/ShorakaFinancingService.php new file mode 100644 index 00000000..ded8ecd7 --- /dev/null +++ b/app/Services/ShorakaFinancingService.php @@ -0,0 +1,153 @@ +cawangan)->exists(); + $response = $this->api()->get(rtrim(config('api.url'), '/') . '/api/shoraka/readiness')->throw()->json(); + abort_unless(!empty($response['ready']), 503, 'Shoraka belum dikonfigurasi. Pembiayaan baharu ditahan.'); + } + + public function api() + { + return Http::withOptions(['verify' => config('app.api_verify')]) + ->acceptJson()->timeout(75); + } + + public function url(string $reference, string $suffix = ''): string + { + return rtrim(config('api.url'), '/') . '/api/shoraka/gadai/' + . rawurlencode(Auth::user()->cawangan) . '/' . rawurlencode($reference) . $suffix; + } + + public function local(string $reference) + { + return ShorakaFinancing::where('kodcaw', Auth::user()->cawangan) + ->where('norujukan', $reference)->first(); + } + + public function hold(Request $request, string $reference, string $action, array $extra = []) + { + $user = Auth::user(); + $record = ShorakaFinancing::firstOrCreate( + ['kodcaw' => $user->cawangan, 'norujukan' => $reference], + [ + 'teller' => $user->name, 'action' => $action, 'state' => 'WAITING', + 'context' => array_merge($request->except(['_token', 'password']), $extra), + ] + ); + // A duplicate request only opens the saved workflow; it never replays registration. + if ($record->wasRecentlyCreated) { + try { + $this->api()->post($this->url($reference, '/submit')); + } catch (Throwable $exception) { + // The persisted workflow remains available after timeouts. + } + } + return $this->pendingResponse($request, $reference); + } + + public function pendingResponse(Request $request, string $reference) + { + $url = route('shoraka.show', ['norujukan' => $reference]); + if ($request->ajax() || $request->expectsJson()) { + return response()->json(['shoraka_pending' => true, 'redirect' => $url], 202); + } + return redirect($url); + } + + public function finish(Request $request, string $reference) + { + $record = $this->local($reference); + abort_unless($record && $record->teller === Auth::user()->name, 403, + 'Pembiayaan perlu diselesaikan oleh teller asal.'); + if ($record->state === 'DONE') { + return redirect()->route('shoraka.show', ['norujukan' => $reference]); + } + abort_unless($record->state === 'WAITING', 409, + 'Penyelesaian telah diproses atau memerlukan semakan operasi. Jangan ulang bayaran.'); + + $this->api()->post($this->url($reference, '/refresh'))->throw(); + $snapshot = $this->api()->get($this->url($reference))->throw()->json()['data']; + abort_unless(!empty($snapshot['can_complete']), 409, + 'Bayaran ditahan sehingga Shoraka Completed dan amaun sepadan.'); + + abort_unless( + number_format((float) ($record->context['pinjaman'] ?? 0), 2, '.', '') + === number_format((float) ($snapshot['order_amount'] ?? 0), 2, '.', ''), + 409, 'Amaun pembiayaan berubah. Semakan operasi diperlukan sebelum bayaran.' + ); + + $claimed = ShorakaFinancing::where('id', $record->id)->where('state', 'WAITING') + ->update(['state' => 'PROCESSING', 'updated_at' => now()]); + abort_unless($claimed === 1, 409, 'Penyelesaian sedang diproses.'); + + $saved = new Request($record->context); + $saved->setLaravelSession($request->session()); + try { + if ($record->action === 'gadai') { + $result = app(GadaianController::class)->completeShorakaFinancing($saved); + } else { + if (!empty($record->context['update_history'])) { + $this->api()->put(rtrim(config('api.url'), '/') . '/api/gadai/updatehistory/' + . rawurlencode($record->kodcaw) . '/' . rawurlencode($reference), [ + 'historygadaian' => $saved->norujukan, + 'jenistebus' => $saved->jenistebus, + ])->throw(); + } + $cash = app(AliranTunaiController::class); + if ($record->action === 'autotawarruq') { + $cash->alirantunaiTebusAutoTawarruq($saved); + } else { + $cash->alirantunaiTebus($saved); + } + event(new \App\Events\Dashboard\TebusNotify('lulus', $record->kodcaw)); + event(new GadaiNotify('lulus', $record->kodcaw)); + $result = redirect()->route('shoraka.show', ['norujukan' => $reference]); + } + $record->update(['state' => 'DONE', 'context' => []]); + return $result; + } catch (Throwable $exception) { + // A failed remote payment response can be ambiguous. Never retry automatically. + $record->update(['state' => 'REVIEW']); + throw $exception; + } + } + + public function printData(string $reference): ?array + { + $response = $this->api()->get($this->url($reference))->throw()->json()['data']; + $order = $response['order'] ?? null; + if (!$order) { + abort_if($this->local($reference), 409, 'SAG ditahan: transaksi Shoraka belum tersedia.'); + return null; // Historical financing keeps its historical print path. + } + abort_unless(!empty($response['can_complete']), 409, 'SAG ditahan: Shoraka belum Completed.'); + $details = $response['details'] ?? []; + $certificate = explode('|', $details['certificateDetails1'] ?? '')[0]; + abort_unless($certificate !== '' && isset($details['unit'], $details['volume'], $details['commodityType']), + 409, 'Maklumat sijil belum lengkap. Semak status Shoraka dahulu.'); + return [ + 'komoditi' => ['no_sijil' => $certificate], + 'product' => [ + 'kodproduk' => $details['commodityType'], + 'description' => $details['commodityType'] === '000-COPPER' ? 'Copper' : 'Plastic resin', + 'unit' => $details['unit'], + ], + 'kuantiti' => $details['volume'], + ]; + } +} diff --git a/app/ShorakaFinancing.php b/app/ShorakaFinancing.php new file mode 100644 index 00000000..3e1e8564 --- /dev/null +++ b/app/ShorakaFinancing.php @@ -0,0 +1,11 @@ + 'array']; +} diff --git a/database/migrations/2026_10_05_120000_create_shoraka_financings_table.php b/database/migrations/2026_10_05_120000_create_shoraka_financings_table.php new file mode 100644 index 00000000..1e4090bd --- /dev/null +++ b/database/migrations/2026_10_05_120000_create_shoraka_financings_table.php @@ -0,0 +1,28 @@ +bigIncrements('id'); + $table->string('kodcaw', 20); + $table->string('norujukan'); + $table->string('teller'); + $table->string('action', 30); + $table->string('state', 30)->default('WAITING'); + $table->json('context'); + $table->timestamps(); + $table->unique(['kodcaw', 'norujukan']); + }); + } + + public function down() + { + Schema::dropIfExists('shoraka_financings'); + } +} diff --git a/resources/views/callcenter/v2/index.blade.php b/resources/views/callcenter/v2/index.blade.php index e0dab1a0..bd13e2bf 100644 --- a/resources/views/callcenter/v2/index.blade.php +++ b/resources/views/callcenter/v2/index.blade.php @@ -2814,25 +2814,25 @@ $.ajax({ method:'get', - url: api + '/api/komuditi/check', + url: api + '/api/shoraka/readiness', success:function(komoditi){ var check_komuditi = Object.keys(komoditi).length; - if(check_komuditi == 0){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komuditi Berbaki Minumum', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; }else{ - if(Number(komoditi['unit_komuditi']) < pinjaman_baru){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Baki Unit Komuditi Tidak Mencukupi', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; diff --git a/resources/views/customers/v1/info.blade.php b/resources/views/customers/v1/info.blade.php index 7a70644c..f1e2ddf1 100644 --- a/resources/views/customers/v1/info.blade.php +++ b/resources/views/customers/v1/info.blade.php @@ -461,25 +461,25 @@ }else{ $.ajax({ method:'get', - url: api_url + '/api/komuditi/check', + url: api_url + '/api/shoraka/readiness', success:function(data){ var check_komuditi = Object.keys(data).length; - if(check_komuditi == 0){ + if (!data.ready){ Swal.fire({ icon: 'error', - title: 'Unit Komoditi Berbaki Minima', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); $("#daftar_gadai").prop('disabled', false); event.preventDefault(); return false; }else{ - if(Number(data['unit_komuditi']) <= min_komoditi){ + if (!data.ready){ $("#daftar_gadai").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komoditi Berbaki Minima', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; diff --git a/resources/views/customers/v2/info.blade.php b/resources/views/customers/v2/info.blade.php index 9ab201a7..4d1cb40d 100644 --- a/resources/views/customers/v2/info.blade.php +++ b/resources/views/customers/v2/info.blade.php @@ -758,25 +758,25 @@ } else { $.ajax({ method: 'get', - url: api_url + '/api/komuditi/check', + url: api_url + '/api/shoraka/readiness', success: function(data) { var check_komuditi = Object.keys(data).length; - if (check_komuditi == 0) { + if (!data.ready) { Swal.fire({ icon: 'error', - title: 'Unit Komoditi Berbaki Minima', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); $("#daftar_gadai").prop('disabled', false); event.preventDefault(); return false; } else { - if (Number(data['unit_komuditi']) <= min_komoditi) { + if (!data.ready) { $("#daftar_gadai").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komoditi Berbaki Minima', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; diff --git a/resources/views/gadaian/v1/index.blade.php b/resources/views/gadaian/v1/index.blade.php index a4be210d..7aa435ed 100644 --- a/resources/views/gadaian/v1/index.blade.php +++ b/resources/views/gadaian/v1/index.blade.php @@ -819,25 +819,25 @@ }else{ $.ajax({ method:'get', - url: api_url + '/api/komuditi/check', + url: api_url + '/api/shoraka/readiness', success:function(data){ var check_komuditi = Object.keys(data).length; - if(check_komuditi == 0){ + if (!data.ready){ $("#terima_gadai").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komuditi Berbaki Minumum', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; }else{ - if(Number(data['unit_komuditi']) < pinjaman){ + if (!data.ready){ $("#terima_gadai").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Baki Unit Komuditi Tidak Mencukupi', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; diff --git a/resources/views/gadaian/v2/index.blade.php b/resources/views/gadaian/v2/index.blade.php index 57a5ae45..b01646e6 100644 --- a/resources/views/gadaian/v2/index.blade.php +++ b/resources/views/gadaian/v2/index.blade.php @@ -1655,25 +1655,25 @@ }else{ $.ajax({ method:'get', - url: api_url + '/api/komuditi/check', + url: api_url + '/api/shoraka/readiness', success:function(data){ var check_komuditi = Object.keys(data).length; - if(check_komuditi == 0){ + if (!data.ready){ $("#terima_gadai").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komuditi Berbaki Minumum', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; }else{ - if(Number(data['unit_komuditi']) < pinjaman){ + if (!data.ready){ $("#terima_gadai").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Baki Unit Komuditi Tidak Mencukupi', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index d7d138b2..948c78c3 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -405,6 +405,7 @@ @endif
+ @include('shoraka.navigation') @yield('content')
diff --git a/resources/views/penebusan/v1/index.blade.php b/resources/views/penebusan/v1/index.blade.php index a43f13de..94c81ed5 100644 --- a/resources/views/penebusan/v1/index.blade.php +++ b/resources/views/penebusan/v1/index.blade.php @@ -2022,25 +2022,25 @@ $.ajax({ method:'get', - url: api + '/api/komuditi/check', + url: api + '/api/shoraka/readiness', success:function(komoditi){ var check_komuditi = Object.keys(komoditi).length; - if(check_komuditi == 0){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komuditi Berbaki Minumum', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; }else{ - if(Number(komoditi['unit_komuditi']) < pinjaman_baru){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Baki Unit Komuditi Tidak Mencukupi', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; diff --git a/resources/views/penebusan/v2/index-khazanah.blade.php b/resources/views/penebusan/v2/index-khazanah.blade.php index 54a367fa..18e23193 100644 --- a/resources/views/penebusan/v2/index-khazanah.blade.php +++ b/resources/views/penebusan/v2/index-khazanah.blade.php @@ -2258,25 +2258,25 @@ $.ajax({ method:'get', - url: api + '/api/komuditi/check', + url: api + '/api/shoraka/readiness', success:function(komoditi){ var check_komuditi = Object.keys(komoditi).length; - if(check_komuditi == 0){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komuditi Berbaki Minumum', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; }else{ - if(Number(komoditi['unit_komuditi']) < pinjaman_baru){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Baki Unit Komuditi Tidak Mencukupi', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; diff --git a/resources/views/penebusan/v2/index.blade.php b/resources/views/penebusan/v2/index.blade.php index a3b75d77..14a40473 100644 --- a/resources/views/penebusan/v2/index.blade.php +++ b/resources/views/penebusan/v2/index.blade.php @@ -2264,27 +2264,27 @@ let komuditiblock = false; $.ajax({ method:'get', - url: api + '/api/komuditi/check', + url: api + '/api/shoraka/readiness', async:false, success:function(komoditi){ var check_komuditi = Object.keys(komoditi).length; - if(check_komuditi == 0){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komuditi Berbaki Minumum', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); komuditiblock = true; event.preventDefault(); return false; }else{ - if(Number(komoditi['unit_komuditi']) < pinjaman_baru){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Baki Unit Komuditi Tidak Mencukupi', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); komuditiblock = true; event.preventDefault(); @@ -2646,29 +2646,29 @@ let komuditiblock = false; $.ajax({ method:'get', - url: api + '/api/komuditi/check', + url: api + '/api/shoraka/readiness', async:false, success:function(komoditi){ var check_komuditi = Object.keys(komoditi).length; - if(check_komuditi == 0){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.close(); Swal.fire({ icon: 'error', - title: 'Unit Komuditi Berbaki Minumum', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); komuditiblock = true; event.preventDefault(); return false; }else{ - if(Number(komoditi['unit_komuditi']) < pinjaman_baru){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.close(); Swal.fire({ icon: 'error', - title: 'Baki Unit Komuditi Tidak Mencukupi', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); komuditiblock = true; event.preventDefault(); @@ -3106,25 +3106,25 @@ $.ajax({ method:'get', - url: api + '/api/komuditi/check', + url: api + '/api/shoraka/readiness', success:function(komoditi){ var check_komuditi = Object.keys(komoditi).length; - if(check_komuditi == 0){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Unit Komuditi Berbaki Minumum', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; }else{ - if(Number(komoditi['unit_komuditi']) < pinjaman_baru){ + if (!komoditi.ready){ $("#terima_transaksi").prop('disabled', false); Swal.fire({ icon: 'error', - title: 'Baki Unit Komuditi Tidak Mencukupi', - text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi' + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); event.preventDefault(); return false; diff --git a/resources/views/print/sag.blade.php b/resources/views/print/sag.blade.php index 8f2267a4..2ef1d719 100644 --- a/resources/views/print/sag.blade.php +++ b/resources/views/print/sag.blade.php @@ -556,9 +556,16 @@ @php - $komuditi = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/check')->json(); + $shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']); + if ($shorakaPrint) { + $komuditi = $shorakaPrint['komoditi']; + $product = $shorakaPrint['product']; + $kuantiti = $shorakaPrint['kuantiti']; + } else { +$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/check')->json(); $product = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/description',['kodproduk'=>$komuditi['kodproduct']])->json(); $kuantiti = $gadai_detail['pinjaman']/$komuditi['nilai']; + } @endphp diff --git a/resources/views/print/sag_ujrah.blade.php b/resources/views/print/sag_ujrah.blade.php index 8844952e..d0d6cfd5 100644 --- a/resources/views/print/sag_ujrah.blade.php +++ b/resources/views/print/sag_ujrah.blade.php @@ -654,13 +654,20 @@ @php - $komuditi = Http::withOptions(['verify' => config('app.api_verify')]) + $shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']); + if ($shorakaPrint) { + $komuditi = $shorakaPrint['komoditi']; + $product = $shorakaPrint['product']; + $kuantiti = $shorakaPrint['kuantiti']; + } else { +$komuditi = Http::withOptions(['verify' => config('app.api_verify')]) ->get($api_url . '/api/komuditi/check') ->json(); $product = Http::withOptions(['verify' => config('app.api_verify')]) ->get($api_url . '/api/komuditi/description', ['kodproduk' => $komuditi['kodproduct']]) ->json(); $kuantiti = $gadai_detail['pinjaman'] / $komuditi['nilai']; + } @endphp
diff --git a/resources/views/print/sag_ujrah_2.blade.php b/resources/views/print/sag_ujrah_2.blade.php index 923c9007..342af026 100644 --- a/resources/views/print/sag_ujrah_2.blade.php +++ b/resources/views/print/sag_ujrah_2.blade.php @@ -713,13 +713,20 @@
@php - $komuditi = Http::withOptions(['verify' => config('app.api_verify')]) + $shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']); + if ($shorakaPrint) { + $komuditi = $shorakaPrint['komoditi']; + $product = $shorakaPrint['product']; + $kuantiti = $shorakaPrint['kuantiti']; + } else { +$komuditi = Http::withOptions(['verify' => config('app.api_verify')]) ->get($api_url . '/api/komuditi/check') ->json(); $product = Http::withOptions(['verify' => config('app.api_verify')]) ->get($api_url . '/api/komuditi/description', ['kodproduk' => $komuditi['kodproduct']]) ->json(); $kuantiti = $gadai_detail['pinjaman'] / $komuditi['nilai']; + } @endphp diff --git a/resources/views/shoraka/index.blade.php b/resources/views/shoraka/index.blade.php new file mode 100644 index 00000000..f5a60e66 --- /dev/null +++ b/resources/views/shoraka/index.blade.php @@ -0,0 +1,21 @@ +@extends('layouts.app') +@section('content') +
+

Pembiayaan Shoraka

+

Semak transaksi komoditi dan selesaikan pembiayaan selepas status Completed.

+
+ + + @forelse($records as $record) + + + + + + @empty + + @endforelse + +
No. rujukanTellerPenyelesaian pembiayaan
{{ $record->norujukan }}{{ $record->teller }}{{ ['WAITING' => 'Menunggu Shoraka / penyelesaian', 'PROCESSING' => 'Sedang diproses', 'REVIEW' => 'Perlu semakan operasi', 'DONE' => 'Selesai'][$record->state] ?? $record->state }}Buka
Tiada transaksi.
+ +@endsection diff --git a/resources/views/shoraka/navigation.blade.php b/resources/views/shoraka/navigation.blade.php new file mode 100644 index 00000000..4953cb22 --- /dev/null +++ b/resources/views/shoraka/navigation.blade.php @@ -0,0 +1,27 @@ +@auth +@if(in_array(Auth::user()->roles, ['teller', 'penilai', 'pc', 'ppc', 'admin', 'masteradmin', 'operasi']) || Auth::user()->role_harian === 'teller') +
+ Pembiayaan Shoraka +
+@endif +@endauth + diff --git a/resources/views/shoraka/show.blade.php b/resources/views/shoraka/show.blade.php new file mode 100644 index 00000000..ad6e7f97 --- /dev/null +++ b/resources/views/shoraka/show.blade.php @@ -0,0 +1,57 @@ +@extends('layouts.app') +@section('content') +
+ Kembali ke senarai +

Komoditi Shoraka — {{ $norujukan }}

+ @if($error)
{{ $error }}
@endif + @if(session('shoraka_message'))
{{ session('shoraka_message') }}
@endif + @php + $order = $data['order'] ?? null; + $status = $order['status'] ?? 'Belum dihantar'; + $ready = !empty($data['can_complete']); + @endphp +
+
Status komoditi
{{ $status }}
+
Order ID
{{ $order['order_id'] ?? '—' }}
+
Pembiayaan
RM {{ number_format($data['order_amount'] ?? 0, 2) }}
+
Harga jualan
RM {{ number_format($data['murabaha_amount'] ?? 0, 2) }}
+
+ @if(!$ready) +
Bayaran pembiayaan dan SAG ditahan sehingga transaksi komoditi Completed.
+ @endif + @if(in_array($status, ['UNKNOWN', 'FAILED', 'PENDING'])) +

Jangan hantar order sekali lagi. Hubungi operasi untuk semakan dengan Shoraka.

+ @endif + @if($record && in_array($record->state, ['REVIEW', 'PROCESSING'])) +
Penyelesaian sedang diproses atau memerlukan semakan operasi. Jangan ulang bayaran.
+ @elseif($record && $record->state === 'DONE') +
Pembiayaan telah diselesaikan.
+ @endif +
+ @if($order && !empty($order['order_id'])) +
@csrf + +
+ @elseif($data && !$order && $record && $record->teller === Auth::user()->name) +
@csrf + +
+ @endif + @if($ready && $record && $record->state === 'WAITING' && $record->teller === Auth::user()->name) +
@csrf + +
+ @endif + @if($ready) + Muat Turun Sijil + @endif +
+
+ +@endsection diff --git a/routes/web.php b/routes/web.php index 64bf6c4c..ad6b00b4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -605,4 +605,12 @@ Route::post('/pampasan/terima',['as' => 'pampasan.terima', 'uses' => 'KhazanahCo Route::get(('/pampasan/resit'), ['as' => 'pampasan.resit', 'uses' => 'KhazanahController@resitPampasan'])->middleware('auth', 'khazanah'); //Surat Pampasan -Route::get('/pampasan/surat/{norujukan}', ['as' => 'pampasan.surat', 'uses' => 'KhazanahController@suratPampasan'])->middleware('auth', 'role:teller,khazanah'); \ No newline at end of file +Route::get('/pampasan/surat/{norujukan}', ['as' => 'pampasan.surat', 'uses' => 'KhazanahController@suratPampasan'])->middleware('auth', 'role:teller,khazanah'); +Route::group(['prefix' => 'shoraka', 'middleware' => ['auth', 'role:teller,penilai,khazanah,admin,masteradmin,operasi']], function () { + Route::get('/', 'ShorakaController@index')->name('shoraka.index'); + Route::get('/{norujukan}', 'ShorakaController@show')->name('shoraka.show'); + Route::post('/{norujukan}/submit', 'ShorakaController@submit')->name('shoraka.submit'); + Route::post('/{norujukan}/refresh', 'ShorakaController@refresh')->name('shoraka.refresh'); + Route::post('/{norujukan}/finish', 'ShorakaController@finish')->name('shoraka.finish'); + Route::get('/{norujukan}/certificate', 'ShorakaController@certificate')->name('shoraka.certificate'); +}); diff --git a/tests/Feature/ShorakaFinancingTest.php b/tests/Feature/ShorakaFinancingTest.php new file mode 100644 index 00000000..4fc80192 --- /dev/null +++ b/tests/Feature/ShorakaFinancingTest.php @@ -0,0 +1,238 @@ + 'shoraka_test', + 'database.connections.shoraka_test' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''], + 'api.url' => 'https://api.test', 'session.driver' => 'array', + ]); + DB::purge('shoraka_test'); + require_once database_path('migrations/2026_10_05_120000_create_shoraka_financings_table.php'); + (new \CreateShorakaFinancingsTable())->up(); + $this->actingAs(new User(['name' => 'teller1', 'cawangan' => 'KB2', 'roles' => 'teller'])); + Http::swap(new Factory()); + } + + protected function record(): ShorakaFinancing + { + return ShorakaFinancing::create([ + 'kodcaw' => 'KB2', 'norujukan' => 'SAG-001', 'teller' => 'teller1', + 'action' => 'gadai', 'state' => 'WAITING', + 'context' => ['norujukan_gadai' => 'SAG-001', 'pinjaman' => 1000, 'cara_terima' => 'TUNAI'], + ]); + } + + protected function request(): Request + { + $request = new Request(['norujukan_gadai' => 'SAG-001', 'pinjaman' => 1000, '_token' => 'private']); + $request->setLaravelSession(app('session.store')); + return $request; + } + + public function testHoldPersistsAndNeverSubmitsTwice(): void + { + Http::fake(function ($request) { + $this->assertSame('https://api.test/api/shoraka/gadai/KB2/SAG-001/submit', $request->url()); + return Http::response(['success' => true, 'data' => ['status' => 'Active']]); + }); + $service = new ShorakaFinancingService(); + $service->hold($this->request(), 'SAG-001', 'gadai'); + $service->hold($this->request(), 'SAG-001', 'gadai'); + $this->assertSame(1, ShorakaFinancing::count()); + $this->assertSame('WAITING', ShorakaFinancing::first()->state); + $this->assertArrayNotHasKey('_token', ShorakaFinancing::first()->context); + Http::assertSentCount(1); + } + + public function testPendingStatusCannotTriggerPayment(): void + { + $record = $this->record(); + Http::fake(function () { return Http::response(['data' => ['can_complete' => false]]); }); + $controller = Mockery::mock(GadaianController::class); + $controller->shouldNotReceive('completeShorakaFinancing'); + $this->app->instance(GadaianController::class, $controller); + try { + (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); + $this->fail('Pending trade must block payment.'); + } catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) { + $this->assertSame(409, $exception->getStatusCode()); + $this->assertSame('WAITING', $record->fresh()->state); + } + } + + public function testCompletedTradePaysOnlyOnce(): void + { + $record = $this->record(); + Http::fake(function () { return Http::response(['data' => ['can_complete' => true, 'order_amount' => 1000]]); }); + $controller = Mockery::mock(GadaianController::class); + $controller->shouldReceive('completeShorakaFinancing')->once()->andReturn(redirect('/')); + $this->app->instance(GadaianController::class, $controller); + $service = new ShorakaFinancingService(); + $service->finish($this->request(), 'SAG-001'); + $service->finish($this->request(), 'SAG-001'); + $this->assertSame('DONE', $record->fresh()->state); + $this->assertSame([], $record->fresh()->context); + Http::assertSentCount(2); + } + + public function testAmbiguousPaymentFailureRequiresReview(): void + { + $record = $this->record(); + Http::fake(function () { return Http::response(['data' => ['can_complete' => true, 'order_amount' => 1000]]); }); + $controller = Mockery::mock(GadaianController::class); + $controller->shouldReceive('completeShorakaFinancing')->once()->andThrow(new \RuntimeException('Timeout')); + $this->app->instance(GadaianController::class, $controller); + try { + (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); + $this->fail('Expected payment failure.'); + } catch (\RuntimeException $exception) { + $this->assertSame('REVIEW', $record->fresh()->state); + } + $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class); + (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); + } + + public function testDifferentTellerCannotCompletePayment(): void + { + $record = $this->record(); + $record->update(['teller' => 'someone-else']); + Http::fake(); + try { + (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); + $this->fail('Expected teller restriction.'); + } catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) { + $this->assertSame(403, $exception->getStatusCode()); + } + Http::assertNothingSent(); + } + + public function testPendingTradeCannotPrintSag(): void + { + $this->record(); + Http::fake(function () { return Http::response(['data' => [ + 'can_complete' => false, 'order' => ['status' => 'Active'], + ]]); }); + $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class); + (new ShorakaFinancingService())->printData('SAG-001'); + } + + public function testPrintUsesOrderCertificateAndVolume(): void + { + Http::fake(function () { return Http::response(['data' => [ + 'can_complete' => true, 'order' => ['status' => 'Completed'], + 'details' => ['certificateDetails1' => 'CERT-01|timestamp|seller', 'unit' => 'Tonnes', + 'volume' => '1.25', 'commodityType' => '000-COPPER'], + ]]); }); + $data = (new ShorakaFinancingService())->printData('SAG-001'); + $this->assertSame('CERT-01', $data['komoditi']['no_sijil']); + $this->assertSame('1.25', $data['kuantiti']); + } + + public function testBranchComesFromAuthenticatedUser(): void + { + $service = new ShorakaFinancingService(); + $this->assertSame('https://api.test/api/shoraka/gadai/KB2/SAG-001', $service->url('SAG-001')); + $record = $this->record(); + $record->update(['kodcaw' => 'KB3']); + $this->assertNull($service->local('SAG-001')); + } + + public function testRegistrationDefersCashAndOnlinePayment(): void + { + \Illuminate\Support\Facades\Event::fake(); + Http::fake(function ($request) { + if (strpos($request->url(), '/shoraka/readiness') !== false) { + return Http::response(['ready' => true]); + } + if (strpos($request->url(), '/marhun/gadai/') !== false) { + return Http::response('2000.00'); + } + if (strpos($request->url(), '/gadai/norujukan/') !== false) { + return Http::response('true'); + } + if (strpos($request->url(), '/shoraka/gadai/') !== false) { + return Http::response(['success' => true, 'data' => ['status' => 'Active']]); + } + $this->fail('Unexpected payment request: ' . $request->url()); + }); + $request = $this->request(); + $request->merge([ + 'cara_terima' => 'TUNAI', 'keuntungan_sebulan' => 10, + 'kadarkeuntungan' => 1, 'total_berat' => 5, 'nokp' => 'IC-1', + ]); + $result = app(GadaianController::class)->store($request); + $this->assertStringContainsString('/shoraka/SAG-001', $result->getTargetUrl()); + $this->assertSame('WAITING', ShorakaFinancing::first()->state); + app(GadaianController::class)->store($request); + Http::assertSentCount(4); + \Illuminate\Support\Facades\Event::assertNotDispatched(\App\Events\Dashboard\GadaiNotify::class); + } + + public function testReadinessFailureBlocksRegistrationBeforeSave(): void + { + Http::fake(function () { return Http::response(['ready' => false]); }); + try { + app(GadaianController::class)->store($this->request()); + $this->fail('Registration should be blocked.'); + } catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) { + $this->assertSame(503, $exception->getStatusCode()); + } + Http::assertSentCount(1); + $this->assertSame(0, ShorakaFinancing::count()); + } + + public function testRefinancingUsesNewReferenceAndDefersCash(): void + { + \Illuminate\Support\Facades\Event::fake(); + Http::fake(function ($request) { + if (strpos($request->url(), '/shoraka/readiness') !== false) { + return Http::response(['ready' => true]); + } + if (strpos($request->url(), '/updatetambahpinjaman/') !== false) { + return Http::response(['result' => 'success', 'norujukan_baru' => 'NEW-SAG']); + } + if (strpos($request->url(), '/shoraka/gadai/KB2/NEW-SAG/submit') !== false) { + return Http::response(['success' => true]); + } + $this->fail('Unexpected cash/legacy commodity request: ' . $request->url()); + }); + $request = $this->request(); + $request->merge(['norujukan' => 'OLD-SAG']); + $request->headers->set('X-Requested-With', 'XMLHttpRequest'); + $result = app(\App\Http\Controllers\PenebusanController::class)->tambahpinjaman_latest($request); + $this->assertSame(202, $result->getStatusCode()); + $this->assertSame('NEW-SAG', ShorakaFinancing::first()->norujukan); + $this->assertSame('refinance', ShorakaFinancing::first()->action); + \Illuminate\Support\Facades\Event::assertNotDispatched(\App\Events\Dashboard\GadaiNotify::class); + } + + public function testChangedSavedPayoutAmountCannotBePaid(): void + { + $this->record(); + Http::fake(function () { return Http::response(['data' => [ + 'can_complete' => true, 'order_amount' => 2000, + ]]); }); + $controller = Mockery::mock(GadaianController::class); + $controller->shouldNotReceive('completeShorakaFinancing'); + $this->app->instance(GadaianController::class, $controller); + $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class); + (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); + } +}