2197 lines
72 KiB
PHP
2197 lines
72 KiB
PHP
<?php
|
|
|
|
namespace App\Services\PembiayaanOnline;
|
|
|
|
use App\ARFiles;
|
|
use App\Bank;
|
|
use App\BatchPembiayaanOnline;
|
|
use App\Cawangan;
|
|
use App\Customer;
|
|
use App\Feature;
|
|
use App\Gadai;
|
|
use App\Jobs\WhatsappPembiayaanOnlinePaid;
|
|
use App\PembiayaanOnline;
|
|
use App\Services\StaffNotificationClient;
|
|
use App\Services\Whatsapp\WhatsappCloudClient;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class PembiayaanOnlineService
|
|
{
|
|
const MIN_PINJAMAN = 1.00;
|
|
const MAX_PINJAMAN = 50000.00;
|
|
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;
|
|
|
|
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'];
|
|
|
|
if (!Feature::enabled($kodcaw, Feature::PEMBIAYAAN_ONLINE)) {
|
|
return Feature::denied('Pembiayaan online tidak diaktifkan untuk cawangan ini');
|
|
}
|
|
|
|
$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 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}
|
|
*/
|
|
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 / VERIFIED_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($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($input['borang_data'])) {
|
|
$saved = $this->storeBuktiFile([
|
|
'data' => $input['borang_data'],
|
|
'filename' => $input['borang_filename'] ?? null,
|
|
'type' => $input['borang_type'] ?? null,
|
|
]);
|
|
if (!$saved['ok']) {
|
|
$saved['payload']['message'] = 'Borang pengesahan diperlukan: fail, nama fail dan jenis fail.';
|
|
return $saved;
|
|
}
|
|
$fields['borang_file_id'] = $saved['payload']['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;
|
|
|
|
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);
|
|
|
|
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'],
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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}
|
|
*/
|
|
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();
|
|
|
|
// Fetch customer phone numbers in one query for the batch approval form.
|
|
$identityNumbers = $lines->pluck('nokp')->filter()->unique()->values()->all();
|
|
$phones = [];
|
|
if ($identityNumbers) {
|
|
$customers = Customer::on('mysql7')->select('kpbaru', 'kplama', 'telefon')
|
|
->where(function ($query) use ($identityNumbers) {
|
|
$query->whereIn('kpbaru', $identityNumbers)->orWhereIn('kplama', $identityNumbers);
|
|
})->get();
|
|
foreach ($customers as $customer) {
|
|
foreach ([$customer->kpbaru, $customer->kplama] as $identity) {
|
|
if ($identity !== null && $identity !== '') {
|
|
$phones[(string) $identity] = $customer->telefon;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
foreach ($lines as $line) {
|
|
$line->setAttribute('telefon', $phones[(string) $line->nokp] ?? null);
|
|
}
|
|
|
|
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 {
|
|
$result = 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_SUBMITTABLE_STATUSES, true)) {
|
|
return [
|
|
'ok' => false,
|
|
'status' => 422,
|
|
'payload' => [
|
|
'message' => 'Rekod belum disemak PC. Sahkan satu-satu sebelum hantar ke Operasi.',
|
|
'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,
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
list($batch, $appended) = $this->findOrCreateSlotBatch(
|
|
$mysql,
|
|
$kodcaw,
|
|
$tarikh,
|
|
$slot,
|
|
$pcBy,
|
|
$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,
|
|
]);
|
|
}
|
|
|
|
if ($appended) {
|
|
BatchPembiayaanOnline::on($mysql)->where('id', $batch->id)->update([
|
|
'submitted_at' => $now,
|
|
]);
|
|
}
|
|
|
|
$batch = $this->refreshBatchStatus($mysql, $batch->id);
|
|
|
|
$freshLines = PembiayaanOnline::on($mysql)
|
|
->where('batch_id', $batch->id)
|
|
->orderBy('id', 'asc')
|
|
->get();
|
|
|
|
return [
|
|
'ok' => true,
|
|
'status' => $appended ? 200 : 201,
|
|
'payload' => [
|
|
'batch' => $batch,
|
|
'lines' => $freshLines,
|
|
'slot_info' => $slotInfo,
|
|
'appended' => $appended,
|
|
],
|
|
];
|
|
});
|
|
} catch (\Throwable $e) {
|
|
Log::error('PembiayaanOnline submitBatch failed: ' . $e->getMessage(), [
|
|
'kodcaw' => $kodcaw,
|
|
]);
|
|
return [
|
|
'ok' => false,
|
|
'status' => 500,
|
|
'payload' => ['message' => 'Gagal hantar batch pembiayaan online'],
|
|
];
|
|
}
|
|
|
|
if (!empty($result['ok'])) {
|
|
$this->notifyOperasiBatchSubmitted($kodcaw, $ids, $pcBy, $result['payload']);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Inbox for all operasi staff after PC submit. Failures must not block the batch.
|
|
*
|
|
* @param string $kodcaw
|
|
* @param array $ids
|
|
* @param string $pcBy
|
|
* @param array $payload
|
|
* @return void
|
|
*/
|
|
protected function notifyOperasiBatchSubmitted($kodcaw, array $ids, $pcBy, array $payload)
|
|
{
|
|
$batch = isset($payload['batch']) ? $payload['batch'] : null;
|
|
if (!$batch) {
|
|
return;
|
|
}
|
|
|
|
$batchId = is_array($batch) ? ($batch['id'] ?? null) : $batch->id;
|
|
$batchNo = is_array($batch) ? ($batch['batch_no'] ?? '') : $batch->batch_no;
|
|
$slot = is_array($batch) ? ($batch['slot'] ?? '') : $batch->slot;
|
|
$bilangan = is_array($batch) ? ($batch['bilangan'] ?? 0) : $batch->bilangan;
|
|
$jumlah = is_array($batch) ? ($batch['jumlah_keseluruhan'] ?? 0) : $batch->jumlah_keseluruhan;
|
|
|
|
if (!$batchId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
app(StaffNotificationClient::class)->notify([
|
|
'type' => 'pembiayaan_online.batch_operasi',
|
|
'cawangan' => $kodcaw,
|
|
'audience' => ['role' => 'operasi'],
|
|
'data' => [
|
|
'batch_id' => $batchId,
|
|
'batch_no' => $batchNo,
|
|
'slot' => $slot,
|
|
'appended' => !empty($payload['appended']),
|
|
'submitted_count' => count($ids),
|
|
'bilangan' => $bilangan,
|
|
'jumlah_keseluruhan' => (float) $jumlah,
|
|
'pc_submitted_by' => $pcBy,
|
|
'cawangan_nama' => $this->cawanganNama($kodcaw),
|
|
],
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Operasi batch notification failed: '.$e->getMessage(), [
|
|
'kodcaw' => $kodcaw,
|
|
'batch_id' => $batchId,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $kodcaw
|
|
* @return string
|
|
*/
|
|
protected function cawanganNama($kodcaw)
|
|
{
|
|
$cawangan = Cawangan::on('mysql7')->where('kodcaw', '=', $kodcaw)->first();
|
|
if (!$cawangan) {
|
|
return $kodcaw;
|
|
}
|
|
|
|
$nama = trim((string) $cawangan->nama_cawangan);
|
|
|
|
return $nama !== '' ? $nama : $kodcaw;
|
|
}
|
|
|
|
/**
|
|
* Inbox for PC at this branch. Failures must not block the operasi action.
|
|
*
|
|
* @param string $kodcaw
|
|
* @param string $type
|
|
* @param array $data
|
|
* @return void
|
|
*/
|
|
protected function notifyPcPembiayaanOnline($kodcaw, $type, array $data)
|
|
{
|
|
try {
|
|
app(StaffNotificationClient::class)->notify([
|
|
'type' => $type,
|
|
'cawangan' => $kodcaw,
|
|
'audience' => ['role' => 'pc'],
|
|
'data' => array_merge($data, [
|
|
'cawangan_nama' => $this->cawanganNama($kodcaw),
|
|
]),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('PC pembiayaan-online notification failed: '.$e->getMessage(), [
|
|
'kodcaw' => $kodcaw,
|
|
'type' => $type,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param mixed $line
|
|
* @return array
|
|
*/
|
|
protected function lineNotifyFields($line)
|
|
{
|
|
if (!$line) {
|
|
return [];
|
|
}
|
|
|
|
$isArr = is_array($line);
|
|
|
|
return [
|
|
'norujukan' => $isArr ? ($line['norujukan'] ?? '') : $line->norujukan,
|
|
'nama_pelanggan' => $isArr ? ($line['nama_pelanggan'] ?? '') : $line->nama_pelanggan,
|
|
'pinjaman' => $isArr ? ($line['pinjaman'] ?? 0) : $line->pinjaman,
|
|
'ops_by' => $isArr ? ($line['ops_by'] ?? '') : $line->ops_by,
|
|
'reject_reason' => $isArr ? ($line['reject_reason'] ?? '') : $line->reject_reason,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @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 {
|
|
$result = 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,
|
|
'pc_verified_by' => null,
|
|
'pc_verified_at' => 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'],
|
|
];
|
|
}
|
|
|
|
if (!empty($result['ok'])) {
|
|
$this->notifyPcPembiayaanOnline($kodcaw, 'pembiayaan_online.rejected_pc', array_merge(
|
|
$this->lineNotifyFields(isset($result['payload']['pembiayaan_online']) ? $result['payload']['pembiayaan_online'] : null),
|
|
[
|
|
'ops_by' => $opsBy,
|
|
'reject_reason' => $reason,
|
|
]
|
|
));
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @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 {
|
|
$result = 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'],
|
|
];
|
|
}
|
|
|
|
if (!empty($result['ok'])) {
|
|
$this->notifyPcPembiayaanOnline($kodcaw, 'pembiayaan_online.paid_pc', array_merge(
|
|
$this->lineNotifyFields(isset($result['payload']['pembiayaan_online']) ? $result['payload']['pembiayaan_online'] : null),
|
|
[
|
|
'paid_count' => 1,
|
|
'ops_by' => $opsBy,
|
|
]
|
|
));
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
$result = 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'],
|
|
];
|
|
}
|
|
|
|
if (!empty($result['ok'])) {
|
|
$batch = isset($result['payload']['batch']) ? $result['payload']['batch'] : null;
|
|
$this->notifyPcPembiayaanOnline($kodcaw, 'pembiayaan_online.paid_pc', [
|
|
'paid_count' => isset($result['payload']['dibayar']) ? (int) $result['payload']['dibayar'] : 0,
|
|
'batch_id' => $batch ? (is_array($batch) ? ($batch['id'] ?? null) : $batch->id) : $batchId,
|
|
'batch_no' => $batch ? (is_array($batch) ? ($batch['batch_no'] ?? '') : $batch->batch_no) : '',
|
|
'jumlah_keseluruhan' => $batch ? (is_array($batch) ? ($batch['jumlah_keseluruhan'] ?? 0) : $batch->jumlah_keseluruhan) : 0,
|
|
'ops_by' => $opsBy,
|
|
]);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get attached signed online-financing confirmation form (borang_file_id).
|
|
*
|
|
* @return array{ok:bool,status:int,payload:mixed}
|
|
*/
|
|
public function getBorang($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->borang_file_id)) {
|
|
return [
|
|
'ok' => false,
|
|
'status' => 404,
|
|
'payload' => ['message' => 'Borang pengesahan belum dimuat naik'],
|
|
];
|
|
}
|
|
|
|
$file = ARFiles::on('mysql7')->where('id', $line->borang_file_id)->first();
|
|
if (!$file) {
|
|
return [
|
|
'ok' => false,
|
|
'status' => 404,
|
|
'payload' => ['message' => 'Fail borang pengesahan 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.
|
|
*
|
|
* @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),
|
|
* then queues WhatsApp Cloud template to each customer (or ADMIN_PHONE_NUMBER off-prod).
|
|
*
|
|
* @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 {
|
|
$result = 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'],
|
|
];
|
|
}
|
|
|
|
if (!empty($result['ok'])) {
|
|
$this->queuePaidWhatsapp($kodcaw, $result['payload']['data'] ?? []);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Dispatch Cloud API jobs after tally commit. Failures are logged; tally stays PAID/blast_sent.
|
|
*
|
|
* @param \Illuminate\Support\Collection|array $lines
|
|
*/
|
|
protected function queuePaidWhatsapp($kodcaw, $lines)
|
|
{
|
|
$client = new WhatsappCloudClient();
|
|
if (!$client->isConfigured()) {
|
|
Log::info('WhatsApp Cloud not configured; tally saved without send', ['kodcaw' => $kodcaw]);
|
|
return;
|
|
}
|
|
|
|
$cawangan = Cawangan::on('mysql7')->where('kodcaw', $kodcaw)->first();
|
|
$cawName = $cawangan ? $cawangan->nama_cawangan : $kodcaw;
|
|
|
|
foreach ($lines as $line) {
|
|
try {
|
|
dispatch(new WhatsappPembiayaanOnlinePaid([
|
|
'kodcaw' => $kodcaw,
|
|
'caw_name' => $cawName,
|
|
'nokp' => $line->nokp ?? null,
|
|
'norujukan' => $line->norujukan ?? null,
|
|
'pinjaman' => $line->pinjaman ?? null,
|
|
'nama_bank' => $line->nama_bank ?? null,
|
|
'noakaun' => $line->noakaun ?? null,
|
|
'tarikh_bayaran' => $line->tarikh_bayaran ?? null,
|
|
'no_rujukan_bayaran' => $line->no_rujukan_bayaran ?? null,
|
|
]));
|
|
} catch (\Throwable $e) {
|
|
Log::error('queuePaidWhatsapp dispatch failed: ' . $e->getMessage(), [
|
|
'kodcaw' => $kodcaw,
|
|
'id' => $line->id ?? null,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 slotJadual()
|
|
{
|
|
return [
|
|
[
|
|
'slot' => 'BATCH1',
|
|
'label' => 'Batch 1',
|
|
'mula' => '00:00',
|
|
'cutoff' => '11:00',
|
|
'proses' => '12:30',
|
|
],
|
|
[
|
|
'slot' => 'BATCH2',
|
|
'label' => 'Batch 2',
|
|
'mula' => '11:00',
|
|
'cutoff' => '15:00',
|
|
'proses' => '16:30',
|
|
],
|
|
];
|
|
}
|
|
|
|
public function suggestSlot()
|
|
{
|
|
$now = Carbon::now('Asia/Kuala_Lumpur');
|
|
$mins = ((int) $now->format('G')) * 60 + (int) $now->format('i');
|
|
$jadual = $this->slotJadual();
|
|
$batch1 = $jadual[0];
|
|
$batch2 = $jadual[1];
|
|
$cutoff1 = $this->hmToMinutes($batch1['cutoff']);
|
|
$cutoff2 = $this->hmToMinutes($batch2['cutoff']);
|
|
|
|
if ($mins < $cutoff1) {
|
|
return $this->slotPayload($batch1, false, $cutoff1 - $mins, 'Dalam waktu Batch 1', $now, $jadual);
|
|
}
|
|
|
|
if ($mins < $cutoff2) {
|
|
return $this->slotPayload($batch2, false, $cutoff2 - $mins, 'Dalam waktu Batch 2', $now, $jadual);
|
|
}
|
|
|
|
$lewat = $this->slotPayload(
|
|
$batch2,
|
|
true,
|
|
0,
|
|
'Selepas cutoff Batch 2 — masih boleh hantar; Operasi mungkin proses esok',
|
|
$now,
|
|
$jadual
|
|
);
|
|
$lewat['proses'] = $batch2['proses'] . ' (hari berikutnya jika sudah lewat)';
|
|
|
|
return $lewat;
|
|
}
|
|
|
|
protected function slotPayload(array $batch, $lewat, $bakiMinit, $nota, Carbon $now, array $jadual)
|
|
{
|
|
$parts = explode(':', (string) $batch['cutoff']);
|
|
$cutoffAt = $now->copy()->setTime(
|
|
isset($parts[0]) ? (int) $parts[0] : 0,
|
|
isset($parts[1]) ? (int) $parts[1] : 0,
|
|
0
|
|
);
|
|
|
|
return [
|
|
'slot' => $batch['slot'],
|
|
'cutoff' => $batch['cutoff'],
|
|
'cutoff_at' => $cutoffAt->toIso8601String(),
|
|
'server_now' => $now->toIso8601String(),
|
|
'proses' => $batch['proses'],
|
|
'lewat' => (bool) $lewat,
|
|
'baki_minit' => (int) $bakiMinit,
|
|
'nota' => $nota,
|
|
'jadual' => $jadual,
|
|
];
|
|
}
|
|
|
|
protected function hmToMinutes($hm)
|
|
{
|
|
$parts = explode(':', (string) $hm);
|
|
$h = isset($parts[0]) ? (int) $parts[0] : 0;
|
|
$m = isset($parts[1]) ? (int) $parts[1] : 0;
|
|
|
|
return ($h * 60) + $m;
|
|
}
|
|
|
|
/**
|
|
* One batch per branch / day / slot (B1-001 or B2-001). Later submits append.
|
|
*
|
|
* @return array{0:\App\BatchPembiayaanOnline,1:bool} [batch, appended]
|
|
*/
|
|
protected function findOrCreateSlotBatch($mysql, $kodcaw, $tarikh, $slot, $pcBy, $now)
|
|
{
|
|
$batch = BatchPembiayaanOnline::on($mysql)
|
|
->where('kodcaw', $kodcaw)
|
|
->where('tarikh', $tarikh)
|
|
->where('slot', $slot)
|
|
->orderBy('id', 'asc')
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($batch) {
|
|
return [$batch, true];
|
|
}
|
|
|
|
$batch = BatchPembiayaanOnline::on($mysql)->create([
|
|
'batch_no' => $this->slotBatchNo($kodcaw, $tarikh, $slot),
|
|
'kodcaw' => $kodcaw,
|
|
'tarikh' => $tarikh,
|
|
'slot' => $slot,
|
|
'bilangan' => 0,
|
|
'jumlah_keseluruhan' => 0,
|
|
'status' => 'SUBMITTED',
|
|
'pc_submitted_by' => $pcBy,
|
|
'submitted_at' => $now,
|
|
]);
|
|
|
|
return [$batch, false];
|
|
}
|
|
|
|
protected function slotBatchNo($kodcaw, $tarikh, $slot)
|
|
{
|
|
$slotTag = $slot === 'BATCH1' ? 'B1' : 'B2';
|
|
$ymd = Carbon::parse($tarikh)->format('Ymd');
|
|
|
|
return strtoupper($kodcaw) . '-PO-' . $ymd . '-' . $slotTag . '-001';
|
|
}
|
|
}
|