diff --git a/.env.annur b/.env.annur
index 057f883f..90993f00 100644
--- a/.env.annur
+++ b/.env.annur
@@ -47,4 +47,7 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
-MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
\ No newline at end of file
+MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
+
+COMMODITY_SHORAKA_ENABLED=true
+COMMODITY_BSAS_ENABLED=true
diff --git a/.env.docker b/.env.docker
index b7f9519a..db6c6098 100644
--- a/.env.docker
+++ b/.env.docker
@@ -53,4 +53,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
-INTERNAL_NOTIFICATION_SECRET=erahn-local-notification-secret
\ No newline at end of file
+INTERNAL_NOTIFICATION_SECRET=erahn-local-notification-secret
+
+COMMODITY_SHORAKA_ENABLED=true
+COMMODITY_BSAS_ENABLED=true
\ No newline at end of file
diff --git a/.env.domain b/.env.domain
index 1cfb20ce..3e238b7e 100644
--- a/.env.domain
+++ b/.env.domain
@@ -52,4 +52,7 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
-MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
\ No newline at end of file
+MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
+
+COMMODITY_SHORAKA_ENABLED=true
+COMMODITY_BSAS_ENABLED=true
diff --git a/.env.example b/.env.example
index 25665a2d..6bdf372b 100644
--- a/.env.example
+++ b/.env.example
@@ -52,3 +52,8 @@ MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET=
LOGVIEWER_PASSWORD=
+
+# Commodity providers available when a teller submits gadai.
+# Turn either or both on. When both are true, the teller chooses on the form.
+COMMODITY_SHORAKA_ENABLED=true
+COMMODITY_BSAS_ENABLED=true
diff --git a/.env.production b/.env.production
index 020d9fd0..b556af48 100644
--- a/.env.production
+++ b/.env.production
@@ -50,4 +50,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN=''
-INTERNAL_NOTIFICATION_SECRET="nIDTe(u@6[)VLJyd"
\ No newline at end of file
+INTERNAL_NOTIFICATION_SECRET="nIDTe(u@6[)VLJyd"
+
+COMMODITY_SHORAKA_ENABLED=true
+COMMODITY_BSAS_ENABLED=true
\ No newline at end of file
diff --git a/.env.staging b/.env.staging
index bdf6ddc6..4d85c65e 100644
--- a/.env.staging
+++ b/.env.staging
@@ -54,4 +54,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN=''
-INTERNAL_NOTIFICATION_SECRET=staging_noti_secret
\ No newline at end of file
+INTERNAL_NOTIFICATION_SECRET=staging_noti_secret
+
+COMMODITY_SHORAKA_ENABLED=true
+COMMODITY_BSAS_ENABLED=true
\ No newline at end of file
diff --git a/app/Http/Controllers/GadaianController.php b/app/Http/Controllers/GadaianController.php
index 041d0a9b..9a2db761 100644
--- a/app/Http/Controllers/GadaianController.php
+++ b/app/Http/Controllers/GadaianController.php
@@ -285,15 +285,24 @@ class GadaianController extends Controller
public function store(Request $request)
{
$auth_user = Auth::user();
- $shoraka = app(\App\Services\ShorakaFinancingService::class);
- if ($shoraka->hasOrder($request->norujukan_gadai)) {
- $message = 'Order Shoraka telah dihantar. Jangan hantar semula.';
- if ($request->ajax() || $request->expectsJson()) {
- abort(409, $message);
- }
- return redirect()->back()->with('error', $message);
+ if (!config('commodity.shoraka') && !config('commodity.bsas')) {
+ return $this->gadaiStoreResult($request, false, 'Tiada pembekal komoditi diaktifkan.');
}
- $shoraka->ensureReady();
+
+ $provider = $this->selectedCommodityProvider($request);
+ if ($provider === null) {
+ return $this->gadaiStoreResult($request, false, 'Sila pilih pembekal komoditi yang diaktifkan.');
+ }
+
+ $shoraka = null;
+ if ($provider === 'shoraka') {
+ $shoraka = app(\App\Services\ShorakaFinancingService::class);
+ if ($shoraka->hasOrder($request->norujukan_gadai)) {
+ return $this->gadaiStoreResult($request, false, 'Order Shoraka telah dihantar. Jangan hantar semula.');
+ }
+ $shoraka->ensureReady();
+ }
+
$caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI'));
$pinjamanAmt = round((float) $request->pinjaman, 2);
@@ -307,7 +316,7 @@ class GadaianController extends Controller
}
if ($caraTerima === 'ONLINE' && !$this->hasMatchingBorangOnlinePrint($request)) {
- return redirect()->back()->with('error', 'Sila cetak borang pengesahan pelanggan sebelum tekan Terima.');
+ return $this->gadaiStoreResult($request, false, 'Sila cetak borang pengesahan pelanggan sebelum tekan Terima.');
}
//$total_nilai_marhun = $request->total_nilai_marhun;
@@ -329,6 +338,28 @@ class GadaianController extends Controller
$keuntungan12bulan = $keuntungan_sebln * 12;
$hargajualan = $request->pinjaman + $keuntungan12bulan;
+
+ if ($provider === 'bsas') {
+ $bsas = Http::withOptions(['verify' => config('app.api_verify')])
+ ->timeout(60)
+ ->post(config('api.url') . '/api/komuditi/bsas/order', [
+ 'norujukan' => $request->norujukan_gadai,
+ 'kodcaw' => $auth_user['cawangan'],
+ 'pinjaman' => round((float) $request->pinjaman, 2),
+ 'hargajualan' => round((float) $hargajualan, 2),
+ 'nama_pelanggan' => $request->nama_pelanggan ?: $request->nama_akaun,
+ 'teller' => $auth_user['name'],
+ ]);
+
+ if (!$bsas->successful()) {
+ $body = $bsas->json();
+ $msg = is_array($body) && isset($body['message'])
+ ? $body['message']
+ : 'Gagal pembelian komoditi BSAS. Sila cuba lagi.';
+ return $this->gadaiStoreResult($request, false, $msg);
+ }
+ }
+
//Log::info('DEBUG NOKP:' . $request->nokp);
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/' . $auth_user['cawangan'] . '/' . $request->norujukan_gadai, [
'kodcaw' => $auth_user['cawangan'],
@@ -353,12 +384,20 @@ class GadaianController extends Controller
'rebate' => $this->parseMoney($request->rebate),
])->throw()->json();
+ if ($provider === 'shoraka') {
+ return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [
+ 'cara_terima' => $caraTerima,
+ 'pinjaman' => $pinjamanAmt,
+ 'temporary_gadai' => true,
+ ]);
+ }
- return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [
- 'cara_terima' => $caraTerima,
- 'pinjaman' => $pinjamanAmt,
- 'temporary_gadai' => true,
- ]);
+ $error = $this->disburseGadai($request, $auth_user, $caraTerima, $pinjamanAmt);
+ if ($error !== null) {
+ return $this->gadaiStoreResult($request, false, $error);
+ }
+
+ return $this->gadaiStoreResult($request, true);
}
public function completeShorakaFinancing(Request $request)
@@ -366,6 +405,20 @@ class GadaianController extends Controller
$auth_user = Auth::user();
$caraTerima = $request->cara_terima;
$pinjamanAmt = round((float) $request->pinjaman, 2);
+ $error = $this->disburseGadai($request, $auth_user, $caraTerima, $pinjamanAmt);
+ if ($error !== null) {
+ throw new \RuntimeException($error);
+ }
+
+ return redirect()->route('customer_info');
+ }
+
+ /**
+ * Cash or online disbursement after the commodity order and gadai save.
+ * Returns an error message, or null when disbursement succeeded.
+ */
+ protected function disburseGadai(Request $request, $auth_user, string $caraTerima, float $pinjamanAmt): ?string
+ {
if ($caraTerima === 'ONLINE') {
$online = Http::withOptions(['verify' => config('app.api_verify')])
->asForm()
@@ -385,10 +438,9 @@ class GadaianController extends Controller
if (!$online->successful()) {
$body = $online->json();
- $msg = is_array($body) && isset($body['message'])
+ return is_array($body) && isset($body['message'])
? $body['message']
: 'Gagal daftar pembiayaan online. Sila cuba lagi.';
- throw new \RuntimeException($msg);
}
$this->notifyPcPembiayaanOnlinePending($auth_user, $request, $pinjamanAmt);
@@ -406,6 +458,55 @@ class GadaianController extends Controller
? 'Gadai berjaya. Pembiayaan online menunggu semakan PC.'
: 'Pembiayaan berjaya dihantar.');
+ return null;
+ }
+
+ /**
+ * Provider chosen on the gadai form, limited to those enabled in config.
+ */
+ protected function selectedCommodityProvider(Request $request): ?string
+ {
+ $enabled = array_values(array_filter([
+ config('commodity.shoraka') ? 'shoraka' : null,
+ config('commodity.bsas') ? 'bsas' : null,
+ ]));
+
+ if ($enabled === []) {
+ return null;
+ }
+
+ $selected = strtolower(trim((string) $request->input('commodity_provider', '')));
+ if ($selected === '' && count($enabled) === 1) {
+ return $enabled[0];
+ }
+
+ return in_array($selected, $enabled, true) ? $selected : null;
+ }
+
+ /**
+ * JSON for AJAX Terima (BSAS progress UI); normal redirect otherwise.
+ */
+ protected function gadaiStoreResult(Request $request, bool $ok, ?string $message = null)
+ {
+ if ($request->ajax() || $request->wantsJson()) {
+ if (!$ok) {
+ return response()->json([
+ 'ok' => false,
+ 'message' => $message ?: 'Gadai tidak berjaya.',
+ ], 422);
+ }
+
+ return response()->json([
+ 'ok' => true,
+ 'message' => $message ?: 'Gadai berjaya.',
+ 'redirect' => route('customer_info'),
+ ]);
+ }
+
+ if (!$ok) {
+ return redirect()->back()->with('error', $message ?: 'Gadai tidak berjaya.');
+ }
+
return redirect()->route('customer_info');
}
@@ -711,20 +812,11 @@ 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();
- $product = Http::withOptions(['verify' => config('app.api_verify')])
- ->get($api_url . '/api/komuditi/description', ['kodproduk' => $komoditi['kodproduct']])
- ->json();
- $kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai'];
- }
+ $commodityPrint = app(\App\Services\CommodityCertificate::class)
+ ->forReference($gadai_detail['norujukan'], $gadai_detail['pinjaman'] ?? 0);
+ $komoditi = $commodityPrint['komoditi'];
+ $product = $commodityPrint['product'];
+ $kuantiti = $commodityPrint['kuantiti'];
if ($gadai_detail['tagbaru'] == 1) {
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
diff --git a/app/Http/Controllers/KomuditiController.php b/app/Http/Controllers/KomuditiController.php
index 72474dbf..7fa44aa0 100644
--- a/app/Http/Controllers/KomuditiController.php
+++ b/app/Http/Controllers/KomuditiController.php
@@ -55,4 +55,274 @@ class KomuditiController extends Controller
return redirect()->route('komuditi');
}
+
+ /**
+ * Khazanah branch list of BSAS commodity orders.
+ */
+ public function khazanahBsas()
+ {
+ $auth_user = Auth::user();
+ $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
+ ->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])
+ ->json();
+
+ return view('homepage.khazanah.bsas_komoditi', compact('cawangan_info'));
+ }
+
+ /**
+ * Dummy BSAS eCert layout preview (bid, OTC, STB, then one commodity schedule).
+ */
+ public function previewBsasEcert()
+ {
+ $line = [
+ ['SUPPLIER' => 'FGV TRADING SDN BHD', 'VOLUME' => '0.75276406'],
+ ];
+
+ return view('print.bsas_ecert', $this->presentBsasCertificates([
+ 'e_cert_no' => 'CPO21OCT26-0000031-000',
+ 'serial_number' => '1',
+ 'otc_counter_party' => 'ALI BIN ABU',
+ 'otc_murabaha_value' => '3,814.40',
+ 'product_code' => 'CPO-MSIA-09',
+ 'bid_ecert' => [
+ 'ECERTNO' => 'CPO21OCT26-0000031-000',
+ 'BUYER' => 'KOPERASI PERMODALAN KELANTAN BERHAD',
+ 'OWNER' => 'KOPERASI PERMODALAN KELANTAN BERHAD',
+ 'BIDNO' => '8',
+ 'TOTALVALUE' => '3,200.00',
+ 'CURRENCY' => 'MYR',
+ 'PRICE' => '4,251.00',
+ 'PRICE_MYR_EQUIVALENT' => '4,251.00',
+ 'PURCHASETIMEDATE' => '15:11:53.316 21 Oct 2026',
+ 'VALUEDATE' => '21 Oct 2026',
+ 'PNAME' => 'CPO-MSIA-09',
+ 'PVOLUME' => '0.75276406',
+ 'LINE' => $line,
+ ],
+ 'otc_ecert' => [
+ 'ECERTNO' => 'CPO21OCT26-0000031-000',
+ 'SELLER' => 'KOPERASI PERMODALAN KELANTAN BERHAD',
+ 'BUYER' => 'ALI BIN ABU',
+ 'REPORTINGTIMEDATE' => '15:11:53.320 21 Oct 2026',
+ 'VALUEDATE' => '21 Oct 2026',
+ 'PNAME' => 'CPO-MSIA-09',
+ 'PVOLUME' => '0.75276406',
+ 'CURRENCY' => 'MYR',
+ 'PRICE' => '4,251.00',
+ 'PRICE_MYR_EQUIVALENT' => '4,251.00',
+ 'TOTALVALUE' => '3,200.00',
+ 'MURABAHAVALUE' => '3,814.40',
+ 'LINE' => $line,
+ ],
+ 'stb_ecert' => [
+ 'ECERTNO' => 'CPO21OCT26-0000031-000',
+ 'SELLER' => 'ALI BIN ABU',
+ 'BUYER' => 'BURSA MALAYSIA ISLAMIC SERVICES',
+ 'TOTALVALUE' => '3,200.00',
+ 'CURRENCY' => 'MYR',
+ 'PRICE' => '4,251.00',
+ 'PRICE_MYR_EQUIVALENT' => '4,251.00',
+ 'SELLINGTIMEDATE' => '15:11:53.325 21 Oct 2026',
+ 'VALUEDATE' => '21 Oct 2026',
+ 'PNAME' => 'CPO-MSIA-09',
+ 'PVOLUME' => '0.75276406',
+ 'LINE' => $line,
+ ],
+ ], true));
+ }
+
+ /**
+ * Live BSAS eCerts from stored bid, OTC and STB payloads.
+ */
+ public function showBsasEcert($uuid)
+ {
+ $response = Http::withOptions(['verify' => config('app.api_verify')])
+ ->get(config('api.url') . '/api/komuditi/bsas/order/' . urlencode($uuid) . '/ecert');
+
+ if ($response->status() === 404) {
+ abort(404, $response->json('message') ?: 'eCert tidak dijumpai.');
+ }
+
+ if (!$response->successful()) {
+ abort(502, 'Gagal memuatkan eCert BSAS.');
+ }
+
+ $payload = $response->json();
+ if (!is_array($payload)) {
+ abort(502, 'Gagal memuatkan eCert BSAS.');
+ }
+
+ $presented = $this->presentBsasCertificates($payload, false);
+ if ($presented['certificates'] === []) {
+ abort(404, 'eCert belum tersedia untuk order ini.');
+ }
+
+ return view('print.bsas_ecert', $presented);
+ }
+
+ /**
+ * Bid, OTC and STB pages, plus one shared commodity schedule.
+ */
+ protected function presentBsasCertificates(array $payload, bool $isDummy): array
+ {
+ $certificates = [];
+
+ $bid = $payload['bid_ecert'] ?? null;
+ if (is_array($bid) && $bid !== []) {
+ $certificates[] = $this->mapBidCertificate($bid, $payload);
+ }
+
+ $otc = $payload['otc_ecert'] ?? null;
+ if (is_array($otc) && $otc !== []) {
+ $certificates[] = $this->mapOtcCertificate($otc, $payload);
+ }
+
+ $stb = $payload['stb_ecert'] ?? null;
+ if (is_array($stb) && $stb !== []) {
+ $certificates[] = $this->mapStbCertificate($stb, $payload);
+ }
+
+ $scheduleSource = [];
+ if (is_array($otc) && $otc !== []) {
+ $scheduleSource = $otc;
+ } elseif (is_array($bid) && $bid !== []) {
+ $scheduleSource = $bid;
+ } elseif (is_array($stb) && $stb !== []) {
+ $scheduleSource = $stb;
+ }
+
+ $ecertNo = $scheduleSource['ECERTNO'] ?? ($payload['e_cert_no'] ?? '-');
+
+ return [
+ 'certificates' => $certificates,
+ 'ecert_no' => $ecertNo,
+ 'lines' => $this->mapBsasLines($scheduleSource),
+ 'isDummy' => $isDummy,
+ ];
+ }
+
+ protected function mapBidCertificate(array $ecert, array $orderMeta): array
+ {
+ [$time, $date] = $this->splitBsasDateTime((string) ($ecert['PURCHASETIMEDATE'] ?? ''));
+
+ return $this->certificatePage(
+ 'Certificate of Purchase',
+ 'This is to certify that the following bid transaction has been executed {{ $company }}
through the Bursa Suq Al-Sila\' in accordance with the
Rules of Bursa Suq Al-Sila\'.',
+ $ecert,
+ $orderMeta,
+ array_merge([
+ ['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? '-'],
+ ['label' => 'Owner', 'value' => $ecert['OWNER'] ?? '-'],
+ ['label' => 'Bid No', 'value' => $ecert['BIDNO'] ?? ($orderMeta['serial_number'] ?? '-')],
+ ['label' => 'Purchase Time / Date', 'value' => $time . ' / ' . $date],
+ ], $this->commodityDetailRows($ecert, $orderMeta))
+ );
+ }
+
+ protected function mapOtcCertificate(array $ecert, array $orderMeta): array
+ {
+ [$time, $date] = $this->splitBsasDateTime((string) ($ecert['REPORTINGTIMEDATE'] ?? ''));
+
+ $page = $this->certificatePage(
+ 'Certificate of Murabahah',
+ 'This is to certify that the following transaction has been executed
through the Bursa Suq Al-Sila\' in accordance with the
Rules of Bursa Suq Al-Sila\'.',
+ $ecert,
+ $orderMeta,
+ array_merge([
+ ['label' => 'Seller', 'value' => $ecert['SELLER'] ?? 'KOPERASI PERMODALAN KELANTAN BERHAD'],
+ ['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? ($orderMeta['otc_counter_party'] ?? '-')],
+ ['label' => 'Bid No', 'value' => $orderMeta['serial_number'] ?? '1'],
+ ['label' => 'Reporting Time / Date', 'value' => $time . ' / ' . $date],
+ ], $this->commodityDetailRows($ecert, $orderMeta), [
+ ['label' => 'Transaction Type', 'value' => '-'],
+ ])
+ );
+
+ $page['show_murabaha'] = true;
+ $page['murabaha'] = $ecert['MURABAHAVALUE'] ?? ($orderMeta['otc_murabaha_value'] ?? '-');
+
+ return $page;
+ }
+
+ protected function mapStbCertificate(array $ecert, array $orderMeta): array
+ {
+ [$time, $date] = $this->splitBsasDateTime((string) ($ecert['SELLINGTIMEDATE'] ?? ''));
+
+ return $this->certificatePage(
+ 'Certificate of Sale',
+ 'This is to certify that the following sale transaction has been executed
through the Bursa Suq Al-Sila\' in accordance with the
Rules of Bursa Suq Al-Sila\'.',
+ $ecert,
+ $orderMeta,
+ array_merge([
+ ['label' => 'Seller', 'value' => $ecert['SELLER'] ?? ($orderMeta['otc_counter_party'] ?? '-')],
+ ['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? '-'],
+ ['label' => 'Selling Time / Date', 'value' => $time . ' / ' . $date],
+ ], $this->commodityDetailRows($ecert, $orderMeta))
+ );
+ }
+
+ protected function certificatePage(string $title, string $statement, array $ecert, array $orderMeta, array $rows): array
+ {
+ return [
+ 'title' => $title,
+ 'statement' => $statement,
+ 'ecert_no' => $ecert['ECERTNO'] ?? ($orderMeta['e_cert_no'] ?? '-'),
+ 'currency' => $ecert['CURRENCY'] ?? 'MYR',
+ 'rows' => $rows,
+ 'show_murabaha' => false,
+ 'murabaha' => null,
+ ];
+ }
+
+ protected function commodityDetailRows(array $ecert, array $orderMeta): array
+ {
+ return [
+ ['label' => 'Value Date', 'value' => isset($ecert['VALUEDATE']) ? strtoupper((string) $ecert['VALUEDATE']) : '-'],
+ ['label' => 'Product Code / Description', 'value' => $ecert['PNAME'] ?? ($orderMeta['product_code'] ?? '-')],
+ ['label' => 'Unit', 'value' => 'Tonnage'],
+ ['label' => 'Volume', 'value' => $ecert['PVOLUME'] ?? '-'],
+ ['label' => 'Currency', 'value' => $ecert['CURRENCY'] ?? 'MYR'],
+ ['label' => 'Price (MYR)', 'value' => $ecert['PRICE'] ?? '-'],
+ ['label' => 'Price (MYR Equivalent)', 'value' => $ecert['PRICE_MYR_EQUIVALENT'] ?? ($ecert['PRICE'] ?? '-')],
+ ['label' => 'Value', 'value' => $ecert['TOTALVALUE'] ?? '-'],
+ ];
+ }
+
+ protected function mapBsasLines(array $ecert): array
+ {
+ $lines = [];
+ foreach (($ecert['LINE'] ?? []) as $line) {
+ if (!is_array($line)) {
+ continue;
+ }
+ $lines[] = [
+ 'supplier' => $line['SUPPLIER'] ?? '-',
+ 'volume' => $line['VOLUME'] ?? ($ecert['PVOLUME'] ?? '-'),
+ ];
+ }
+
+ if ($lines === []) {
+ $lines[] = [
+ 'supplier' => '-',
+ 'volume' => $ecert['PVOLUME'] ?? '-',
+ ];
+ }
+
+ return $lines;
+ }
+
+ protected function splitBsasDateTime(string $value): array
+ {
+ $value = trim($value);
+ if ($value === '') {
+ return ['-', '-'];
+ }
+
+ $parts = explode(' ', $value, 2);
+
+ return [
+ $parts[0] ?? '-',
+ isset($parts[1]) ? strtoupper($parts[1]) : '-',
+ ];
+ }
}
diff --git a/app/Http/Controllers/PembiayaanOnlineController.php b/app/Http/Controllers/PembiayaanOnlineController.php
index 2644dae9..e3dbee6c 100644
--- a/app/Http/Controllers/PembiayaanOnlineController.php
+++ b/app/Http/Controllers/PembiayaanOnlineController.php
@@ -585,22 +585,11 @@ 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', [
- 'kodproduk' => is_array($komoditi) ? ($komoditi['kodproduct'] ?? '') : '',
- ])
- ->json();
-
- $nilai = is_array($komoditi) ? (float) ($komoditi['nilai'] ?? 0) : 0;
- $kuantiti = $nilai > 0 ? ((float) ($gadai_detail['pinjaman'] ?? 0) / $nilai) : 0;
- }
+ $commodityPrint = app(\App\Services\CommodityCertificate::class)
+ ->forReference($gadai_detail['norujukan'], $gadai_detail['pinjaman'] ?? 0);
+ $komoditi = $commodityPrint['komoditi'];
+ $product = $commodityPrint['product'];
+ $kuantiti = $commodityPrint['kuantiti'];
if (($gadai_detail['tagbaru'] ?? 0) == 1) {
$cawangan_info = $this->api()
diff --git a/app/Services/CommodityCertificate.php b/app/Services/CommodityCertificate.php
new file mode 100644
index 00000000..2753185c
--- /dev/null
+++ b/app/Services/CommodityCertificate.php
@@ -0,0 +1,94 @@
+shorakaPrint($reference);
+ if ($shoraka) {
+ return $this->normalize($shoraka['komoditi'], $shoraka['product'], $shoraka['kuantiti']);
+ }
+
+ $bsas = Http::withOptions(['verify' => config('app.api_verify')])
+ ->get(rtrim((string) config('api.url'), '/') . '/api/komuditi/bsas/order/norujukan/' . urlencode($reference));
+
+ if ($bsas->successful() && is_array($bsas->json())) {
+ $payload = $bsas->json();
+ return $this->normalize(
+ $payload['komoditi'] ?? [],
+ $payload['product'] ?? [],
+ (float) ($payload['kuantiti'] ?? 0)
+ );
+ }
+
+ return $this->legacy($pinjaman);
+ }
+
+ private function shorakaPrint(string $reference): ?array
+ {
+ try {
+ return app(ShorakaFinancingService::class)->printData($reference);
+ } catch (HttpException $exception) {
+ if ($exception->getStatusCode() === 409) {
+ throw $exception;
+ }
+ return null;
+ } catch (Throwable $exception) {
+ return null;
+ }
+ }
+
+ private function legacy($pinjaman): array
+ {
+ $api = rtrim((string) config('api.url'), '/');
+ $komoditi = Http::withOptions(['verify' => config('app.api_verify')])
+ ->get($api . '/api/komuditi/check')
+ ->json();
+ if (!is_array($komoditi)) {
+ $komoditi = [];
+ }
+
+ $product = Http::withOptions(['verify' => config('app.api_verify')])
+ ->get($api . '/api/komuditi/description', [
+ 'kodproduk' => $komoditi['kodproduct'] ?? '',
+ ])
+ ->json();
+
+ $nilai = (float) ($komoditi['nilai'] ?? 0);
+ $kuantiti = $nilai > 0 ? ((float) $pinjaman / $nilai) : 0;
+
+ return $this->normalize($komoditi, is_array($product) ? $product : [], $kuantiti);
+ }
+
+ private function normalize($komoditi, $product, $kuantiti): array
+ {
+ if (!is_array($komoditi)) {
+ $komoditi = [];
+ }
+ if (!is_array($product)) {
+ $product = [];
+ }
+
+ $code = $product['product_code'] ?? $product['kodproduk'] ?? ($komoditi['kodproduct'] ?? '');
+ $product['product_code'] = $product['product_code'] ?? $code;
+ $product['kodproduk'] = $product['kodproduk'] ?? $code;
+ $product['description'] = $product['description'] ?? '';
+ $product['unit'] = $product['unit'] ?? 'TAN';
+
+ return [
+ 'komoditi' => $komoditi,
+ 'product' => $product,
+ 'kuantiti' => $kuantiti,
+ ];
+ }
+}
diff --git a/config/commodity.php b/config/commodity.php
new file mode 100644
index 00000000..c9db472a
--- /dev/null
+++ b/config/commodity.php
@@ -0,0 +1,10 @@
+ filter_var(env('COMMODITY_SHORAKA_ENABLED', false), FILTER_VALIDATE_BOOLEAN),
+ 'bsas' => filter_var(env('COMMODITY_BSAS_ENABLED', false), FILTER_VALIDATE_BOOLEAN),
+];
diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile
index e5d5b986..107bfcad 100644
--- a/docker/php/Dockerfile
+++ b/docker/php/Dockerfile
@@ -2,14 +2,14 @@ FROM php:7.4-fpm AS base
WORKDIR /var/www/html
-# PHP 7.4 uses Debian Bullseye; use its archived package repositories.
-# Archived security metadata expires, but package signatures remain verified.
-RUN printf '%s\n' \
- 'deb https://archive.debian.org/debian bullseye main' \
- 'deb [check-valid-until=no] https://archive.debian.org/debian-security bullseye-security main' \
- > /etc/apt/sources.list \
- && rm -rf /var/lib/apt/lists/* \
- && apt-get -o Acquire::Retries=3 update \
+# php:7.4-fpm is Debian 11 (bullseye); after LTS end, packages live on archive.debian.org
+RUN sed -i \
+ -e 's|http://deb.debian.org/debian|http://archive.debian.org/debian|g' \
+ -e 's|http://security.debian.org/debian-security|http://archive.debian.org/debian-security|g' \
+ -e 's|http://deb.debian.org/debian-security|http://archive.debian.org/debian-security|g' \
+ /etc/apt/sources.list \
+ && printf 'Acquire::Check-Valid-Until "false";\n' > /etc/apt/apt.conf.d/99no-check-valid-until \
+ && apt-get update \
&& apt-get install -y --no-install-recommends \
git \
pkg-config \
diff --git a/public/img/svg/logo-arrahn-new.svg b/public/img/svg/logo-arrahn-new.svg
new file mode 100644
index 00000000..149be9fa
--- /dev/null
+++ b/public/img/svg/logo-arrahn-new.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/img/logo-arrahn.svg b/public/img/svg/logo-arrahn-old.svg
similarity index 100%
rename from public/img/logo-arrahn.svg
rename to public/img/svg/logo-arrahn-old.svg
diff --git a/public/img/svg/logo-kopkb.svg b/public/img/svg/logo-kopkb.svg
new file mode 100644
index 00000000..5c3ea85b
--- /dev/null
+++ b/public/img/svg/logo-kopkb.svg
@@ -0,0 +1,161 @@
+
+
+
+
diff --git a/resources/views/components/print/bsas/footer.blade.php b/resources/views/components/print/bsas/footer.blade.php
new file mode 100644
index 00000000..a914fef2
--- /dev/null
+++ b/resources/views/components/print/bsas/footer.blade.php
@@ -0,0 +1,50 @@
+@props([
+ 'note' => '',
+ 'page' => null,
+ 'total' => null,
+])
+
+
+
+
diff --git a/resources/views/components/print/bsas/header.blade.php b/resources/views/components/print/bsas/header.blade.php
new file mode 100644
index 00000000..724754a1
--- /dev/null
+++ b/resources/views/components/print/bsas/header.blade.php
@@ -0,0 +1,63 @@
+@props([
+ 'company' => 'Koperasi Permodalan Kelantan Berhad',
+ 'leftLogo' => null,
+ 'rightLogo' => null,
+])
+
+@php
+ $leftLogo = $leftLogo ?? asset('img/svg/logo-kopkb.svg');
+ $rightLogo = $rightLogo ?? asset('img/svg/logo-arrahn-new.svg');
+@endphp
+
+
+
+
+
+
Sedang memproses pembelian komoditi...
' + + 'Sila tunggu, proses ini boleh mengambil masa 10–30 saat.
' + + '