Files
api_arrahn/app/Services/PembiayaanOnline/PembiayaanOnlineService.php
T
ismailmasseran bd605f398e
Build Docker Image / build-backend (push) Successful in 36s
Explore/ismail (#6)
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Reviewed-on: #6
2026-09-01 13:53:07 +08:00

1559 lines
51 KiB
PHP

<?php
namespace App\Services\PembiayaanOnline;
use App\ARFiles;
use App\Bank;
use App\BatchPembiayaanOnline;
use App\Cawangan;
use App\Customer;
use App\Gadai;
use App\PembiayaanOnline;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class PembiayaanOnlineService
{
const MIN_PINJAMAN = 5000.00;
const MAX_PINJAMAN = 50000.00;
const PC_QUEUE_STATUSES = ['PENDING_PC', 'REJECTED_PC'];
protected $modalOnlineService;
public function __construct(ModalOnlineService $modalOnlineService = null)
{
$this->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);
}
}