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); } }