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(); $context = array_merge($request->except(['_token', 'password']), $extra); if (!empty($extra['temporary_gadai'])) { $key = 'shoraka:payout:' . hash('sha256', $user->cawangan . ':' . $reference); $temporaryContext = $context; $context = ['temporary_gadai' => true, 'pinjaman' => $extra['pinjaman']]; } $record = ShorakaFinancing::firstOrCreate( ['kodcaw' => $user->cawangan, 'norujukan' => $reference], [ 'teller' => $user->name, 'action' => $action, 'state' => 'WAITING', 'context' => $context, ] ); // A duplicate request only opens the saved workflow; it never replays registration. if ($record->wasRecentlyCreated) { if (isset($temporaryContext) && !Cache::store('file')->put($key, $temporaryContext, 604800)) { $record->update(['state' => 'REVIEW']); abort(503, 'Maklumat pembiayaan sementara tidak dapat disimpan.'); } 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.' ); $context = $record->context; if (!empty($context['temporary_gadai'])) { $context = Cache::store('file')->get('shoraka:payout:' . hash('sha256', $record->kodcaw . ':' . $reference)); abort_unless(is_array($context), 409, 'Maklumat sementara tamat. 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($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' => []]); if (!empty($context['temporary_gadai'])) { Cache::store('file')->forget('shoraka:payout:' . hash('sha256', $record->kodcaw . ':' . $reference)); } 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'] ?? []; // Shoraka may return a list of pipe-delimited certificate details. $certificateDetails = $details['certificateDetails1'] ?? ''; if (is_array($certificateDetails)) { $certificateDetails = $certificateDetails[0] ?? ''; } $certificate = is_string($certificateDetails) ? trim(explode('|', $certificateDetails)[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'], ]; } }