diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..fedbeed --- /dev/null +++ b/.env.docker @@ -0,0 +1,66 @@ +APP_NAME=Lumen +APP_ENV=local +APP_KEY=WbPGkYNmXZ6ZQplGlvl0iZ7zyW1b09xa +APP_DEBUG=true +APP_URL=http://localhost +APP_TIMEZONE=Asia/Kuala_Lumpur + +LOG_CHANNEL=stack +LOG_LEVEL=debug +LOG_SLACK_WEBHOOK_URL= + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=erahn_xtra +DB_USERNAME=root +DB_PASSWORD=root + +DB_CONNECTION_7=mysql +DB_HOST_7=mysql +DB_PORT_7=3306 +DB_DATABASE_7=master_erahn +DB_USERNAME_7=root +DB_PASSWORD_7=root + +# DB_CONNECTION_3=mysql +# DB_HOST_3=127.0.0.1 +# DB_PORT_3=3306 +# DB_DATABASE_3=erahn_baling +# DB_USERNAME_3=root +# DB_PASSWORD_3=root + +# DB_CONNECTION_4=mysql +# DB_HOST_4=127.0.0.1 +# DB_PORT_4=3306 +# DB_DATABASE_4=arrahn_ld +# DB_USERNAME_4=root +# DB_PASSWORD_4=root + +# DB_CONNECTION_5=mysql +# DB_HOST_5=127.0.0.1 +# DB_PORT_5=3306 +# DB_DATABASE_5=erahn_segamat +# DB_USERNAME_5=root +# DB_PASSWORD_5=root + +# DB_CONNECTION_6=mysql +# DB_HOST_6=127.0.0.1 +# DB_PORT_6=3306 +# DB_DATABASE_6=arrahn_kb2 +# DB_USERNAME_6=spider +# DB_PASSWORD_6=G0dz!ll42020 + + +# DB_CONNECTION_8=mysql +# DB_HOST_8=127.0.0.1 +# DB_PORT_8=3306 +# DB_DATABASE_8=arrahn_klanas +# DB_USERNAME_8=spider +# DB_PASSWORD_8=G0dz!ll42020 + +TENNANT_MASTER_DB=master_erahn +TENNANT_KAUNTER_DB=kaunter_erahn + +CACHE_DRIVER=file +QUEUE_CONNECTION=sync \ No newline at end of file diff --git a/.gitignore b/.gitignore index 39329cd..7d5a7d3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Homestead.yaml .env .phpunit.result.cache ray.php -docker-compose.yml /docker_src /.vscode +**/.DS_Store +db-dump \ No newline at end of file diff --git a/README.md b/README.md index ac83c8d..3b1ebc0 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ You know what is this when you in this project + +If you does't know what is this you probably in wrong project + +```sql +--- for all ip +CREATE USER 'master_erahn'@'%' IDENTIFIED BY 'password'; + +--- for specific ip +CREATE USER 'kaunter_erahn'@'localhost' IDENTIFIED BY 'password'; + +GRANT ALL PRIVILEGES ON kaunter_erahn.* TO 'kaunter_erahn'@'%'; +FLUSH PRIVILEGES; +``` + +Gadai=Pembiayaan +Tebus=penyelesaian penuh/ansuran \ No newline at end of file diff --git a/app/BatchPembiayaanOnline.php b/app/BatchPembiayaanOnline.php new file mode 100644 index 0000000..7adeef1 --- /dev/null +++ b/app/BatchPembiayaanOnline.php @@ -0,0 +1,22 @@ +attributes['details_caw'] ?? '')); + + return $nama !== '' ? $nama : (string) ($this->attributes['kodcaw'] ?? ''); + } + /** * The attributes that are mass assignable. * diff --git a/app/Gadai.php b/app/Gadai.php index 7f3f595..c8037c3 100644 --- a/app/Gadai.php +++ b/app/Gadai.php @@ -25,6 +25,7 @@ class Gadai extends Model 'nilaimarhun', 'berat', 'pinjaman', + 'cara_terima', 'telahbayar', 'upahtelahbyr', 'cajtelahbyr', diff --git a/app/Http/Controllers/CustomerController.php b/app/Http/Controllers/CustomerController.php index d672bb5..e7c3f16 100644 --- a/app/Http/Controllers/CustomerController.php +++ b/app/Http/Controllers/CustomerController.php @@ -403,58 +403,31 @@ class CustomerController extends Controller } public function assignAnggota($noic, Request $request) -{ - // Semak sama ada pelanggan wujud - $customer = Customer::on('mysql7') - ->where('kpbaru', $noic) - ->orWhere('kplama', $noic) - ->first(); + { + // Semak sama ada pelanggan wujud + $customer = Customer::on('mysql7') + ->where('kpbaru', $noic) + ->orWhere('kplama', $noic) + ->first(); + + if (!$customer) { + return response()->json([ + 'success' => false, + 'message' => 'Pelanggan tidak dijumpai.' + ], 404); + } + + // Kemaskini tag_anggota + $customer->tag_anggota = 1; + $customer->save(); - if (!$customer) { return response()->json([ - 'success' => false, - 'message' => 'Pelanggan tidak dijumpai.' - ], 404); + 'success' => true, + 'message' => 'Tag anggota berjaya dikemaskini.', + 'data' => $customer + ], 200); } - // Kemaskini tag_anggota - $customer->tag_anggota = 1; - $customer->save(); - - return response()->json([ - 'success' => true, - 'message' => 'Tag anggota berjaya dikemaskini.', - 'data' => $customer - ], 200); -} - } - - public function assignAnggota($noic, Request $request) -{ - // Semak sama ada pelanggan wujud - $customer = Customer::on('mysql7') - ->where('kpbaru', $noic) - ->orWhere('kplama', $noic) - ->first(); - - if (!$customer) { - return response()->json([ - 'success' => false, - 'message' => 'Pelanggan tidak dijumpai.' - ], 404); - } - - // Kemaskini tag_anggota - $customer->tag_anggota = 1; - $customer->save(); - - return response()->json([ - 'success' => true, - 'message' => 'Tag anggota berjaya dikemaskini.', - 'data' => $customer - ], 200); -} - public function customerUpdateKaunter($nokp,$kodcaw, Request $request){ $current = Carbon::now(); @@ -889,14 +862,6 @@ class CustomerController extends Controller } - public function anggotaKoperasi(Request $request){ - - $anggota = Anggota::where('noic',$request->noic)->first(); - - return response()->json($anggota); - } - - public function showCustomerByEachCawangan($ic) { $cawangan_all = Cawangan::on('mysql7')->get(); diff --git a/app/Http/Controllers/KomuditiController.php b/app/Http/Controllers/KomuditiController.php index e20e1fe..c9c070b 100644 --- a/app/Http/Controllers/KomuditiController.php +++ b/app/Http/Controllers/KomuditiController.php @@ -145,15 +145,19 @@ class KomuditiController extends Controller $komuditi_current = KomuditiPurchase::on('mysql7')->where('date_purchase','=',$current->toDateString())->orderBy('created_at','desc')->first(); $unit_current = $komuditi_current['unit_komuditi']; - $update_unit = $unit_current - $request->unit_komuditi; + // $unit_current = round((float) $komuditi_current['unit_komuditi'], 2); + // $unit_komuditi = round((float) $request->unit_komuditi, 2); + // $update_unit = round($unit_current - $unit_komuditi, 2); + $update_komuditi = KomuditiPurchase::on('mysql7')->where('id','=',$komuditi_current['id'])->update([ 'unit_komuditi' => $update_unit ]); $komuditi_transaksi = KomuditiTransaksi::on('mysql7')->create([ 'unit_komuditi' => $request->unit_komuditi, + // 'unit_komuditi' => $unit_komuditi, 'nosijil'=>$komuditi_current['no_sijil'], 'norujukan' => $request->norujukan, 'kodcaw' => $request->kodcaw , diff --git a/app/Http/Controllers/ModalOnlineController.php b/app/Http/Controllers/ModalOnlineController.php new file mode 100644 index 0000000..eaec1d7 --- /dev/null +++ b/app/Http/Controllers/ModalOnlineController.php @@ -0,0 +1,160 @@ +service = $service ?: new ModalOnlineService(); + } + + protected function respond(array $result) + { + return response()->json($result['payload'], $result['status']); + } + + /** + * GET /api/modal-online/{kodcaw} + * Read-only: does not create today's row. + */ + public function get($kodcaw) + { + return $this->respond($this->service->get($kodcaw)); + } + + /** + * @deprecated Alias of get() — no longer auto-creates RM100k. + */ + public function getOrCreate($kodcaw) + { + return $this->get($kodcaw); + } + + /** + * POST /api/modal-online/{kodcaw}/reserve + */ + public function reserve($kodcaw, Request $request) + { + return $this->respond($this->service->reserve($kodcaw, $request->input('amaun', 0))); + } + + /** + * GET /api/initiate/modal-online?zone=all|{zone} + */ + public function initiate(Request $request) + { + return $this->respond($this->service->initiate($request->input('zone', 'all'))); + } + + /** + * POST /api/modal-online/{kodcaw}/topup/request + * PC mohon had (hari pertama) atau tambah modal. Body: amaun, dimohon_oleh, nota? + */ + public function requestTopUp($kodcaw, Request $request) + { + return $this->respond($this->service->requestTopUp( + $kodcaw, + $request->input('amaun', 0), + $request->input('dimohon_oleh', ''), + $request->input('nota') + )); + } + + /** + * GET /api/modal-online/{kodcaw}/topup/requests?status=&tarikh= + */ + public function listTopUpByCawangan($kodcaw, Request $request) + { + return $this->respond($this->service->listTopUpRequests( + $kodcaw, + $request->input('status'), + $request->input('tarikh') + )); + } + + /** + * GET /api/modal-online/operasi/topup/requests?status=PENDING&tarikh= + */ + public function listTopUpOperasi(Request $request) + { + $status = $request->input('status', 'PENDING'); + if ($status === 'all' || $status === '') { + $status = null; + } + + return $this->respond($this->service->listTopUpRequests( + null, + $status, + $request->input('tarikh') + )); + } + + /** + * POST /api/modal-online/operasi/topup/{id}/approve + * Body: diluluskan_oleh, nota?, amaun? (optional override) + */ + public function approveTopUp($id, Request $request) + { + return $this->respond($this->service->approveTopUp( + $id, + $request->input('diluluskan_oleh', ''), + $request->input('nota'), + null, + $request->input('amaun') + )); + } + + /** + * POST /api/modal-online/operasi/topup/{id}/reject + * Body: diluluskan_oleh, nota? + */ + public function rejectTopUp($id, Request $request) + { + return $this->respond($this->service->rejectTopUp( + $id, + $request->input('diluluskan_oleh', ''), + $request->input('nota') + )); + } + + /** + * POST /api/modal-online/{kodcaw}/close + * Body: ditutup_oleh + */ + public function close($kodcaw, Request $request) + { + return $this->respond($this->service->close( + $kodcaw, + $request->input('ditutup_oleh', '') + )); + } + + /** + * GET /api/modal-online/operasi/eod?zone=all&tarikh= + */ + public function listToday(Request $request) + { + return $this->respond($this->service->listToday( + $request->input('zone', 'all'), + $request->input('tarikh') + )); + } + + /** + * POST /api/modal-online/operasi/eod/close-all + * Body: ditutup_oleh, zone? + */ + public function closeAll(Request $request) + { + return $this->respond($this->service->closeAll( + $request->input('ditutup_oleh', ''), + $request->input('zone', 'all') + )); + } +} diff --git a/app/Http/Controllers/PembiayaanOnlineController.php b/app/Http/Controllers/PembiayaanOnlineController.php new file mode 100644 index 0000000..700fdd0 --- /dev/null +++ b/app/Http/Controllers/PembiayaanOnlineController.php @@ -0,0 +1,204 @@ +service = $service ?: new PembiayaanOnlineService(); + } + + protected function respond(array $result) + { + return response()->json($result['payload'], $result['status']); + } + + /** + * POST /api/pembiayaan-online/{kodcaw} + */ + public function store($kodcaw, Request $request) + { + return $this->respond($this->service->store($kodcaw, $request->all())); + } + + /** + * GET /api/pembiayaan-online/{kodcaw}/by-norujukan/{norujukan} + */ + public function show($kodcaw, $norujukan) + { + return $this->respond($this->service->show($kodcaw, $norujukan)); + } + + /** + * GET /api/pembiayaan-online/{kodcaw}/queue + */ + public function queue($kodcaw, Request $request) + { + return $this->respond($this->service->queue($kodcaw, $request->input('status'))); + } + + /** + * PUT /api/pembiayaan-online/{kodcaw}/lines/{id} + */ + public function updateLine($kodcaw, $id, Request $request) + { + // Prefer only present keys so "has" semantics match previous controller. + $input = []; + foreach ([ + 'kodbank', 'noakaun', 'nama_bank', 'nama_akaun', 'nota', + 'bukti_akaun_file_id', 'borang_file_id', 'update_customer_bank', + ] as $key) { + if ($request->has($key)) { + $input[$key] = $request->input($key); + } + } + + return $this->respond($this->service->updateLine($kodcaw, $id, $input)); + } + + /** + * GET /api/pembiayaan-online/{kodcaw}/batches + */ + public function listBatches($kodcaw, Request $request) + { + return $this->respond($this->service->listBatches( + $kodcaw, + $request->input('tarikh'), + $request->input('status') + )); + } + + /** + * GET /api/pembiayaan-online/{kodcaw}/batches/{batchId} + */ + public function showBatch($kodcaw, $batchId) + { + return $this->respond($this->service->showBatch($kodcaw, $batchId)); + } + + /** + * POST /api/pembiayaan-online/{kodcaw}/batches/submit + */ + public function submitBatch($kodcaw, Request $request) + { + return $this->respond($this->service->submitBatch( + $kodcaw, + $request->input('ids', []), + $request->input('pc_submitted_by', ''), + $request->input('slot') + )); + } + + /** + * GET /api/pembiayaan-online/operasi/batches + */ + public function operasiListBatches(Request $request) + { + return $this->respond($this->service->operasiListBatches( + $request->input('status', 'SUBMITTED'), + $request->input('tarikh') + )); + } + + /** + * GET /api/pembiayaan-online/operasi/{kodcaw}/batches/{batchId} + */ + public function operasiShowBatch($kodcaw, $batchId) + { + return $this->showBatch($kodcaw, $batchId); + } + + /** + * POST /api/pembiayaan-online/operasi/{kodcaw}/lines/{id}/approve + */ + public function operasiApproveLine($kodcaw, $id, Request $request) + { + return $this->respond($this->service->operasiApproveLine( + $kodcaw, + $id, + $request->input('ops_by', '') + )); + } + + /** + * POST /api/pembiayaan-online/operasi/{kodcaw}/lines/{id}/reject + */ + public function operasiRejectLine($kodcaw, $id, Request $request) + { + return $this->respond($this->service->operasiRejectLine( + $kodcaw, + $id, + $request->input('ops_by', ''), + $request->input('reject_reason', '') + )); + } + + /** + * POST /api/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/approve + */ + public function operasiApproveBatch($kodcaw, $batchId, Request $request) + { + return $this->respond($this->service->operasiApproveBatch( + $kodcaw, + $batchId, + $request->input('ops_by', '') + )); + } + + /** + * POST /api/pembiayaan-online/operasi/{kodcaw}/lines/{id}/paid + * Body: ops_by, no_rujukan_bayaran, tarikh_bayaran?, + * bukti_data + bukti_filename + bukti_type OR bukti_file_id + */ + public function operasiMarkPaid($kodcaw, $id, Request $request) + { + return $this->respond($this->service->operasiMarkPaid($kodcaw, $id, $request->all())); + } + + /** + * POST /api/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/paid + */ + public function operasiMarkPaidBatch($kodcaw, $batchId, Request $request) + { + return $this->respond($this->service->operasiMarkPaidBatch($kodcaw, $batchId, $request->all())); + } + + /** + * GET /api/pembiayaan-online/{kodcaw}/lines/{id}/bukti + */ + public function getBukti($kodcaw, $id) + { + return $this->respond($this->service->getBukti($kodcaw, $id)); + } + + /** + * GET /api/pembiayaan-online/{kodcaw}/paid?blast_sent=0 + */ + public function paidQueue($kodcaw, Request $request) + { + return $this->respond($this->service->paidQueue( + $kodcaw, + $request->input('blast_sent') + )); + } + + /** + * POST /api/pembiayaan-online/{kodcaw}/paid/blast + * Body: ids[] (or ids), blast_by? + */ + public function markBlastSent($kodcaw, Request $request) + { + $ids = $request->input('ids', $request->input('id')); + return $this->respond($this->service->markBlastSent( + $kodcaw, + $ids, + $request->input('blast_by') + )); + } +} diff --git a/app/Http/Controllers/VaultController.php b/app/Http/Controllers/VaultController.php index 04f3290..03490f9 100644 --- a/app/Http/Controllers/VaultController.php +++ b/app/Http/Controllers/VaultController.php @@ -400,12 +400,14 @@ class VaultController extends Controller } + $bakiakhirVault = $vault ? $vault['bakiakhir'] : 0; + if($total != 0) { - $totalbakiakhirtunai = $vault['bakiakhir'] + $total; + $totalbakiakhirtunai = $bakiakhirVault + $total; }else { - $totalbakiakhirtunai = $vault['bakiakhir']; + $totalbakiakhirtunai = $bakiakhirVault; } return response()->json($totalbakiakhirtunai); diff --git a/app/ModalOnline.php b/app/ModalOnline.php new file mode 100644 index 0000000..a640eff --- /dev/null +++ b/app/ModalOnline.php @@ -0,0 +1,23 @@ +where('kodcaw', '=', $kodcaw)->first(); + + if (!$cawangan) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Cawangan tidak dijumpai'], + ]; + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => null, + 'mysql' => $cawangan['mysql'], + ]; + } + + protected function todayDate() + { + return Carbon::now()->toDateString(); + } + + protected function findPending($kodcaw) + { + return RequestModalOnline::on('mysql7') + ->where('kodcaw', $kodcaw) + ->where('status', 'PENDING') + ->first(); + } + + protected function pendingPayload($pending) + { + return $pending ? $pending->toArray() : null; + } + + protected function missingSnapshot($kodcaw, $tarikh, $pending = null) + { + return [ + 'tarikh' => $tarikh, + 'kodcaw' => $kodcaw, + 'had' => 0.00, + 'digunakan' => 0.00, + 'baki' => 0.00, + 'tambah' => 0.00, + 'kurang' => 0.00, + 'ditutup' => 0, + 'missing' => true, + 'pending_request' => $this->pendingPayload($pending), + ]; + } + + protected function recalcBaki($modal) + { + $modal->baki = round( + (float) $modal->had + (float) $modal->tambah - (float) $modal->kurang - (float) $modal->digunakan, + 2 + ); + } + + /** + * Get today's modal_online row. Does not create — PC must request had from Operasi. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function get($kodcaw) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + + $mysql = $resolved['mysql']; + $tarikh = $this->todayDate(); + $pending = $this->findPending($kodcaw); + + $modal = ModalOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('tarikh', $tarikh) + ->first(); + + if (!$modal) { + return [ + 'ok' => true, + 'status' => 200, + 'payload' => $this->missingSnapshot($kodcaw, $tarikh, $pending), + ]; + } + + $payload = $modal->toArray(); + $payload['missing'] = false; + $payload['pending_request'] = $this->pendingPayload($pending); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => $payload, + ]; + } + + /** + * @deprecated Use get(). Kept so existing callers do not auto-seed RM100k. + */ + public function getOrCreate($kodcaw) + { + return $this->get($kodcaw); + } + + /** + * Reserve amount inside an existing branch DB connection (caller may wrap in transaction). + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function reserveAmountOnConnection($mysql, $kodcaw, $amaun, $tarikh = null) + { + $amaun = round((float) $amaun, 2); + if ($amaun <= 0) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'Amaun tidak sah'], + ]; + } + + $tarikh = $tarikh ?: Carbon::now()->toDateString(); + + $modal = ModalOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('tarikh', $tarikh) + ->lockForUpdate() + ->first(); + + if (!$modal) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Modal online belum diwujudkan. Sila mohon had daripada Operasi terlebih dahulu.', + 'missing' => true, + ], + ]; + } + + if ((int) ($modal->ditutup ?? 0) === 1) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Modal online hari ini sudah ditutup (EOD)', + 'modal_online' => $modal, + ], + ]; + } + + if ((float) $modal->baki < $amaun) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Baki modal online tidak mencukupi', + 'baki' => (float) $modal->baki, + 'amaun' => $amaun, + ], + ]; + } + + $modal->digunakan = round((float) $modal->digunakan + $amaun, 2); + $modal->baki = round( + (float) $modal->had + (float) $modal->tambah - (float) $modal->kurang - (float) $modal->digunakan, + 2 + ); + $modal->save(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => $modal, + ]; + } + + /** + * Reserve amount from today's online float (transactional). + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function reserve($kodcaw, $amaun) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + + $mysql = $resolved['mysql']; + $amaun = round((float) $amaun, 2); + + if ($amaun <= 0) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'Amaun tidak sah'], + ]; + } + + try { + return DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $amaun) { + return $this->reserveAmountOnConnection($mysql, $kodcaw, $amaun); + }); + } catch (\Throwable $e) { + Log::error('ModalOnline reserve failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal reserve modal online'], + ]; + } + } + + /** + * Had is no longer auto-seeded. Returns how many branches already have a row today. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function initiate($zone = 'all') + { + $current = $this->todayDate(); + + if ($zone == 'all') { + $cawanganAll = Cawangan::on('mysql7')->get(); + } else { + $cawanganAll = Cawangan::on('mysql7')->where('zone', $zone)->get(); + } + + $existing = 0; + $missing = 0; + + foreach ($cawanganAll as $cawangan) { + try { + $exists = ModalOnline::on($cawangan['mysql']) + ->where('kodcaw', $cawangan['kodcaw']) + ->where('tarikh', $current) + ->exists(); + + if ($exists) { + $existing++; + } else { + $missing++; + } + } catch (\Throwable $e) { + Log::warning('initiate modal_online skip ' . $cawangan['kodcaw'] . ': ' . $e->getMessage()); + } + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'tarikh' => $current, + 'created' => 0, + 'existing' => $existing, + 'missing' => $missing, + 'message' => 'Modal online tidak di-initiate automatik. PC perlu mohon had daripada Operasi.', + ], + ]; + } + + /** + * PC request opening had (or extra top-up) from Operasi. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function requestTopUp($kodcaw, $amaun, $dimohonOleh, $nota = null) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $amaun = round((float) $amaun, 2); + $dimohonOleh = trim((string) $dimohonOleh); + + if ($amaun <= 0) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'Amaun tidak sah'], + ]; + } + if ($dimohonOleh === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'dimohon_oleh diperlukan'], + ]; + } + + $tarikh = $this->todayDate(); + $pending = $this->findPending($kodcaw); + + if ($pending) { + return [ + 'ok' => false, + 'status' => 409, + 'payload' => [ + 'message' => 'Masih ada permohonan modal online PENDING untuk cawangan ini', + 'request' => $pending, + ], + ]; + } + + $modal = ModalOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('tarikh', $tarikh) + ->first(); + + if ($modal && (int) ($modal->ditutup ?? 0) === 1) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Modal online hari ini sudah ditutup (EOD). Tidak boleh mohon.', + 'modal_online' => $modal, + ], + ]; + } + + $jenis = $modal ? 'TOPUP' : 'HAD'; + + $request = RequestModalOnline::on('mysql7')->create([ + 'kodcaw' => $kodcaw, + 'tarikh' => $tarikh, + 'amaun' => $amaun, + 'dimohon_oleh' => $dimohonOleh, + 'status' => 'PENDING', + 'nota' => $nota, + ]); + + return [ + 'ok' => true, + 'status' => 201, + 'payload' => [ + 'request' => $request, + 'jenis' => $jenis, + 'modal_online' => $modal ?: $this->missingSnapshot($kodcaw, $tarikh, $request), + ], + ]; + } + + /** + * List top-up requests. + * For PC: pass kodcaw. For Operasi: omit kodcaw (all branches). + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function listTopUpRequests($kodcaw = null, $status = null, $tarikh = null) + { + $query = RequestModalOnline::on('mysql7')->newQuery(); + + if ($kodcaw) { + $query->where('kodcaw', $kodcaw); + } + if ($status) { + $query->where('status', $status); + } + if ($tarikh) { + $query->where('tarikh', $tarikh); + } + + $rows = $query->orderBy('id', 'desc')->get(); + + $cawMap = []; + if (!$kodcaw) { + foreach (Cawangan::on('mysql7')->get() as $caw) { + $cawMap[$caw->kodcaw] = $caw->nama_cawangan; + } + } + + $data = []; + foreach ($rows as $row) { + $item = $row->toArray(); + if (!$kodcaw) { + $item['cawangan_nama'] = $cawMap[$row->kodcaw] ?? null; + } + $data[] = $item; + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'bilangan' => count($data), + 'data' => $data, + ], + ]; + } + + /** + * Operasi approve: first request of the day sets had; later requests add tambah. + * Optional $amaunOverride lets operasi lulus a different amount than dimohon. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function approveTopUp($requestId, $diluluskanOleh, $nota = null, $expectedKodcaw = null, $amaunOverride = null) + { + $diluluskanOleh = trim((string) $diluluskanOleh); + if ($diluluskanOleh === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'diluluskan_oleh diperlukan'], + ]; + } + + $req = RequestModalOnline::on('mysql7')->where('id', $requestId)->first(); + if (!$req) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Permohonan tidak dijumpai'], + ]; + } + + if ($expectedKodcaw && $req->kodcaw !== $expectedKodcaw) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Permohonan tidak dijumpai'], + ]; + } + + if ($req->status !== 'PENDING') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Hanya permohonan PENDING boleh diluluskan', + 'status' => $req->status, + ], + ]; + } + + $kodcaw = $req->kodcaw; + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + $amaunDimohon = round((float) $req->amaun, 2); + $amaun = $amaunDimohon; + if ($amaunOverride !== null && $amaunOverride !== '') { + $amaun = round((float) $amaunOverride, 2); + if ($amaun <= 0) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'Amaun diluluskan tidak sah'], + ]; + } + } + $tarikh = $this->todayDate(); + + try { + $applied = DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $tarikh, $amaun) { + $modal = ModalOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('tarikh', $tarikh) + ->lockForUpdate() + ->first(); + + $jenis = 'TOPUP'; + + if (!$modal) { + $modal = ModalOnline::on($mysql)->create([ + 'tarikh' => $tarikh, + 'kodcaw' => $kodcaw, + 'had' => $amaun, + 'digunakan' => 0.00, + 'baki' => $amaun, + 'tambah' => 0.00, + 'kurang' => 0.00, + 'ditutup' => 0, + ]); + $modal = ModalOnline::on($mysql) + ->where('id', $modal->id) + ->lockForUpdate() + ->first(); + $jenis = 'HAD'; + } else { + if ((int) ($modal->ditutup ?? 0) === 1) { + throw new \RuntimeException('MODAL_ONLINE_CLOSED'); + } + + $modal->tambah = round((float) $modal->tambah + $amaun, 2); + $this->recalcBaki($modal); + $modal->save(); + } + + return ['modal' => $modal, 'jenis' => $jenis]; + }); + + $modal = $applied['modal']; + $jenis = $applied['jenis']; + + $req->status = 'APPROVED'; + $req->diluluskan_oleh = $diluluskanOleh; + $req->amaun = $amaun; + + $notes = []; + if (abs($amaun - $amaunDimohon) >= 0.01) { + $notes[] = 'Amaun dimohon RM ' . number_format($amaunDimohon, 2) + . ', diluluskan RM ' . number_format($amaun, 2); + } + if ($nota !== null && trim((string) $nota) !== '') { + $notes[] = trim((string) $nota); + } + if ($notes) { + $existing = trim((string) $req->nota); + $req->nota = $existing === '' + ? implode(' | ', $notes) + : $existing . ' | ' . implode(' | ', $notes); + } + $req->save(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'request' => $req->fresh(), + 'jenis' => $jenis, + 'modal_online' => $modal, + ], + ]; + } catch (\Throwable $e) { + if ($e->getMessage() === 'MODAL_ONLINE_CLOSED') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'Modal online hari ini sudah ditutup (EOD). Tidak boleh lulus permohonan.'], + ]; + } + Log::error('approveTopUp failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal lulus permohonan modal online'], + ]; + } + } + + /** + * Operasi reject top-up request. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function rejectTopUp($requestId, $diluluskanOleh, $nota = null, $expectedKodcaw = null) + { + $diluluskanOleh = trim((string) $diluluskanOleh); + if ($diluluskanOleh === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'diluluskan_oleh diperlukan'], + ]; + } + + $req = RequestModalOnline::on('mysql7')->where('id', $requestId)->first(); + if (!$req) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Permohonan tidak dijumpai'], + ]; + } + + if ($expectedKodcaw && $req->kodcaw !== $expectedKodcaw) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Permohonan tidak dijumpai'], + ]; + } + + if ($req->status !== 'PENDING') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Hanya permohonan PENDING boleh ditolak', + 'status' => $req->status, + ], + ]; + } + + $req->status = 'REJECTED'; + $req->diluluskan_oleh = $diluluskanOleh; + if ($nota !== null && trim((string) $nota) !== '') { + $req->nota = trim( + trim((string) $req->nota) === '' + ? (string) $nota + : $req->nota . ' | ' . $nota + ); + } + $req->save(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => ['request' => $req], + ]; + } + + /** + * EOD close for one branch: return unused baki via kurang, mark ditutup. + * Also auto-rejects PENDING top-up requests for the branch. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function close($kodcaw, $ditutupOleh) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $ditutupOleh = trim((string) $ditutupOleh); + if ($ditutupOleh === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ditutup_oleh diperlukan'], + ]; + } + + $tarikh = Carbon::now()->toDateString(); + $now = Carbon::now(); + + try { + $result = DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $tarikh, $ditutupOleh, $now) { + $modal = ModalOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('tarikh', $tarikh) + ->lockForUpdate() + ->first(); + + if (!$modal) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => [ + 'message' => 'Tiada modal online hari ini untuk ditutup. Teller belum mohon / PC belum lulus had.', + 'missing' => true, + ], + ]; + } + + if ((int) ($modal->ditutup ?? 0) === 1) { + return [ + 'ok' => false, + 'status' => 409, + 'payload' => [ + 'message' => 'Modal online sudah ditutup untuk hari ini', + 'modal_online' => $modal, + ], + ]; + } + + $unused = round((float) $modal->baki, 2); + $modal->kurang = round((float) $modal->kurang + $unused, 2); + $modal->baki = round( + (float) $modal->had + (float) $modal->tambah - (float) $modal->kurang - (float) $modal->digunakan, + 2 + ); + $modal->ditutup = 1; + $modal->ditutup_oleh = $ditutupOleh; + $modal->ditutup_at = $now; + $modal->save(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'modal_online' => $modal, + 'dikembalikan' => $unused, + ], + ]; + }); + + if (!$result['ok']) { + return $result; + } + + $pendingRejected = RequestModalOnline::on('mysql7') + ->where('kodcaw', $kodcaw) + ->where('status', 'PENDING') + ->get(); + + foreach ($pendingRejected as $req) { + $req->status = 'REJECTED'; + $req->diluluskan_oleh = $ditutupOleh; + $tag = 'Auto reject: EOD close'; + $req->nota = trim((string) $req->nota) === '' + ? $tag + : trim($req->nota) . ' | ' . $tag; + $req->save(); + } + + $result['payload']['pending_topup_ditolak'] = $pendingRejected->count(); + return $result; + } catch (\Throwable $e) { + Log::error('modal online close failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal tutup modal online'], + ]; + } + } + + /** + * Operasi: list today's modal_online across branches. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function listToday($zone = 'all', $tarikh = null) + { + $tarikh = $tarikh ?: Carbon::now()->toDateString(); + + if ($zone == 'all') { + $cawanganAll = Cawangan::on('mysql7')->get(); + } else { + $cawanganAll = Cawangan::on('mysql7')->where('zone', $zone)->get(); + } + + $rows = []; + foreach ($cawanganAll as $cawangan) { + try { + $modal = ModalOnline::on($cawangan['mysql']) + ->where('kodcaw', $cawangan['kodcaw']) + ->where('tarikh', $tarikh) + ->first(); + + $row = $modal ? $modal->toArray() : [ + 'tarikh' => $tarikh, + 'kodcaw' => $cawangan['kodcaw'], + 'had' => 0, + 'digunakan' => 0, + 'baki' => 0, + 'tambah' => 0, + 'kurang' => 0, + 'ditutup' => 0, + 'missing' => true, + ]; + $row['cawangan_nama'] = $cawangan->nama_cawangan; + $row['missing'] = empty($modal); + $rows[] = $row; + } catch (\Throwable $e) { + Log::warning('listToday modal_online skip ' . $cawangan['kodcaw'] . ': ' . $e->getMessage()); + } + } + + usort($rows, function ($a, $b) { + return strcmp((string) ($a['kodcaw'] ?? ''), (string) ($b['kodcaw'] ?? '')); + }); + + $open = 0; + $closed = 0; + $missing = 0; + foreach ($rows as $row) { + if (!empty($row['missing'])) { + $missing++; + } elseif ((int) ($row['ditutup'] ?? 0) === 1) { + $closed++; + } else { + $open++; + } + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'tarikh' => $tarikh, + 'bilangan' => count($rows), + 'open' => $open, + 'closed' => $closed, + 'missing' => $missing, + 'data' => $rows, + ], + ]; + } + + /** + * Operasi bulk EOD close for open branches today. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function closeAll($ditutupOleh, $zone = 'all') + { + $ditutupOleh = trim((string) $ditutupOleh); + if ($ditutupOleh === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ditutup_oleh diperlukan'], + ]; + } + + $list = $this->listToday($zone); + $closed = 0; + $skipped = 0; + $failed = []; + + foreach ($list['payload']['data'] as $row) { + if (!empty($row['missing']) || (int) ($row['ditutup'] ?? 0) === 1) { + $skipped++; + continue; + } + $res = $this->close($row['kodcaw'], $ditutupOleh); + if ($res['ok']) { + $closed++; + } else { + $failed[] = [ + 'kodcaw' => $row['kodcaw'], + 'message' => $res['payload']['message'] ?? 'gagal', + ]; + } + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'closed' => $closed, + 'skipped' => $skipped, + 'failed' => $failed, + ], + ]; + } +} diff --git a/app/Services/PembiayaanOnline/PembiayaanOnlineService.php b/app/Services/PembiayaanOnline/PembiayaanOnlineService.php new file mode 100644 index 0000000..31fde74 --- /dev/null +++ b/app/Services/PembiayaanOnline/PembiayaanOnlineService.php @@ -0,0 +1,1558 @@ +modalOnlineService = $modalOnlineService ?: new ModalOnlineService(); + } + + /** + * @return array{ok:bool,status:int,payload:mixed,mysql?:string} + */ + public function resolveMysql($kodcaw) + { + return $this->modalOnlineService->resolveMysql($kodcaw); + } + + protected function resolveNamaBank($kodbank) + { + if ($kodbank === null || $kodbank === '') { + return null; + } + + $bank = Bank::on('mysql7')->where('id', $kodbank)->first(); + if ($bank) { + return $bank->bankdesc ?? $bank->nama ?? $bank->name ?? (string) $kodbank; + } + + $bank = Bank::on('mysql7')->where('bankcode', $kodbank)->first(); + if ($bank) { + return $bank->bankdesc ?? $bank->nama ?? (string) $kodbank; + } + + return (string) $kodbank; + } + + /** + * Create online payout line: reserve float + PENDING_PC + gadai.cara_terima. + * + * @param array $data + * @return array{ok:bool,status:int,payload:mixed} + */ + public function store($kodcaw, array $data) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $norujukan = trim((string) ($data['norujukan'] ?? '')); + $nokp = trim((string) ($data['nokp'] ?? '')); + $pinjaman = round((float) ($data['pinjaman'] ?? 0), 2); + $kodbank = $data['kodbank'] ?? null; + $noakaun = trim((string) ($data['noakaun'] ?? '')); + $createdBy = $data['created_by'] ?? null; + $updateCustomerBank = array_key_exists('update_customer_bank', $data) + ? (bool) $data['update_customer_bank'] + : true; + + if ($norujukan === '' || $nokp === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'norujukan dan nokp diperlukan'], + ]; + } + + if ($pinjaman < self::MIN_PINJAMAN || $pinjaman > self::MAX_PINJAMAN) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Pembiayaan online hanya untuk RM5,000 hingga RM50,000', + 'pinjaman' => $pinjaman, + ], + ]; + } + + if ($kodbank === null || $kodbank === '' || $noakaun === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'kodbank dan noakaun diperlukan'], + ]; + } + + $customer = Customer::on($mysql) + ->where('kpbaru', $nokp) + ->orWhere('kplama', $nokp) + ->first(); + + $namaPelanggan = $data['nama_pelanggan'] ?? ($customer->nama ?? null); + $nopelanggan = $data['nopelanggan'] ?? ($customer->nopelanggan ?? null); + $namaAkaun = $data['nama_akaun'] ?? $namaPelanggan; + $namaBank = $data['nama_bank'] ?? $this->resolveNamaBank($kodbank); + + try { + return DB::connection($mysql)->transaction(function () use ( + $mysql, + $kodcaw, + $norujukan, + $nokp, + $pinjaman, + $kodbank, + $noakaun, + $namaBank, + $namaAkaun, + $namaPelanggan, + $nopelanggan, + $createdBy, + $customer, + $updateCustomerBank + ) { + $existing = PembiayaanOnline::on($mysql) + ->where('norujukan', $norujukan) + ->where('kodcaw', $kodcaw) + ->lockForUpdate() + ->first(); + + if ($existing) { + return [ + 'ok' => false, + 'status' => 409, + 'payload' => [ + 'message' => 'Pembiayaan online untuk SAG ini sudah wujud', + 'pembiayaan_online' => $existing, + ], + ]; + } + + $gadai = Gadai::on($mysql) + ->where('norujukan', '=', $norujukan) + ->lockForUpdate() + ->first(); + + if (!$gadai) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => [ + 'message' => 'Rekod gadai tidak dijumpai', + 'kodcaw' => $kodcaw, + 'norujukan' => $norujukan, + 'hint' => 'Pastikan SAG sudah wujud dalam DB cawangan ini (daftar gadai / Terima dulu).', + ], + ]; + } + + $reserve = $this->modalOnlineService->reserveAmountOnConnection($mysql, $kodcaw, $pinjaman); + if (!$reserve['ok']) { + return $reserve; + } + + $line = PembiayaanOnline::on($mysql)->create([ + 'norujukan' => $norujukan, + 'nokp' => $nokp, + 'nopelanggan' => $nopelanggan, + 'nama_pelanggan' => $namaPelanggan, + 'kodcaw' => $kodcaw, + 'pinjaman' => $pinjaman, + 'kodbank' => $kodbank, + 'nama_bank' => $namaBank, + 'noakaun' => $noakaun, + 'nama_akaun' => $namaAkaun, + 'status' => 'PENDING_PC', + 'created_by' => $createdBy, + ]); + + Gadai::on($mysql) + ->where('norujukan', '=', $norujukan) + ->update(['cara_terima' => 'ONLINE']); + + if ($updateCustomerBank && $customer) { + Customer::on($mysql) + ->where(function ($q) use ($nokp) { + $q->where('kpbaru', $nokp)->orWhere('kplama', $nokp); + }) + ->update([ + 'kodbank' => $kodbank, + 'noakaun' => $noakaun, + ]); + } + + return [ + 'ok' => true, + 'status' => 201, + 'payload' => [ + 'pembiayaan_online' => $line, + 'modal_online' => $reserve['payload'], + ], + ]; + }); + } catch (\Throwable $e) { + Log::error('PembiayaanOnline store failed: ' . $e->getMessage(), [ + 'kodcaw' => $kodcaw, + 'norujukan' => $norujukan, + ]); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal cipta pembiayaan online'], + ]; + } + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function show($kodcaw, $norujukan) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + + $line = PembiayaanOnline::on($resolved['mysql']) + ->where('kodcaw', $kodcaw) + ->where('norujukan', $norujukan) + ->first(); + + if (!$line) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Pembiayaan online tidak dijumpai'], + ]; + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => $line, + ]; + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function queue($kodcaw, $status = null) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $query = PembiayaanOnline::on($mysql)->where('kodcaw', $kodcaw); + + if ($status) { + if (!in_array($status, self::PC_QUEUE_STATUSES, true)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Status queue tidak sah', + 'allowed' => self::PC_QUEUE_STATUSES, + ], + ]; + } + $query->where('status', $status); + } else { + $query->whereIn('status', self::PC_QUEUE_STATUSES); + } + + $lines = $query->orderBy('id', 'asc')->get(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'kodcaw' => $kodcaw, + 'slot_cadangan' => $this->suggestSlot(), + 'bilangan' => $lines->count(), + 'jumlah' => round((float) $lines->sum('pinjaman'), 2), + 'data' => $lines, + ], + ]; + } + + /** + * @param array $input raw request fields + * @return array{ok:bool,status:int,payload:mixed} + */ + public function updateLine($kodcaw, $id, array $input) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $line = PembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('id', $id) + ->first(); + + if (!$line) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Pembiayaan online tidak dijumpai'], + ]; + } + + if (!in_array($line->status, self::PC_QUEUE_STATUSES, true)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Hanya rekod PENDING_PC / REJECTED_PC boleh dikemaskini', + 'status' => $line->status, + ], + ]; + } + + $fields = []; + $has = function ($key) use ($input) { + return array_key_exists($key, $input); + }; + + if ($has('kodbank')) { + $fields['kodbank'] = $input['kodbank']; + if (!$has('nama_bank')) { + $fields['nama_bank'] = $this->resolveNamaBank($fields['kodbank']); + } + } + if ($has('nama_bank')) { + $fields['nama_bank'] = $input['nama_bank']; + } + if ($has('noakaun')) { + $fields['noakaun'] = trim((string) $input['noakaun']); + } + if ($has('nama_akaun')) { + $fields['nama_akaun'] = $input['nama_akaun']; + } + if ($has('nota')) { + $fields['nota'] = $input['nota']; + } + if ($has('bukti_akaun_file_id')) { + $fields['bukti_akaun_file_id'] = $input['bukti_akaun_file_id']; + } + if ($has('borang_file_id')) { + $fields['borang_file_id'] = $input['borang_file_id']; + } + + if (empty($fields)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'Tiada medan untuk dikemaskini'], + ]; + } + + if (isset($fields['kodbank']) && ($fields['kodbank'] === null || $fields['kodbank'] === '')) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'kodbank tidak sah'], + ]; + } + if (array_key_exists('noakaun', $fields) && $fields['noakaun'] === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'noakaun tidak sah'], + ]; + } + + $updateCustomerBank = array_key_exists('update_customer_bank', $input) + ? (bool) $input['update_customer_bank'] + : true; + + try { + DB::connection($mysql)->transaction(function () use ($mysql, $line, $fields, $updateCustomerBank) { + PembiayaanOnline::on($mysql)->where('id', $line->id)->update($fields); + + if ( + $updateCustomerBank + && (isset($fields['kodbank']) || isset($fields['noakaun'])) + ) { + $kodbank = $fields['kodbank'] ?? $line->kodbank; + $noakaun = $fields['noakaun'] ?? $line->noakaun; + Customer::on($mysql) + ->where(function ($q) use ($line) { + $q->where('kpbaru', $line->nokp)->orWhere('kplama', $line->nokp); + }) + ->update([ + 'kodbank' => $kodbank, + 'noakaun' => $noakaun, + ]); + } + }); + + $fresh = PembiayaanOnline::on($mysql)->where('id', $line->id)->first(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => $fresh, + ]; + } catch (\Throwable $e) { + Log::error('PembiayaanOnline updateLine failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal kemaskini pembiayaan online'], + ]; + } + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function listBatches($kodcaw, $tarikh = null, $status = null) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + + $query = BatchPembiayaanOnline::on($resolved['mysql'])->where('kodcaw', $kodcaw); + + if ($tarikh) { + $query->where('tarikh', $tarikh); + } + if ($status) { + $query->where('status', $status); + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => $query->orderBy('id', 'desc')->get(), + ]; + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function showBatch($kodcaw, $batchId) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $batch = BatchPembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('id', $batchId) + ->first(); + + if (!$batch) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Batch tidak dijumpai'], + ]; + } + + $lines = PembiayaanOnline::on($mysql) + ->where('batch_id', $batch->id) + ->orderBy('id', 'asc') + ->get(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'batch' => $batch, + 'lines' => $lines, + ], + ]; + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function submitBatch($kodcaw, $ids, $pcBy, $slot = null) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + if (is_string($ids)) { + $ids = array_filter(array_map('trim', explode(',', $ids))); + } + if (!is_array($ids) || count($ids) === 0) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ids diperlukan (senarai pembiayaan_online.id)'], + ]; + } + + $ids = array_values(array_unique(array_map('intval', $ids))); + $pcBy = trim((string) $pcBy); + if ($pcBy === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'pc_submitted_by diperlukan'], + ]; + } + + $slotInfo = $this->suggestSlot(); + $slot = strtoupper((string) ($slot ?: $slotInfo['slot'])); + if (!in_array($slot, ['BATCH1', 'BATCH2'], true)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'slot mesti BATCH1 atau BATCH2'], + ]; + } + + $now = Carbon::now(); + $tarikh = $now->toDateString(); + + try { + return DB::connection($mysql)->transaction(function () use ( + $mysql, + $kodcaw, + $ids, + $pcBy, + $slot, + $tarikh, + $now, + $slotInfo + ) { + $lines = PembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->whereIn('id', $ids) + ->lockForUpdate() + ->get(); + + if ($lines->count() !== count($ids)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Sebahagian id tidak dijumpai untuk cawangan ini', + 'requested' => $ids, + 'found' => $lines->pluck('id')->all(), + ], + ]; + } + + foreach ($lines as $line) { + if (!in_array($line->status, self::PC_QUEUE_STATUSES, true)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Rekod bukan dalam queue PC', + 'id' => $line->id, + 'norujukan' => $line->norujukan, + 'status' => $line->status, + ], + ]; + } + if (empty($line->kodbank) || empty($line->noakaun)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Maklumat bank tidak lengkap', + 'id' => $line->id, + 'norujukan' => $line->norujukan, + ], + ]; + } + } + + $batchNo = $this->nextBatchNo($mysql, $kodcaw, $tarikh, $slot); + $jumlah = round((float) $lines->sum('pinjaman'), 2); + + $batch = BatchPembiayaanOnline::on($mysql)->create([ + 'batch_no' => $batchNo, + 'kodcaw' => $kodcaw, + 'tarikh' => $tarikh, + 'slot' => $slot, + 'bilangan' => $lines->count(), + 'jumlah_keseluruhan' => $jumlah, + 'status' => 'SUBMITTED', + 'pc_submitted_by' => $pcBy, + 'submitted_at' => $now, + ]); + + foreach ($lines as $line) { + PembiayaanOnline::on($mysql)->where('id', $line->id)->update([ + 'batch_id' => $batch->id, + 'status' => 'IN_BATCH', + 'pc_submitted_by' => $pcBy, + 'reject_reason' => null, + 'rejected_by' => null, + 'rejected_at' => null, + ]); + } + + $freshLines = PembiayaanOnline::on($mysql) + ->where('batch_id', $batch->id) + ->orderBy('id', 'asc') + ->get(); + + return [ + 'ok' => true, + 'status' => 201, + 'payload' => [ + 'batch' => $batch, + 'lines' => $freshLines, + 'slot_info' => $slotInfo, + ], + ]; + }); + } catch (\Throwable $e) { + Log::error('PembiayaanOnline submitBatch failed: ' . $e->getMessage(), [ + 'kodcaw' => $kodcaw, + ]); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal hantar batch pembiayaan online'], + ]; + } + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function operasiListBatches($statusFilter = 'SUBMITTED', $tarikh = null) + { + $allowed = ['SUBMITTED', 'PARTIAL', 'OPS_DONE', 'PAID', 'all']; + if (!in_array($statusFilter, $allowed, true)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'status tidak sah', + 'allowed' => $allowed, + ], + ]; + } + + $cawanganAll = Cawangan::on('mysql7')->get(); + $batches = []; + + foreach ($cawanganAll as $cawangan) { + try { + $query = BatchPembiayaanOnline::on($cawangan['mysql']) + ->where('kodcaw', $cawangan['kodcaw']); + + if ($statusFilter !== 'all') { + $query->where('status', $statusFilter); + } else { + $query->whereIn('status', ['SUBMITTED', 'PARTIAL', 'OPS_DONE', 'PAID']); + } + + if ($tarikh) { + $query->where('tarikh', $tarikh); + } + + foreach ($query->orderBy('id', 'desc')->get() as $batch) { + $row = $batch->toArray(); + $row['cawangan_nama'] = $cawangan->nama_cawangan; + $batches[] = $row; + } + } catch (\Throwable $e) { + Log::warning('operasiListBatches skip ' . $cawangan['kodcaw'] . ': ' . $e->getMessage()); + } + } + + usort($batches, function ($a, $b) { + return ($b['id'] ?? 0) <=> ($a['id'] ?? 0); + }); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'bilangan' => count($batches), + 'data' => $batches, + ], + ]; + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function operasiApproveLine($kodcaw, $id, $opsBy) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $opsBy = trim((string) $opsBy); + if ($opsBy === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ops_by diperlukan'], + ]; + } + + try { + return DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $id, $opsBy) { + $line = PembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('id', $id) + ->lockForUpdate() + ->first(); + + if (!$line) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Pembiayaan online tidak dijumpai'], + ]; + } + + if (!in_array($line->status, ['IN_BATCH', 'OPS_REVIEW'], true)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Hanya rekod IN_BATCH / OPS_REVIEW boleh diluluskan', + 'status' => $line->status, + ], + ]; + } + + PembiayaanOnline::on($mysql)->where('id', $line->id)->update([ + 'status' => 'APPROVED', + 'ops_by' => $opsBy, + 'reject_reason' => null, + 'rejected_by' => null, + 'rejected_at' => null, + ]); + + $batch = null; + if ($line->batch_id) { + $batch = $this->refreshBatchStatus($mysql, $line->batch_id); + } + + $fresh = PembiayaanOnline::on($mysql)->where('id', $line->id)->first(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'pembiayaan_online' => $fresh, + 'batch' => $batch, + ], + ]; + }); + } catch (\Throwable $e) { + Log::error('operasiApproveLine failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal lulus pembiayaan online'], + ]; + } + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function operasiRejectLine($kodcaw, $id, $opsBy, $reason) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $opsBy = trim((string) $opsBy); + $reason = trim((string) $reason); + + if ($opsBy === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ops_by diperlukan'], + ]; + } + if ($reason === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'reject_reason diperlukan'], + ]; + } + + $now = Carbon::now(); + + try { + return DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $id, $opsBy, $reason, $now) { + $line = PembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('id', $id) + ->lockForUpdate() + ->first(); + + if (!$line) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Pembiayaan online tidak dijumpai'], + ]; + } + + if (!in_array($line->status, ['IN_BATCH', 'OPS_REVIEW', 'APPROVED'], true)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Status semasa tidak boleh ditolak', + 'status' => $line->status, + ], + ]; + } + + if ($line->status === 'PAID') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'Rekod sudah PAID, tidak boleh ditolak'], + ]; + } + + $batchId = $line->batch_id; + + PembiayaanOnline::on($mysql)->where('id', $line->id)->update([ + 'status' => 'REJECTED_PC', + 'reject_reason' => $reason, + 'rejected_by' => $opsBy, + 'rejected_at' => $now, + 'ops_by' => $opsBy, + 'batch_id' => null, + ]); + + $batch = null; + if ($batchId) { + $batch = $this->refreshBatchStatus($mysql, $batchId); + } + + $fresh = PembiayaanOnline::on($mysql)->where('id', $line->id)->first(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'pembiayaan_online' => $fresh, + 'batch' => $batch, + ], + ]; + }); + } catch (\Throwable $e) { + Log::error('operasiRejectLine failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal tolak pembiayaan online'], + ]; + } + } + + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function operasiApproveBatch($kodcaw, $batchId, $opsBy) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $opsBy = trim((string) $opsBy); + if ($opsBy === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ops_by diperlukan'], + ]; + } + + try { + return DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $batchId, $opsBy) { + $batch = BatchPembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('id', $batchId) + ->lockForUpdate() + ->first(); + + if (!$batch) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Batch tidak dijumpai'], + ]; + } + + $updated = PembiayaanOnline::on($mysql) + ->where('batch_id', $batch->id) + ->whereIn('status', ['IN_BATCH', 'OPS_REVIEW']) + ->update([ + 'status' => 'APPROVED', + 'ops_by' => $opsBy, + 'reject_reason' => null, + 'rejected_by' => null, + 'rejected_at' => null, + ]); + + $batch = $this->refreshBatchStatus($mysql, $batch->id); + $lines = PembiayaanOnline::on($mysql) + ->where('batch_id', $batch->id) + ->orderBy('id', 'asc') + ->get(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'diluluskan' => $updated, + 'batch' => $batch, + 'lines' => $lines, + ], + ]; + }); + } catch (\Throwable $e) { + Log::error('operasiApproveBatch failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal lulus batch'], + ]; + } + } + + /** + * Operasi mark one APPROVED line as PAID + upload bukti bayaran. + * + * @param array $data ops_by, no_rujukan_bayaran, tarikh_bayaran?, + * bukti_file_id? OR bukti_data + bukti_filename + bukti_type (base64 like CIT) + * @return array{ok:bool,status:int,payload:mixed} + */ + public function operasiMarkPaid($kodcaw, $id, array $data) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $opsBy = trim((string) ($data['ops_by'] ?? '')); + $noRujukanBayaran = trim((string) ($data['no_rujukan_bayaran'] ?? '')); + $tarikhBayaran = $data['tarikh_bayaran'] ?? Carbon::now()->toDateString(); + + if ($opsBy === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ops_by diperlukan'], + ]; + } + if ($noRujukanBayaran === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'no_rujukan_bayaran diperlukan'], + ]; + } + + try { + Carbon::parse($tarikhBayaran); + } catch (\Throwable $e) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'tarikh_bayaran tidak sah'], + ]; + } + + try { + return DB::connection($mysql)->transaction(function () use ( + $mysql, + $kodcaw, + $id, + $opsBy, + $noRujukanBayaran, + $tarikhBayaran, + $data + ) { + $line = PembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('id', $id) + ->lockForUpdate() + ->first(); + + if (!$line) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Pembiayaan online tidak dijumpai'], + ]; + } + + if ($line->status === 'PAID') { + return [ + 'ok' => false, + 'status' => 409, + 'payload' => [ + 'message' => 'Rekod sudah PAID', + 'pembiayaan_online' => $line, + ], + ]; + } + + if ($line->status !== 'APPROVED') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Hanya rekod APPROVED boleh ditanda PAID', + 'status' => $line->status, + ], + ]; + } + + $buktiFileId = $data['bukti_file_id'] ?? null; + if (empty($buktiFileId)) { + $saved = $this->storeBuktiFile($data); + if (!$saved['ok']) { + return $saved; + } + $buktiFileId = $saved['payload']['id']; + } else { + $exists = ARFiles::on('mysql7')->where('id', $buktiFileId)->first(); + if (!$exists) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'bukti_file_id tidak dijumpai'], + ]; + } + } + + $now = Carbon::now(); + + PembiayaanOnline::on($mysql)->where('id', $line->id)->update([ + 'status' => 'PAID', + 'bukti_file_id' => $buktiFileId, + 'no_rujukan_bayaran' => $noRujukanBayaran, + 'tarikh_bayaran' => $tarikhBayaran, + 'paid_at' => $now, + 'ops_by' => $opsBy, + ]); + + $batch = null; + if ($line->batch_id) { + $batch = $this->refreshBatchStatus($mysql, $line->batch_id); + } + + $fresh = PembiayaanOnline::on($mysql)->where('id', $line->id)->first(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'pembiayaan_online' => $fresh, + 'batch' => $batch, + 'bukti_file_id' => $buktiFileId, + ], + ]; + }); + } catch (\Throwable $e) { + Log::error('operasiMarkPaid failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal tanda PAID / muat naik bukti'], + ]; + } + } + + /** + * Mark all APPROVED lines in a batch as PAID (shared bukti + rujukan). + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function operasiMarkPaidBatch($kodcaw, $batchId, array $data) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $opsBy = trim((string) ($data['ops_by'] ?? '')); + $noRujukanBayaran = trim((string) ($data['no_rujukan_bayaran'] ?? '')); + $tarikhBayaran = $data['tarikh_bayaran'] ?? Carbon::now()->toDateString(); + + if ($opsBy === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ops_by diperlukan'], + ]; + } + if ($noRujukanBayaran === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'no_rujukan_bayaran diperlukan'], + ]; + } + + try { + Carbon::parse($tarikhBayaran); + } catch (\Throwable $e) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'tarikh_bayaran tidak sah'], + ]; + } + + try { + return DB::connection($mysql)->transaction(function () use ( + $mysql, + $kodcaw, + $batchId, + $opsBy, + $noRujukanBayaran, + $tarikhBayaran, + $data + ) { + $batch = BatchPembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('id', $batchId) + ->lockForUpdate() + ->first(); + + if (!$batch) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Batch tidak dijumpai'], + ]; + } + + $lines = PembiayaanOnline::on($mysql) + ->where('batch_id', $batch->id) + ->where('status', 'APPROVED') + ->lockForUpdate() + ->get(); + + if ($lines->isEmpty()) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'Tiada rekod APPROVED dalam batch ini'], + ]; + } + + $buktiFileId = $data['bukti_file_id'] ?? null; + if (empty($buktiFileId)) { + $saved = $this->storeBuktiFile($data); + if (!$saved['ok']) { + return $saved; + } + $buktiFileId = $saved['payload']['id']; + } else { + $exists = ARFiles::on('mysql7')->where('id', $buktiFileId)->first(); + if (!$exists) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'bukti_file_id tidak dijumpai'], + ]; + } + } + + $now = Carbon::now(); + $ids = $lines->pluck('id')->all(); + + PembiayaanOnline::on($mysql)->whereIn('id', $ids)->update([ + 'status' => 'PAID', + 'bukti_file_id' => $buktiFileId, + 'no_rujukan_bayaran' => $noRujukanBayaran, + 'tarikh_bayaran' => $tarikhBayaran, + 'paid_at' => $now, + 'ops_by' => $opsBy, + ]); + + $batch = $this->refreshBatchStatus($mysql, $batch->id); + $freshLines = PembiayaanOnline::on($mysql) + ->where('batch_id', $batch->id) + ->orderBy('id', 'asc') + ->get(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'dibayar' => count($ids), + 'bukti_file_id' => $buktiFileId, + 'batch' => $batch, + 'lines' => $freshLines, + ], + ]; + }); + } catch (\Throwable $e) { + Log::error('operasiMarkPaidBatch failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal tanda PAID batch'], + ]; + } + } + + /** + * Get bukti metadata/content for a paid (or any) line. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function getBukti($kodcaw, $id) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + + $line = PembiayaanOnline::on($resolved['mysql']) + ->where('kodcaw', $kodcaw) + ->where('id', $id) + ->first(); + + if (!$line) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Pembiayaan online tidak dijumpai'], + ]; + } + + if (empty($line->bukti_file_id)) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Bukti bayaran belum dimuat naik'], + ]; + } + + $file = ARFiles::on('mysql7')->where('id', $line->bukti_file_id)->first(); + if (!$file) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Fail bukti tidak dijumpai'], + ]; + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'pembiayaan_online_id' => $line->id, + 'norujukan' => $line->norujukan, + 'no_rujukan_bayaran' => $line->no_rujukan_bayaran, + 'tarikh_bayaran' => $line->tarikh_bayaran, + 'bukti' => [ + 'id' => $file->id, + 'filename' => $file->filename, + 'type' => $file->type, + 'tarikhmasa' => $file->tarikhmasa, + 'data' => $file->data, + ], + ], + ]; + } + + /** + * PC list of PAID lines awaiting blast / tally. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function paidQueue($kodcaw, $blastSent = null) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + + $query = PembiayaanOnline::on($resolved['mysql']) + ->where('kodcaw', $kodcaw) + ->where('status', 'PAID'); + + if ($blastSent !== null && $blastSent !== '') { + $query->where('blast_sent', (int) $blastSent); + } + + $lines = $query->orderBy('paid_at', 'desc')->get(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'kodcaw' => $kodcaw, + 'bilangan' => $lines->count(), + 'jumlah' => round((float) $lines->sum('pinjaman'), 2), + 'data' => $lines, + ], + ]; + } + + /** + * PC marks PAID line(s) as tallied / blast done (blast_sent=1). + * Actual WhatsApp/SMS dispatch can be wired later. + * + * @param array|int|string $ids + * @return array{ok:bool,status:int,payload:mixed} + */ + public function markBlastSent($kodcaw, $ids, $blastBy = null) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + if (!is_array($ids)) { + $ids = array_filter(explode(',', (string) $ids)); + } + $ids = array_values(array_unique(array_map('intval', $ids))); + if (count($ids) === 0) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'ids diperlukan'], + ]; + } + + $now = Carbon::now(); + $updated = 0; + + try { + return DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $ids, $blastBy, $now, &$updated) { + $lines = PembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->whereIn('id', $ids) + ->where('status', 'PAID') + ->lockForUpdate() + ->get(); + + if ($lines->isEmpty()) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Tiada rekod PAID dijumpai untuk ids diberi'], + ]; + } + + foreach ($lines as $line) { + $nota = $line->nota; + if ($blastBy) { + $tag = '[TALLY/BLAST ' . $now->toDateTimeString() . ' oleh ' . $blastBy . ']'; + $nota = trim((string) $nota); + $nota = $nota === '' ? $tag : ($nota . "\n" . $tag); + } + + PembiayaanOnline::on($mysql)->where('id', $line->id)->update([ + 'blast_sent' => 1, + 'nota' => $nota, + ]); + $updated++; + } + + $fresh = PembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->whereIn('id', $lines->pluck('id')->all()) + ->orderBy('id', 'asc') + ->get(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'dikemaskini' => $updated, + 'data' => $fresh, + ], + ]; + }); + } catch (\Throwable $e) { + Log::error('markBlastSent failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal tanda blast/tally'], + ]; + } + } + + /** + * Store bukti into arrahn_files (mysql7), same pattern as CIT. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + protected function storeBuktiFile(array $data) + { + $buktiData = $data['bukti_data'] ?? $data['data'] ?? null; + $filename = $data['bukti_filename'] ?? $data['filename'] ?? null; + $type = $data['bukti_type'] ?? $data['type'] ?? null; + + if (empty($buktiData) || empty($filename) || empty($type)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Bukti diperlukan: bukti_data, bukti_filename, bukti_type (atau bukti_file_id)', + ], + ]; + } + + $file = new ARFiles(); + $file->data = $buktiData; + $file->filename = $filename; + $file->type = $type; + $file->tarikhmasa = Carbon::now()->toDateTimeString(); + $file->save(); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'id' => $file->id, + 'filename' => $file->filename, + 'type' => $file->type, + ], + ]; + } + + protected function refreshBatchStatus($mysql, $batchId) + { + $batch = BatchPembiayaanOnline::on($mysql)->where('id', $batchId)->first(); + if (!$batch) { + return null; + } + + $lines = PembiayaanOnline::on($mysql)->where('batch_id', $batchId)->get(); + + $bilangan = $lines->count(); + $jumlah = round((float) $lines->sum('pinjaman'), 2); + $statuses = $lines->pluck('status')->unique()->values()->all(); + + if ($bilangan === 0) { + $status = 'OPS_DONE'; + } elseif (count(array_diff($statuses, ['PAID'])) === 0) { + $status = 'PAID'; + } elseif (count(array_diff($statuses, ['APPROVED', 'PAID'])) === 0) { + $status = 'OPS_DONE'; + } elseif (in_array('IN_BATCH', $statuses, true) || in_array('OPS_REVIEW', $statuses, true)) { + $hasDecided = count(array_intersect($statuses, ['APPROVED', 'PAID'])) > 0; + $status = $hasDecided ? 'PARTIAL' : 'SUBMITTED'; + } else { + $status = 'PARTIAL'; + } + + BatchPembiayaanOnline::on($mysql)->where('id', $batchId)->update([ + 'bilangan' => $bilangan, + 'jumlah_keseluruhan' => $jumlah, + 'status' => $status, + ]); + + return BatchPembiayaanOnline::on($mysql)->where('id', $batchId)->first(); + } + + public function suggestSlot() + { + $now = Carbon::now(); + $mins = ((int) $now->format('G')) * 60 + (int) $now->format('i'); + + if ($mins < 11 * 60) { + return [ + 'slot' => 'BATCH1', + 'cutoff' => '11:00', + 'proses' => '12:30', + 'lewat' => false, + 'nota' => 'Dalam waktu Batch 1', + ]; + } + + if ($mins < 15 * 60) { + return [ + 'slot' => 'BATCH2', + 'cutoff' => '15:00', + 'proses' => '16:30', + 'lewat' => false, + 'nota' => 'Dalam waktu Batch 2', + ]; + } + + return [ + 'slot' => 'BATCH2', + 'cutoff' => '15:00', + 'proses' => '16:30 (hari berikutnya jika sudah lewat)', + 'lewat' => true, + 'nota' => 'Selepas cutoff Batch 2 — masih boleh hantar; Operasi mungkin proses esok', + ]; + } + + protected function nextBatchNo($mysql, $kodcaw, $tarikh, $slot) + { + $slotTag = $slot === 'BATCH1' ? 'B1' : 'B2'; + $ymd = Carbon::parse($tarikh)->format('Ymd'); + $prefix = strtoupper($kodcaw) . '-PO-' . $ymd . '-' . $slotTag . '-'; + + $last = BatchPembiayaanOnline::on($mysql) + ->where('kodcaw', $kodcaw) + ->where('tarikh', $tarikh) + ->where('slot', $slot) + ->orderBy('id', 'desc') + ->first(); + + $seq = 1; + if ($last && preg_match('/-(\d+)$/', $last->batch_no, $m)) { + $seq = ((int) $m[1]) + 1; + } + + return $prefix . str_pad((string) $seq, 3, '0', STR_PAD_LEFT); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index d358544..e5febcc 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -23,6 +23,33 @@ $app = new Laravel\Lumen\Application( dirname(__DIR__) ); +/* +| Laravel's storage_path() helper resolves app('path.storage'). Lumen does not +| register that binding by default; config files that call storage_path() at +| load time (e.g. config/logging.php) would fail without this. +*/ +$app->instance('path.storage', $app->storagePath()); + +/* +| Laravel's Foundation helpers are loaded before Lumen's and win for names like +| response() and url(). Those helpers resolve Illuminate\Contracts\* from the +| container; Lumen only registers the "url" service, not the contracts. Map the +| contracts to Lumen's implementations so helpers work across the app. +*/ +$app->singleton( + \Illuminate\Contracts\Routing\ResponseFactory::class, + function () { + return new \Laravel\Lumen\Http\ResponseFactory(); + } +); + +$app->singleton( + \Illuminate\Contracts\Routing\UrlGenerator::class, + function ($app) { + return $app->make('url'); + } +); + $app->withFacades(); $app->withEloquent(); @@ -60,6 +87,7 @@ $app->singleton( */ $app->configure('app'); +$app->configure('logging'); $app->configure('tinker'); $app->configure('tennant_database'); $app->configure('report'); diff --git a/config/app_parameter.php b/config/app_parameter.php index c46f175..b9b4014 100644 --- a/config/app_parameter.php +++ b/config/app_parameter.php @@ -1,7 +1,7 @@ env('APP_URL', url('/')), + 'url' => env('APP_URL', 'http://localhost'), 'api_arrahnbid' => env('API_ARRAHNBID', 'http://lelong.test/api/'), 'api_v3' => env('API_V3', 'http://apiv3.localhost/api/') ]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..985406d --- /dev/null +++ b/config/logging.php @@ -0,0 +1,103 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['single'], + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => 14, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + 'emoji' => ':boom:', + 'level' => env('LOG_LEVEL', 'critical'), + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => SyslogUdpHandler::class, + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + ], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + ], + +]; diff --git a/database/migrations/2026_08_10_100001_create_modal_online_table.php b/database/migrations/2026_08_10_100001_create_modal_online_table.php new file mode 100644 index 0000000..8863321 --- /dev/null +++ b/database/migrations/2026_08_10_100001_create_modal_online_table.php @@ -0,0 +1,53 @@ +getPdo(); + return true; + } catch (\Exception $e) { + return false; + } + }); + } + + public function up() + { + foreach ($this->getActiveDB() as $dbase) { + if (!Schema::connection($dbase)->hasTable('modal_online')) { + Schema::connection($dbase)->create('modal_online', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->date('tarikh'); + $table->string('kodcaw', 20); + $table->decimal('had', 12, 2)->default(100000.00); + $table->decimal('digunakan', 12, 2)->default(0.00); + $table->decimal('baki', 12, 2)->default(100000.00); + $table->decimal('tambah', 12, 2)->default(0.00); + $table->decimal('kurang', 12, 2)->default(0.00); + $table->timestamps(); + $table->unique(['tarikh', 'kodcaw']); + }); + } + } + } + + public function down() + { + foreach ($this->getActiveDB() as $dbase) { + Schema::connection($dbase)->dropIfExists('modal_online'); + } + } +} diff --git a/database/migrations/2026_08_10_100002_create_batch_pembiayaan_online_table.php b/database/migrations/2026_08_10_100002_create_batch_pembiayaan_online_table.php new file mode 100644 index 0000000..6ef7ed5 --- /dev/null +++ b/database/migrations/2026_08_10_100002_create_batch_pembiayaan_online_table.php @@ -0,0 +1,55 @@ +getPdo(); + return true; + } catch (\Exception $e) { + return false; + } + }); + } + + public function up() + { + foreach ($this->getActiveDB() as $dbase) { + if (!Schema::connection($dbase)->hasTable('batch_pembiayaan_online')) { + Schema::connection($dbase)->create('batch_pembiayaan_online', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('batch_no', 50); + $table->string('kodcaw', 20); + $table->date('tarikh'); + $table->string('slot', 20)->nullable()->comment('BATCH1 / BATCH2'); + $table->integer('bilangan')->default(0); + $table->decimal('jumlah_keseluruhan', 12, 2)->default(0.00); + $table->string('status', 30)->default('DRAFT'); + $table->string('pc_submitted_by', 100)->nullable(); + $table->timestamp('submitted_at')->nullable(); + $table->timestamps(); + $table->index(['kodcaw', 'tarikh', 'status']); + }); + } + } + } + + public function down() + { + foreach ($this->getActiveDB() as $dbase) { + Schema::connection($dbase)->dropIfExists('batch_pembiayaan_online'); + } + } +} diff --git a/database/migrations/2026_08_10_100003_create_pembiayaan_online_table.php b/database/migrations/2026_08_10_100003_create_pembiayaan_online_table.php new file mode 100644 index 0000000..ebc7c92 --- /dev/null +++ b/database/migrations/2026_08_10_100003_create_pembiayaan_online_table.php @@ -0,0 +1,74 @@ +getPdo(); + return true; + } catch (\Exception $e) { + return false; + } + }); + } + + public function up() + { + foreach ($this->getActiveDB() as $dbase) { + if (!Schema::connection($dbase)->hasTable('pembiayaan_online')) { + Schema::connection($dbase)->create('pembiayaan_online', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('norujukan', 50); + $table->string('nokp', 20); + $table->string('nopelanggan', 50)->nullable(); + $table->string('nama_pelanggan', 150)->nullable(); + $table->string('kodcaw', 20); + $table->decimal('pinjaman', 12, 2); + $table->string('kodbank', 20)->nullable(); + $table->string('nama_bank', 100)->nullable(); + $table->string('noakaun', 50)->nullable(); + $table->string('nama_akaun', 150)->nullable(); + $table->unsignedBigInteger('bukti_akaun_file_id')->nullable(); + $table->unsignedBigInteger('borang_file_id')->nullable(); + $table->string('status', 30)->default('PENDING_PC'); + $table->text('reject_reason')->nullable(); + $table->string('rejected_by', 100)->nullable(); + $table->timestamp('rejected_at')->nullable(); + $table->unsignedBigInteger('batch_id')->nullable(); + $table->unsignedBigInteger('bukti_file_id')->nullable(); + $table->string('no_rujukan_bayaran', 100)->nullable(); + $table->date('tarikh_bayaran')->nullable(); + $table->timestamp('paid_at')->nullable(); + $table->string('created_by', 100)->nullable(); + $table->string('pc_submitted_by', 100)->nullable(); + $table->string('ops_by', 100)->nullable(); + $table->text('nota')->nullable(); + $table->tinyInteger('blast_sent')->default(0); + $table->timestamps(); + $table->index(['kodcaw', 'status']); + $table->index('norujukan'); + $table->index('batch_id'); + }); + } + } + } + + public function down() + { + foreach ($this->getActiveDB() as $dbase) { + Schema::connection($dbase)->dropIfExists('pembiayaan_online'); + } + } +} diff --git a/database/migrations/2026_08_10_100004_add_cara_terima_to_gadai_table.php b/database/migrations/2026_08_10_100004_add_cara_terima_to_gadai_table.php new file mode 100644 index 0000000..5326172 --- /dev/null +++ b/database/migrations/2026_08_10_100004_add_cara_terima_to_gadai_table.php @@ -0,0 +1,54 @@ +getPdo(); + return true; + } catch (\Exception $e) { + return false; + } + }); + } + + public function up() + { + foreach ($this->getActiveDB() as $dbase) { + if ( + Schema::connection($dbase)->hasTable('gadai') + && !Schema::connection($dbase)->hasColumn('gadai', 'cara_terima') + ) { + Schema::connection($dbase)->table('gadai', function (Blueprint $table) { + $table->string('cara_terima', 20)->nullable()->after('pinjaman'); + }); + } + } + } + + public function down() + { + foreach ($this->getActiveDB() as $dbase) { + if ( + Schema::connection($dbase)->hasTable('gadai') + && Schema::connection($dbase)->hasColumn('gadai', 'cara_terima') + ) { + Schema::connection($dbase)->table('gadai', function (Blueprint $table) { + $table->dropColumn('cara_terima'); + }); + } + } + } +} diff --git a/database/migrations/2026_08_10_100005_create_request_modal_online_table.php b/database/migrations/2026_08_10_100005_create_request_modal_online_table.php new file mode 100644 index 0000000..2143874 --- /dev/null +++ b/database/migrations/2026_08_10_100005_create_request_modal_online_table.php @@ -0,0 +1,31 @@ +hasTable('request_modal_online')) { + Schema::connection('mysql7')->create('request_modal_online', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('kodcaw', 20); + $table->date('tarikh'); + $table->decimal('amaun', 12, 2); + $table->string('dimohon_oleh', 100)->nullable(); + $table->string('status', 20)->default('PENDING'); + $table->string('diluluskan_oleh', 100)->nullable(); + $table->text('nota')->nullable(); + $table->timestamps(); + $table->index(['kodcaw', 'tarikh', 'status']); + }); + } + } + + public function down() + { + Schema::connection('mysql7')->dropIfExists('request_modal_online'); + } +} diff --git a/database/migrations/2026_08_11_100000_create_arrahn_files_table.php b/database/migrations/2026_08_11_100000_create_arrahn_files_table.php new file mode 100644 index 0000000..2f3e14e --- /dev/null +++ b/database/migrations/2026_08_11_100000_create_arrahn_files_table.php @@ -0,0 +1,38 @@ +hasTable('arrahn_files')) { + Schema::connection('mysql7')->create('arrahn_files', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->unsignedBigInteger('files_id')->nullable()->index(); + $table->longText('data')->nullable(); + $table->string('filename', 255)->nullable(); + $table->string('type', 100)->nullable(); + $table->string('tarikhmasa', 50)->nullable(); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::connection('mysql7')->dropIfExists('arrahn_files'); + } +} diff --git a/database/migrations/2026_08_12_100000_add_eod_close_columns_to_modal_online_table.php b/database/migrations/2026_08_12_100000_add_eod_close_columns_to_modal_online_table.php new file mode 100644 index 0000000..4c68a0f --- /dev/null +++ b/database/migrations/2026_08_12_100000_add_eod_close_columns_to_modal_online_table.php @@ -0,0 +1,61 @@ +getPdo(); + + return true; + } catch (\Exception $e) { + return false; + } + } + ); + } + + public function up() + { + foreach ($this->getActiveDB() as $dbase) { + Schema::connection($dbase)->table('modal_online', function (Blueprint $table) { + $table->tinyInteger('ditutup') + ->default(0) + ->after('kurang'); + + $table->string('ditutup_oleh', 100) + ->nullable() + ->after('ditutup'); + + $table->timestamp('ditutup_at') + ->nullable() + ->after('ditutup_oleh'); + }); + } + } + + public function down() + { + foreach ($this->getActiveDB() as $dbase) { + Schema::connection($dbase)->table('modal_online', function (Blueprint $table) { + $table->dropColumn([ + 'ditutup', + 'ditutup_oleh', + 'ditutup_at', + ]); + }); + } + } +} \ No newline at end of file diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 223a96c..47a1830 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -9,7 +9,7 @@ server { client_max_body_size 50M; location / { - try_files $uri $uri/ @php; + try_files $uri @php; } location @php { diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 172df1c..45aac51 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -4,22 +4,22 @@ WORKDIR /var/www/html RUN apt-get update \ && apt-get install -y --no-install-recommends \ - git \ - pkg-config \ - unzip \ - libonig-dev \ - libzip-dev \ - libpng-dev \ - libjpeg62-turbo-dev \ - libfreetype6-dev \ + git \ + pkg-config \ + unzip \ + libonig-dev \ + libzip-dev \ + libpng-dev \ + libjpeg62-turbo-dev \ + libfreetype6-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j"$(nproc)" \ - pdo_mysql \ - mbstring \ - bcmath \ - zip \ - exif \ - gd \ + pdo_mysql \ + mbstring \ + bcmath \ + zip \ + exif \ + gd \ && rm -rf /var/lib/apt/lists/* COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer @@ -40,12 +40,12 @@ COPY --chown=www-data:www-data . /var/www/html RUN composer install --no-interaction --prefer-dist --no-dev --optimize-autoloader \ && mkdir -p \ - storage/logs \ - storage/framework/cache/data \ - storage/framework/views \ - storage/framework/sessions \ - storage/app/public \ - bootstrap/cache \ + storage/logs \ + storage/framework/cache/data \ + storage/framework/views \ + storage/framework/sessions \ + storage/app/public \ + bootstrap/cache \ && chown -R www-data:www-data storage bootstrap/cache vendor FROM base AS development diff --git a/routes/web.php b/routes/web.php index a239c1d..8a66c5b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -371,9 +371,11 @@ $router->group(['prefix'=>'api'], function () use ($router){ $router->get('GetCurrentTunai/{kodcaw}',['uses' =>'TunaiController@getCurrentTunai']); $router->get('getPastTunai/{kodcaw}',['uses' =>'TunaiController@getTunaiPast']); $router->get('GetLatestTunai/{kodcaw}',['uses' =>'TunaiController@getLatestTunai']); + $router->get('getAddTunai/{kodcaw}/kodlaluan',['uses' =>'TunaiController@getAddTunaibyKodlaluan']); + $router->get('getAddTunai/{kodcaw}/kodlaluan/',['uses' =>'TunaiController@getAddTunaibyKodlaluan']); + $router->get('getAddTunai/{kodcaw}',['uses' =>'TunaiController@getAddTunai']); $router->get('getAddTunai/{kodcaw}/',['uses' =>'TunaiController@getAddTunai']); $router->get('getAddTunaiDate/{kodcaw}/',['uses' =>'TunaiController@getAddTunaiDate']); - $router->get('getAddTunai/{kodcaw}/kodlaluan/',['uses' =>'TunaiController@getAddTunaibyKodlaluan']); $router->get('getAddTunaiLatest/{kodcaw}',['uses' =>'TunaiController@getAddTunaibyKodlaluanLatest']); $router->put('gadai/hapus/{kodcaw}/{norujukan}', ['uses' => 'GadaiController@GadaiHapus']); $router->put('tebus/hapus/{kodcaw}/{norujukan}', ['uses' => 'GadaiController@TebusHapus']); @@ -516,6 +518,7 @@ $router->group(['prefix'=>'api'], function () use ($router){ $router->get('TuntutBaki/{kodcaw}',['uses'=>'LelongController@showTuntutBaki']); $router->get('TuntutBakiRangeDate/{kodcaw}',['uses'=>'LelongController@showTuntutBakiRangeDate']); $router->get('TuntutBaki/{kodcaw}/teller',['uses'=>'LelongController@showTuntutBakiTeller']); + $router->get('TuntutBaki/{kodcaw}/teller/',['uses'=>'LelongController@showTuntutBakiTeller']); $router->get('TuntutBaki/resit/{kodcaw}/{norujukan}',['uses'=>'LelongController@resitLelong']); $router->post('Tuntutbakisimpan/{kodcaw}',['uses'=>'LelongController@simpantuntutbaki']); $router->put('tuntutbakiupdate/{kodcaw}/{norujukan}',['uses'=>'LelongController@updatetagtbaki']); @@ -673,6 +676,42 @@ $router->group(['prefix'=>'api'], function () use ($router){ //initiateTunai $router->get('initiate/tunai',['uses'=>'TunaiController@initiateTunai']); + + // Modal Online (pembiayaan atas talian float) + // Operasi top-up routes before {kodcaw} catch-alls + $router->get('modal-online/operasi/topup/requests', ['uses' => 'ModalOnlineController@listTopUpOperasi']); + $router->post('modal-online/operasi/topup/{id}/approve', ['uses' => 'ModalOnlineController@approveTopUp']); + $router->post('modal-online/operasi/topup/{id}/reject', ['uses' => 'ModalOnlineController@rejectTopUp']); + $router->get('modal-online/operasi/eod', ['uses' => 'ModalOnlineController@listToday']); + $router->post('modal-online/operasi/eod/close-all', ['uses' => 'ModalOnlineController@closeAll']); + + $router->get('modal-online/{kodcaw}', ['uses' => 'ModalOnlineController@get']); + $router->post('modal-online/{kodcaw}/reserve', ['uses' => 'ModalOnlineController@reserve']); + $router->post('modal-online/{kodcaw}/topup/request', ['uses' => 'ModalOnlineController@requestTopUp']); + $router->get('modal-online/{kodcaw}/topup/requests', ['uses' => 'ModalOnlineController@listTopUpByCawangan']); + $router->post('modal-online/{kodcaw}/close', ['uses' => 'ModalOnlineController@close']); + $router->get('initiate/modal-online', ['uses' => 'ModalOnlineController@initiate']); + + // Operasi kelulusan pembiayaan online (must be before {kodcaw} routes) + $router->get('pembiayaan-online/operasi/batches', ['uses' => 'PembiayaanOnlineController@operasiListBatches']); + $router->get('pembiayaan-online/operasi/{kodcaw}/batches/{batchId}', ['uses' => 'PembiayaanOnlineController@operasiShowBatch']); + $router->post('pembiayaan-online/operasi/{kodcaw}/lines/{id}/approve', ['uses' => 'PembiayaanOnlineController@operasiApproveLine']); + $router->post('pembiayaan-online/operasi/{kodcaw}/lines/{id}/reject', ['uses' => 'PembiayaanOnlineController@operasiRejectLine']); + $router->post('pembiayaan-online/operasi/{kodcaw}/lines/{id}/paid', ['uses' => 'PembiayaanOnlineController@operasiMarkPaid']); + $router->post('pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/approve', ['uses' => 'PembiayaanOnlineController@operasiApproveBatch']); + $router->post('pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/paid', ['uses' => 'PembiayaanOnlineController@operasiMarkPaidBatch']); + + // Pembiayaan Online (payout line + PC queue / batch) + $router->post('pembiayaan-online/{kodcaw}', ['uses' => 'PembiayaanOnlineController@store']); + $router->get('pembiayaan-online/{kodcaw}/queue', ['uses' => 'PembiayaanOnlineController@queue']); + $router->get('pembiayaan-online/{kodcaw}/paid', ['uses' => 'PembiayaanOnlineController@paidQueue']); + $router->post('pembiayaan-online/{kodcaw}/paid/blast', ['uses' => 'PembiayaanOnlineController@markBlastSent']); + $router->put('pembiayaan-online/{kodcaw}/lines/{id}', ['uses' => 'PembiayaanOnlineController@updateLine']); + $router->get('pembiayaan-online/{kodcaw}/lines/{id}/bukti', ['uses' => 'PembiayaanOnlineController@getBukti']); + $router->get('pembiayaan-online/{kodcaw}/batches', ['uses' => 'PembiayaanOnlineController@listBatches']); + $router->get('pembiayaan-online/{kodcaw}/batches/{batchId}', ['uses' => 'PembiayaanOnlineController@showBatch']); + $router->post('pembiayaan-online/{kodcaw}/batches/submit', ['uses' => 'PembiayaanOnlineController@submitBatch']); + $router->get('pembiayaan-online/{kodcaw}/by-norujukan/{norujukan}', ['uses' => 'PembiayaanOnlineController@show']); //WTD $router->get('check/wtd',['uses'=>'WTDController@getDraftWTD']); $router->get('check/customer',['uses' => 'WTDController@getCustomerWTD']);