Files
kaunter/app/Services/NotificationService.php
ismailmasseran c437a3f504
Build Docker Image / build-frontend (push) Successful in 53s
Feature/whatsapp blast (#24)
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #24
2026-09-24 11:28:05 +08:00

569 lines
19 KiB
PHP

<?php
namespace App\Services;
use App\StaffNotification;
use App\User;
use Illuminate\Support\Facades\Log;
class NotificationService
{
const TYPE_PEMBIAYAAN_PENDING_PC = 'pembiayaan_online.pending_pc';
const TYPE_MODAL_PENDING_OPERASI = 'modal_online.pending_operasi';
const TYPE_PEMBIAYAAN_BATCH_OPERASI = 'pembiayaan_online.batch_operasi';
const TYPE_PEMBIAYAAN_REJECTED_PC = 'pembiayaan_online.rejected_pc';
const TYPE_PEMBIAYAAN_PAID_PC = 'pembiayaan_online.paid_pc';
const TYPE_GADAI_KELULUSAN = 'gadai.kelulusan';
const TYPE_ANGGOTA_PENDING_OPERASI = 'anggota.pending_operasi';
const TYPE_ALIRAN_TUNAI_PENDING = 'aliran_tunai.pending_khazanah';
const TYPE_ALIRAN_TUNAI_DECIDED = 'aliran_tunai.decided_teller';
const GADAI_KELULUSAN_MIN = 10000;
/**
* Create one inbox row per staff matching role + branch.
*
* @param string $role
* @param string $kodcaw
* @param array $payload
* @return \Illuminate\Support\Collection
*/
public function notifyRoleAtCawangan($role, $kodcaw, array $payload)
{
$users = User::query()
->where('roles', $role)
->where('cawangan', $kodcaw)
->get();
$created = collect();
foreach ($users as $user) {
$created->push($this->notifyUser($user, $kodcaw, $role, $payload));
}
return $created;
}
/**
* Create one inbox row per staff matching role (any branch). For HQ like operasi.
*
* @param string $role
* @param array $payload
* @return \Illuminate\Support\Collection
*/
public function notifyRole($role, array $payload)
{
$kodcaw = isset($payload['cawangan']) ? $payload['cawangan'] : null;
$users = User::query()->where('roles', $role)->get();
$created = collect();
foreach ($users as $user) {
$created->push($this->notifyUser($user, $kodcaw, $role, $payload));
}
return $created;
}
/**
* Entry point for api_arrahn (and local callers). Builds title/url from type when omitted.
*
* @param array $input
* @return \Illuminate\Support\Collection
*/
public function dispatch(array $input)
{
$type = isset($input['type']) ? $input['type'] : '';
$kodcaw = isset($input['cawangan']) ? $input['cawangan'] : null;
$audience = isset($input['audience']) && is_array($input['audience']) ? $input['audience'] : [];
$role = isset($audience['role']) ? $audience['role'] : null;
$data = isset($input['data']) && is_array($input['data']) ? $input['data'] : [];
if ($type === self::TYPE_ALIRAN_TUNAI_PENDING && $kodcaw) {
return $this->notifyAliranTunaiPending($kodcaw, $data);
}
$payload = $this->payloadForType($type, $kodcaw, $data);
if (!empty($input['title'])) {
$payload['title'] = $input['title'];
}
if (array_key_exists('body', $input) && $input['body'] !== null && $input['body'] !== '') {
$payload['body'] = $input['body'];
}
if (!empty($input['url'])) {
$payload['url'] = $input['url'];
}
if ($role === 'operasi') {
return $this->notifyRole('operasi', $payload);
}
if ($role && $kodcaw) {
return $this->notifyRoleAtCawangan($role, $kodcaw, $payload);
}
return collect();
}
/**
* Gadai >= RM10,000: inbox for PC/PPC at the branch and all operasi.
*
* @param string $kodcaw
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyGadaiKelulusan($kodcaw, array $data)
{
$pinjaman = isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0;
if ($pinjaman < self::GADAI_KELULUSAN_MIN) {
return collect();
}
$payload = $this->payloadForType(self::TYPE_GADAI_KELULUSAN, $kodcaw, $data);
$created = collect();
foreach (['pc', 'ppc'] as $role) {
$created = $created->merge(
$this->notifyRoleAtCawangan($role, $kodcaw, array_merge($payload, [
'url' => '/kelulusan',
]))
);
}
return $created->merge(
$this->notifyRole('operasi', array_merge($payload, [
'url' => '/operasi/kelulusan',
]))
);
}
/**
* Teller pendahuluan/serahan: inbox for PC/PPC at the branch.
*
* @param string $kodcaw
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyAliranTunaiPending($kodcaw, array $data)
{
$payload = $this->payloadForType(self::TYPE_ALIRAN_TUNAI_PENDING, $kodcaw, $data);
$created = collect();
foreach (['pc', 'ppc'] as $role) {
$created = $created->merge($this->notifyRoleAtCawangan($role, $kodcaw, $payload));
}
return $created;
}
/**
* Khazanah lulus/batal: inbox for the teller who requested cash.
*
* @param string $kodcaw
* @param string $tellerName
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyAliranTunaiDecided($kodcaw, $tellerName, array $data)
{
$tellerName = trim((string) $tellerName);
if ($tellerName === '') {
return collect();
}
$payload = $this->payloadForType(self::TYPE_ALIRAN_TUNAI_DECIDED, $kodcaw, $data);
$user = User::query()
->where('name', $tellerName)
->where('cawangan', $kodcaw)
->first();
if (!$user) {
$user = User::query()->where('name', $tellerName)->first();
}
if (!$user) {
return collect();
}
return collect([$this->notifyUser($user, $kodcaw, $user->roles, $payload)]);
}
/**
* @param string $type
* @param string|null $kodcaw
* @param array $data
* @return array
*/
protected function payloadForType($type, $kodcaw, array $data)
{
if ($type === self::TYPE_MODAL_PENDING_OPERASI) {
$jenis = isset($data['jenis']) ? strtoupper((string) $data['jenis']) : 'HAD';
$label = $jenis === 'TOPUP' ? 'tambah modal' : 'had modal';
$nama = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$amaun = number_format(isset($data['amaun']) ? (float) $data['amaun'] : 0, 2);
$oleh = isset($data['dimohon_oleh']) ? trim((string) $data['dimohon_oleh']) : '';
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Permohonan modal online',
'body' => sprintf(
'%s mohon %s RM %s%s',
$nama,
$label,
$amaun,
$oleh !== '' ? ' — '.$oleh : ''
),
'url' => '/pembiayaan-online/operasi/topup',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_BATCH_OPERASI) {
$nama = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$batchNo = !empty($data['batch_no']) ? $data['batch_no'] : 'batch';
$batchId = isset($data['batch_id']) ? $data['batch_id'] : null;
$submitted = isset($data['submitted_count']) ? (int) $data['submitted_count'] : 0;
$jumlah = number_format(isset($data['jumlah_keseluruhan']) ? (float) $data['jumlah_keseluruhan'] : 0, 2);
$oleh = isset($data['pc_submitted_by']) ? trim((string) $data['pc_submitted_by']) : '';
$appended = !empty($data['appended']);
if ($appended) {
$body = sprintf(
'%s tambah %d rekod ke %s (RM %s)%s',
$nama,
$submitted,
$batchNo,
$jumlah,
$oleh !== '' ? ' — '.$oleh : ''
);
} else {
$body = sprintf(
'%s hantar %s — %d rekod (RM %s)%s',
$nama,
$batchNo,
$submitted,
$jumlah,
$oleh !== '' ? ' — '.$oleh : ''
);
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Batch pembiayaan online',
'body' => $body,
'url' => ($kodcaw && $batchId)
? '/pembiayaan-online/operasi/'.$kodcaw.'/batches/'.$batchId
: '/pembiayaan-online/operasi',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_REJECTED_PC) {
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : 'rekod';
$reason = isset($data['reject_reason']) ? trim((string) $data['reject_reason']) : '';
$amaun = number_format(isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0, 2);
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Pembiayaan online ditolak',
'body' => sprintf(
'%s (RM %s) ditolak%s',
$norujukan,
$amaun,
$reason !== '' ? ' — '.$reason : ''
),
'url' => '/pembiayaan-online/pc',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_PAID_PC) {
$paidCount = isset($data['paid_count']) ? (int) $data['paid_count'] : 1;
$batchNo = !empty($data['batch_no']) ? $data['batch_no'] : '';
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : '';
$nama = isset($data['nama_pelanggan']) ? trim((string) $data['nama_pelanggan']) : '';
$amaun = number_format(
isset($data['pinjaman'])
? (float) $data['pinjaman']
: (isset($data['jumlah_keseluruhan']) ? (float) $data['jumlah_keseluruhan'] : 0),
2
);
if ($paidCount > 1 || ($batchNo !== '' && $norujukan === '')) {
$body = sprintf(
'%d rekod telah dibayar%s (RM %s)',
$paidCount > 0 ? $paidCount : 0,
$batchNo !== '' ? ' dalam '.$batchNo : '',
$amaun
);
} else {
$body = sprintf(
'%s telah dibayar (RM %s)%s',
$norujukan !== '' ? $norujukan : 'Rekod',
$amaun,
$nama !== '' ? ' — '.$nama : ''
);
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Pembiayaan online telah dibayar',
'body' => $body,
'url' => '/pembiayaan-online/pc/paid',
'data' => $data,
];
}
if ($type === self::TYPE_ALIRAN_TUNAI_PENDING) {
$jenis = $this->aliranTunaiJenis($data);
$amaun = $this->aliranTunaiAmaun($data, $jenis);
$teller = isset($data['kodlaluan']) ? trim((string) $data['kodlaluan']) : '';
$label = $jenis === 'serahan' ? 'serahan' : 'pendahuluan';
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => $jenis === 'serahan' ? 'Permohonan serahan modal' : 'Permohonan pendahuluan',
'body' => sprintf(
'%s mohon %s RM %s',
$teller !== '' ? $teller : 'Teller',
$label,
number_format($amaun, 2)
),
'url' => $jenis === 'serahan' ? '/serahan' : '/pendahuluan',
'data' => $data,
];
}
if ($type === self::TYPE_ALIRAN_TUNAI_DECIDED) {
$jenis = $this->aliranTunaiJenis($data);
$amaun = $this->aliranTunaiAmaun($data, $jenis);
$lulus = isset($data['pengesahan']) && strcasecmp((string) $data['pengesahan'], 'Lulus') === 0;
$label = 'Pendahuluan';
if ($jenis === 'serahan') {
$label = 'Serahan';
} elseif ($jenis === 'all') {
$label = 'Aliran tunai';
}
if ($lulus) {
$title = $label.' diluluskan';
$body = sprintf('%s RM %s telah diluluskan', $label, number_format($amaun, 2));
} else {
$title = $label.' tidak diluluskan';
$body = sprintf('Permohonan %s tidak diluluskan', strtolower($label));
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => $title,
'body' => $body,
'url' => '/aliran_tunai',
'data' => $data,
];
}
if ($type === self::TYPE_ANGGOTA_PENDING_OPERASI) {
$nokp = !empty($data['nokp']) ? $data['nokp'] : '';
$nama = isset($data['nama']) ? trim((string) $data['nama']) : '';
$teller = isset($data['teller']) ? trim((string) $data['teller']) : '';
$komen = isset($data['komen']) ? trim((string) $data['komen']) : '';
$namaCaw = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$who = $nama !== '' ? $nama : 'pelanggan';
if ($nokp !== '') {
$who .= ' ('.$nokp.')';
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Permohonan assign anggota',
'body' => sprintf(
'%s — %s%s%s',
$namaCaw,
$who,
$teller !== '' ? ' — '.$teller : '',
$komen !== '' ? ' — '.$komen : ''
),
'url' => '/operasi/kelulusan/anggota',
'data' => $data,
];
}
if ($type === self::TYPE_GADAI_KELULUSAN) {
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : 'gadai';
$amaun = number_format(isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0, 2);
$teller = isset($data['teller']) ? trim((string) $data['teller']) : '';
$namaCaw = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Kelulusan gadai melebihi RM 10,000',
'body' => sprintf(
'%s — %s (RM %s)%s',
$namaCaw,
$norujukan,
$amaun,
$teller !== '' ? ' — '.$teller : ''
),
'url' => '/kelulusan',
'data' => $data,
];
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => isset($data['title']) ? $data['title'] : $type,
'body' => isset($data['body']) ? $data['body'] : null,
'url' => isset($data['url']) ? $data['url'] : null,
'data' => $data,
];
}
/**
* @param array $data
* @return string pendahuluan|serahan|all
*/
protected function aliranTunaiJenis(array $data)
{
if (!empty($data['jenis'])) {
$jenis = strtolower(trim((string) $data['jenis']));
if ($jenis === 'serahan' || $jenis === 'pendahuluan' || $jenis === 'all') {
return $jenis;
}
}
$modal = isset($data['modal']) ? (float) $data['modal'] : 0;
$serahan = isset($data['serahan']) ? (float) $data['serahan'] : 0;
if ($serahan > 0 && $modal <= 0) {
return 'serahan';
}
return 'pendahuluan';
}
/**
* @param array $data
* @param string $jenis
* @return float
*/
protected function aliranTunaiAmaun(array $data, $jenis)
{
if (isset($data['amaun']) && is_numeric($data['amaun'])) {
return (float) $data['amaun'];
}
if ($jenis === 'serahan') {
return isset($data['serahan']) ? (float) $data['serahan'] : 0;
}
return isset($data['modal']) ? (float) $data['modal'] : 0;
}
/**
* Create one inbox row for a specific user.
*
* @param \App\User $user
* @param string|null $kodcaw
* @param string|null $role
* @param array $payload
* @return \App\StaffNotification
*/
public function notifyUser(User $user, $kodcaw, $role, array $payload)
{
return StaffNotification::create([
'user_id' => $user->id,
'cawangan' => $kodcaw,
'role' => $role ?: $user->roles,
'type' => $payload['type'],
'title' => $payload['title'],
'body' => isset($payload['body']) ? $payload['body'] : null,
'url' => isset($payload['url']) ? $payload['url'] : null,
'data' => isset($payload['data']) ? $payload['data'] : null,
]);
}
/**
* @param \App\User $user
* @param int $limit
* @return \Illuminate\Support\Collection
*/
public function inboxFor(User $user, $limit = 20)
{
return StaffNotification::query()
->where('user_id', $user->id)
->orderByDesc('id')
->limit($limit)
->get();
}
/**
* @param \App\User $user
* @return int
*/
public function unreadCount(User $user)
{
return StaffNotification::query()
->where('user_id', $user->id)
->unread()
->count();
}
/**
* @param \App\User $user
* @param int $id
* @return \App\StaffNotification|null
*/
public function markAsRead(User $user, $id)
{
$notification = StaffNotification::query()
->where('user_id', $user->id)
->where('id', $id)
->first();
if (!$notification) {
return null;
}
if ($notification->read_at === null) {
$notification->read_at = now();
$notification->save();
}
return $notification;
}
/**
* @param \App\User $user
* @return int
*/
public function markAllAsRead(User $user)
{
return StaffNotification::query()
->where('user_id', $user->id)
->unread()
->update(['read_at' => now()]);
}
/**
* @param \App\User $user
* @param int $limit
* @return array
*/
public function inboxPayload(User $user, $limit = 20)
{
return [
'unread_count' => $this->unreadCount($user),
'notifications' => $this->inboxFor($user, $limit)->map(function (StaffNotification $notification) {
return $notification->toInboxArray();
})->values(),
];
}
}