diff --git a/app/Http/Controllers/PembiayaanOnlineController.php b/app/Http/Controllers/PembiayaanOnlineController.php index 700fdd0..547736f 100644 --- a/app/Http/Controllers/PembiayaanOnlineController.php +++ b/app/Http/Controllers/PembiayaanOnlineController.php @@ -35,6 +35,14 @@ class PembiayaanOnlineController extends Controller return $this->respond($this->service->show($kodcaw, $norujukan)); } + /** + * GET /api/pembiayaan-online/{kodcaw}/lines/{id} + */ + public function showLine($kodcaw, $id) + { + return $this->respond($this->service->showLine($kodcaw, $id)); + } + /** * GET /api/pembiayaan-online/{kodcaw}/queue */ @@ -53,6 +61,7 @@ class PembiayaanOnlineController extends Controller foreach ([ 'kodbank', 'noakaun', 'nama_bank', 'nama_akaun', 'nota', 'bukti_akaun_file_id', 'borang_file_id', 'update_customer_bank', + 'bukti_akaun_data', 'bukti_akaun_filename', 'bukti_akaun_type', ] as $key) { if ($request->has($key)) { $input[$key] = $request->input($key); @@ -62,6 +71,26 @@ class PembiayaanOnlineController extends Controller return $this->respond($this->service->updateLine($kodcaw, $id, $input)); } + /** + * POST /api/pembiayaan-online/{kodcaw}/lines/{id}/verify + */ + public function verifyLine($kodcaw, $id, Request $request) + { + return $this->respond($this->service->verifyLine( + $kodcaw, + $id, + $request->input('pc_verified_by', '') + )); + } + + /** + * POST /api/pembiayaan-online/{kodcaw}/lines/{id}/unverify + */ + public function unverifyLine($kodcaw, $id) + { + return $this->respond($this->service->unverifyLine($kodcaw, $id)); + } + /** * GET /api/pembiayaan-online/{kodcaw}/batches */ @@ -177,6 +206,14 @@ class PembiayaanOnlineController extends Controller return $this->respond($this->service->getBukti($kodcaw, $id)); } + /** + * GET /api/pembiayaan-online/{kodcaw}/lines/{id}/bukti-akaun + */ + public function getBuktiAkaun($kodcaw, $id) + { + return $this->respond($this->service->getBuktiAkaun($kodcaw, $id)); + } + /** * GET /api/pembiayaan-online/{kodcaw}/paid?blast_sent=0 */ diff --git a/app/PembiayaanOnline.php b/app/PembiayaanOnline.php index a1bf1b3..079e3ee 100644 --- a/app/PembiayaanOnline.php +++ b/app/PembiayaanOnline.php @@ -32,6 +32,8 @@ class PembiayaanOnline extends Model 'paid_at', 'created_by', 'pc_submitted_by', + 'pc_verified_by', + 'pc_verified_at', 'ops_by', 'nota', 'blast_sent', diff --git a/app/Services/PembiayaanOnline/PembiayaanOnlineService.php b/app/Services/PembiayaanOnline/PembiayaanOnlineService.php index abd0231..27c2c24 100644 --- a/app/Services/PembiayaanOnline/PembiayaanOnlineService.php +++ b/app/Services/PembiayaanOnline/PembiayaanOnlineService.php @@ -16,9 +16,11 @@ use Illuminate\Support\Facades\Log; class PembiayaanOnlineService { - const MIN_PINJAMAN = 5000.00; + const MIN_PINJAMAN = 1.00; const MAX_PINJAMAN = 50000.00; - const PC_QUEUE_STATUSES = ['PENDING_PC', 'REJECTED_PC']; + const PC_QUEUE_STATUSES = ['PENDING_PC', 'REJECTED_PC', 'VERIFIED_PC']; + const PC_VERIFY_FROM_STATUSES = ['PENDING_PC', 'REJECTED_PC']; + const PC_SUBMITTABLE_STATUSES = ['VERIFIED_PC']; protected $modalOnlineService; @@ -258,6 +260,36 @@ class PembiayaanOnlineService ]; } + /** + * @return array{ok:bool,status:int,payload:mixed} + */ + public function showLine($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'], + ]; + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => $line, + ]; + } + /** * @return array{ok:bool,status:int,payload:mixed} */ @@ -332,7 +364,7 @@ class PembiayaanOnlineService 'ok' => false, 'status' => 422, 'payload' => [ - 'message' => 'Hanya rekod PENDING_PC / REJECTED_PC boleh dikemaskini', + 'message' => 'Hanya rekod PENDING_PC / REJECTED_PC / VERIFIED_PC boleh dikemaskini', 'status' => $line->status, ], ]; @@ -368,6 +400,18 @@ class PembiayaanOnlineService $fields['borang_file_id'] = $input['borang_file_id']; } + if (!empty($input['bukti_akaun_data'])) { + $saved = $this->storeBuktiFile([ + 'data' => $input['bukti_akaun_data'], + 'filename' => $input['bukti_akaun_filename'] ?? $input['filename'] ?? null, + 'type' => $input['bukti_akaun_type'] ?? $input['type'] ?? null, + ]); + if (!$saved['ok']) { + return $saved; + } + $fields['bukti_akaun_file_id'] = $saved['payload']['id']; + } + if (empty($fields)) { return [ 'ok' => false, @@ -395,6 +439,12 @@ class PembiayaanOnlineService ? (bool) $input['update_customer_bank'] : true; + if ($line->status === 'VERIFIED_PC') { + $fields['status'] = !empty($line->reject_reason) ? 'REJECTED_PC' : 'PENDING_PC'; + $fields['pc_verified_by'] = null; + $fields['pc_verified_at'] = null; + } + try { DB::connection($mysql)->transaction(function () use ($mysql, $line, $fields, $updateCustomerBank) { PembiayaanOnline::on($mysql)->where('id', $line->id)->update($fields); @@ -433,6 +483,159 @@ class PembiayaanOnlineService } } + /** + * PC confirms one queue line before it can be submitted to Operasi. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function verifyLine($kodcaw, $id, $pcBy) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + $pcBy = trim((string) $pcBy); + if ($pcBy === '') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => ['message' => 'pc_verified_by diperlukan'], + ]; + } + + try { + return DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $id, $pcBy) { + $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 === 'VERIFIED_PC') { + return [ + 'ok' => true, + 'status' => 200, + 'payload' => $line, + ]; + } + + if (!in_array($line->status, self::PC_VERIFY_FROM_STATUSES, true)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Hanya rekod PENDING_PC / REJECTED_PC boleh disemak', + 'status' => $line->status, + ], + ]; + } + + if (empty($line->kodbank) || empty($line->noakaun)) { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Maklumat bank tidak lengkap. Kemaskini bank sebelum semak.', + 'id' => $line->id, + 'norujukan' => $line->norujukan, + ], + ]; + } + + PembiayaanOnline::on($mysql)->where('id', $line->id)->update([ + 'status' => 'VERIFIED_PC', + 'pc_verified_by' => $pcBy, + 'pc_verified_at' => Carbon::now(), + ]); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => PembiayaanOnline::on($mysql)->where('id', $line->id)->first(), + ]; + }); + } catch (\Throwable $e) { + Log::error('PembiayaanOnline verifyLine failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal semak pembiayaan online'], + ]; + } + } + + /** + * Undo PC verification so the line must be checked again. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function unverifyLine($kodcaw, $id) + { + $resolved = $this->resolveMysql($kodcaw); + if (!$resolved['ok']) { + return $resolved; + } + $mysql = $resolved['mysql']; + + try { + return DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $id) { + $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 !== 'VERIFIED_PC') { + return [ + 'ok' => false, + 'status' => 422, + 'payload' => [ + 'message' => 'Hanya rekod VERIFIED_PC boleh dibatalkan semakan', + 'status' => $line->status, + ], + ]; + } + + PembiayaanOnline::on($mysql)->where('id', $line->id)->update([ + 'status' => !empty($line->reject_reason) ? 'REJECTED_PC' : 'PENDING_PC', + 'pc_verified_by' => null, + 'pc_verified_at' => null, + ]); + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => PembiayaanOnline::on($mysql)->where('id', $line->id)->first(), + ]; + }); + } catch (\Throwable $e) { + Log::error('PembiayaanOnline unverifyLine failed: ' . $e->getMessage()); + return [ + 'ok' => false, + 'status' => 500, + 'payload' => ['message' => 'Gagal batal semakan pembiayaan online'], + ]; + } + } + /** * @return array{ok:bool,status:int,payload:mixed} */ @@ -573,12 +776,12 @@ class PembiayaanOnlineService } foreach ($lines as $line) { - if (!in_array($line->status, self::PC_QUEUE_STATUSES, true)) { + if (!in_array($line->status, self::PC_SUBMITTABLE_STATUSES, true)) { return [ 'ok' => false, 'status' => 422, 'payload' => [ - 'message' => 'Rekod bukan dalam queue PC', + 'message' => 'Rekod belum disemak PC. Sahkan satu-satu sebelum hantar ke Operasi.', 'id' => $line->id, 'norujukan' => $line->norujukan, 'status' => $line->status, @@ -869,6 +1072,8 @@ class PembiayaanOnlineService 'rejected_at' => $now, 'ops_by' => $opsBy, 'batch_id' => null, + 'pc_verified_by' => null, + 'pc_verified_at' => null, ]); $batch = null; @@ -1314,6 +1519,65 @@ class PembiayaanOnlineService ]; } + /** + * Get attached bank-account slip (bukti_akaun_file_id) for a line. + * + * @return array{ok:bool,status:int,payload:mixed} + */ + public function getBuktiAkaun($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_akaun_file_id)) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Slip bank belum dimuat naik'], + ]; + } + + $file = ARFiles::on('mysql7')->where('id', $line->bukti_akaun_file_id)->first(); + if (!$file) { + return [ + 'ok' => false, + 'status' => 404, + 'payload' => ['message' => 'Fail slip bank tidak dijumpai'], + ]; + } + + return [ + 'ok' => true, + 'status' => 200, + 'payload' => [ + 'pembiayaan_online_id' => $line->id, + 'norujukan' => $line->norujukan, + 'file' => [ + 'id' => $file->id, + 'filename' => $file->filename, + 'type' => $file->type, + 'tarikhmasa' => $file->tarikhmasa, + 'data' => $file->data, + ], + ], + ]; + } + /** * PC list of PAID lines awaiting blast / tally. * diff --git a/database/migrations/2026_09_17_100000_add_pc_verified_to_pembiayaan_online_table.php b/database/migrations/2026_09_17_100000_add_pc_verified_to_pembiayaan_online_table.php new file mode 100644 index 0000000..b98f47c --- /dev/null +++ b/database/migrations/2026_09_17_100000_add_pc_verified_to_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')) { + continue; + } + + Schema::connection($dbase)->table('pembiayaan_online', function (Blueprint $table) use ($dbase) { + if (!Schema::connection($dbase)->hasColumn('pembiayaan_online', 'pc_verified_by')) { + $table->string('pc_verified_by', 100) + ->nullable() + ->after('pc_submitted_by'); + } + + if (!Schema::connection($dbase)->hasColumn('pembiayaan_online', 'pc_verified_at')) { + $table->timestamp('pc_verified_at') + ->nullable() + ->after('pc_verified_by'); + } + }); + } + } + + public function down() + { + foreach ($this->getActiveDB() as $dbase) { + if (!Schema::connection($dbase)->hasTable('pembiayaan_online')) { + continue; + } + + Schema::connection($dbase)->table('pembiayaan_online', function (Blueprint $table) use ($dbase) { + $cols = []; + if (Schema::connection($dbase)->hasColumn('pembiayaan_online', 'pc_verified_at')) { + $cols[] = 'pc_verified_at'; + } + if (Schema::connection($dbase)->hasColumn('pembiayaan_online', 'pc_verified_by')) { + $cols[] = 'pc_verified_by'; + } + if (!empty($cols)) { + $table->dropColumn($cols); + } + }); + } + } +} diff --git a/routes/web.php b/routes/web.php index 2aa7084..39fc2c3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -713,7 +713,11 @@ $router->group(['prefix'=>'api'], function () use ($router){ $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->post('pembiayaan-online/{kodcaw}/lines/{id}/verify', ['uses' => 'PembiayaanOnlineController@verifyLine']); + $router->post('pembiayaan-online/{kodcaw}/lines/{id}/unverify', ['uses' => 'PembiayaanOnlineController@unverifyLine']); $router->get('pembiayaan-online/{kodcaw}/lines/{id}/bukti', ['uses' => 'PembiayaanOnlineController@getBukti']); + $router->get('pembiayaan-online/{kodcaw}/lines/{id}/bukti-akaun', ['uses' => 'PembiayaanOnlineController@getBuktiAkaun']); + $router->get('pembiayaan-online/{kodcaw}/lines/{id}', ['uses' => 'PembiayaanOnlineController@showLine']); $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']);