diff --git a/app/Controllers/MasterController.php b/app/Controllers/MasterController.php index 32e868e3..9d33863e 100755 --- a/app/Controllers/MasterController.php +++ b/app/Controllers/MasterController.php @@ -4011,204 +4011,10 @@ class MasterController extends AdminController */ public function cronCdLowBalanceAlert() { - log_message('error', 'cronCdLowBalanceAlert: started'); + $response = \App\Helpers\CdLowBalanceAlertHelper::runCron(); + $statusCode = ($response['status'] ?? false) ? 200 : 500; - try { - $cdMasters = $this->CDMasterModel->getCdMastersWithLowAlertThreshold(); - log_message('error', 'cronCdLowBalanceAlert: CD masters with alert threshold count = ' . count($cdMasters)); - - $lowBalanceAccounts = []; - - foreach ($cdMasters as $cdMaster) { - $cdAcPk = (int) ($cdMaster['id'] ?? 0); - $threshold = (float) ($cdMaster['cd_balance_low_alert_amount'] ?? 0); - - if ($cdAcPk <= 0 || $threshold <= 0) { - continue; - } - - $lastDeposit = $this->clientDepositModel - ->select('balance') - ->where('cd_ac_pk', $cdAcPk) - ->where('is_active', 1) - ->orderBy('id', 'DESC') - ->first(); - - if (! empty($lastDeposit)) { - $currentBalance = (float) ($lastDeposit['balance'] ?? 0); - } else { - $currentBalance = (float) ($cdMaster['opening_bal'] ?? 0); - } - - if ($currentBalance >= $threshold) { - continue; - } - - $lowBalanceAccounts[] = array_merge($cdMaster, [ - 'current_balance' => $currentBalance, - 'alert_amount' => $threshold, - ]); - } - - if (empty($lowBalanceAccounts)) { - $response = [ - 'status' => true, - 'message' => 'No CD accounts below the configured low-balance alert amount.', - 'low_balance_count' => 0, - 'account_manager_mails_sent' => 0, - 'data' => [], - 'errors' => [], - ]; - log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); - return $this->respond($response, 200); - } - - $accountManagerMailsSent = 0; - $mailErrors = []; - $data = []; - - foreach ($lowBalanceAccounts as $account) { - $result = $this->sendCdLowBalanceAccountManagerAlert($account); - $accountManagerMailsSent += (int) ($result['sent'] ?? 0); - if (! empty($result['errors'])) { - $mailErrors = array_merge($mailErrors, $result['errors']); - } - if (! empty($result['data'])) { - $data = array_merge($data, $result['data']); - } - } - - $response = [ - 'status' => true, - 'message' => 'CD low balance alert cron completed.', - 'low_balance_count' => count($lowBalanceAccounts), - 'account_manager_mails_sent' => $accountManagerMailsSent, - 'data' => $data, - 'errors' => $mailErrors, - ]; - - log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); - return $this->respond($response, 200); - } catch (\Throwable $e) { - $response = [ - 'status' => false, - 'message' => 'CD low balance alert cron failed.', - 'low_balance_count' => 0, - 'account_manager_mails_sent' => 0, - 'data' => [], - 'errors' => [$e->getMessage()], - ]; - log_message('error', 'cronCdLowBalanceAlert Exception: ' . $e->getMessage()); - log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); - return $this->respond($response, 500); - } - } - - private function sendCdLowBalanceAccountManagerAlert(array $account): array - { - $clientId = (int) ($account['client_id'] ?? 0); - $cdAcPk = (int) ($account['id'] ?? 0); - $cdAcNo = (string) ($account['cd_ac_no'] ?? ''); - - if ($clientId <= 0) { - log_message('error', 'cronCdLowBalanceAlert ACM mail: skipped, invalid client_id for cd_ac_pk=' . $cdAcPk); - return ['sent' => 0, 'errors' => [], 'data' => []]; - } - - $recipients = $this->getAccountManagerRecipients($clientId); - if (empty($recipients)) { - $error = "Client {$clientId}: No account manager found."; - log_message('error', 'cronCdLowBalanceAlert ACM mail: ' . $error); - return ['sent' => 0, 'errors' => [$error], 'data' => []]; - } - - $sent = 0; - $errors = []; - $data = []; - $subject = 'CD Low Balance Alert - ' . ($account['client_name'] ?? 'Client'); - - foreach ($recipients as $recipient) { - $acmEmail = (string) ($recipient['email'] ?? ''); - $acmName = (string) ($recipient['name'] ?? 'Account Manager'); - $message = $this->buildCdLowBalanceAccountManagerMailContent($account, $acmName); - - $mailResponse = MailHelper::send_email([ - 'mail' => $acmEmail, - 'subject' => $subject, - 'message' => $message, - 'common' => [ - 'mail_type' => 'account_manager_cd_low_balance_alert', - 'client_id' => $clientId, - ], - ]); - - $decoded = is_string($mailResponse) ? json_decode($mailResponse, true) : $mailResponse; - $mailStatus = is_array($decoded) && ($decoded['status'] ?? '') === 'success' ? 'success' : 'failed'; - - $data[] = [ - 'client_id' => $clientId, - 'cd_ac_pk' => $cdAcPk, - 'cd_ac_no' => $cdAcNo, - 'acm_name' => $acmName, - 'acm_email' => $acmEmail, - 'mail_status' => $mailStatus, - ]; - - if ($mailStatus === 'success') { - $sent++; - } else { - $errors[] = "Client {$clientId}: Failed to send alert to {$acmEmail}."; - } - } - - return ['sent' => $sent, 'errors' => $errors, 'data' => $data]; - } - - /** - * @return array - */ - private function getAccountManagerRecipients(int $clientId): array - { - $rows = $this->insurerRMModel - ->select('user_profiles.first_name, user_profiles.email') - ->join('user_profiles', 'user_profiles.id = client_rm.user_id') - ->where('client_rm.client_id', $clientId) - ->where('client_rm.level', 3) - ->where('client_rm.is_active', 1) - ->where('user_profiles.is_active', 1) - ->findAll(); - - $recipients = []; - $seenEmails = []; - - foreach ($rows as $row) { - $email = strtolower(trim((string) ($row['email'] ?? ''))); - if ($email === '' || isset($seenEmails[$email])) { - continue; - } - - $seenEmails[$email] = true; - $recipients[] = [ - 'name' => trim((string) ($row['first_name'] ?? 'Account Manager')) ?: 'Account Manager', - 'email' => (string) $row['email'], - ]; - } - - return $recipients; - } - - private function buildCdLowBalanceAccountManagerMailContent(array $account, string $recipientName): string - { - return '

Dear ' . esc($recipientName) . ',

' - . '

The CD account balance for ' . esc((string) ($account['client_name'] ?? '')) . ' has fallen below the configured alert threshold.

' - . '' - . '' - . '' - . '' - . '' - . '' - . '
Insurer' . esc((string) ($account['insurer_name'] ?? '')) . '
Insurer Branch' . esc((string) ($account['insurer_branch_name'] ?? '')) . '
CD Account No' . esc((string) ($account['cd_ac_no'] ?? '')) . '
Current Balance' . esc(number_format((float) ($account['current_balance'] ?? 0), 2, '.', '')) . '
Alert Amount' . esc(number_format((float) ($account['alert_amount'] ?? 0), 2, '.', '')) . '
' - . '

Please review the CD account and take the required action.

'; + return $this->respond($response, $statusCode); } diff --git a/app/Helpers/CdLowBalanceAlertHelper.php b/app/Helpers/CdLowBalanceAlertHelper.php new file mode 100644 index 00000000..4a70d740 --- /dev/null +++ b/app/Helpers/CdLowBalanceAlertHelper.php @@ -0,0 +1,274 @@ + + */ + public static function runCron(): array + { + log_message('error', 'cronCdLowBalanceAlert: started'); + + try { + $cdMasterModel = new CDMasterModel(); + $cdMasters = $cdMasterModel->getCdMastersWithLowAlertThreshold(); + log_message('error', 'cronCdLowBalanceAlert: CD masters with alert threshold count = ' . count($cdMasters)); + + $depositModel = new ClientDepositModel(); + $lowBalanceAccounts = []; + + foreach ($cdMasters as $cdMaster) { + $cdAcPk = (int) ($cdMaster['id'] ?? 0); + $threshold = (float) ($cdMaster['cd_balance_low_alert_amount'] ?? 0); + + if ($cdAcPk <= 0 || $threshold <= 0) { + continue; + } + + $currentBalance = self::resolveCurrentBalance($depositModel, $cdAcPk, (float) ($cdMaster['opening_bal'] ?? 0)); + if ($currentBalance >= $threshold) { + continue; + } + + $lowBalanceAccounts[] = array_merge($cdMaster, [ + 'current_balance' => $currentBalance, + 'alert_amount' => $threshold, + ]); + } + + if (empty($lowBalanceAccounts)) { + $response = [ + 'status' => true, + 'message' => 'No CD accounts below the configured low-balance alert amount.', + 'low_balance_count' => 0, + 'account_manager_mails_sent' => 0, + 'data' => [], + 'errors' => [], + ]; + log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); + + return $response; + } + + $accountManagerMailsSent = 0; + $mailErrors = []; + $data = []; + + foreach ($lowBalanceAccounts as $account) { + $result = self::sendAccountManagerAlert($account); + $accountManagerMailsSent += (int) ($result['sent'] ?? 0); + if (! empty($result['errors'])) { + $mailErrors = array_merge($mailErrors, $result['errors']); + } + if (! empty($result['data'])) { + $data = array_merge($data, $result['data']); + } + } + + $response = [ + 'status' => true, + 'message' => 'CD low balance alert cron completed.', + 'low_balance_count' => count($lowBalanceAccounts), + 'account_manager_mails_sent' => $accountManagerMailsSent, + 'data' => $data, + 'errors' => $mailErrors, + ]; + log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); + + return $response; + } catch (\Throwable $e) { + $response = [ + 'status' => false, + 'message' => 'CD low balance alert cron failed.', + 'low_balance_count' => 0, + 'account_manager_mails_sent' => 0, + 'data' => [], + 'errors' => [$e->getMessage()], + ]; + log_message('error', 'cronCdLowBalanceAlert Exception: ' . $e->getMessage()); + log_message('error', 'cronCdLowBalanceAlert: ' . json_encode($response)); + + return $response; + } + } + + /** + * @return array|null + */ + private static function buildLowBalanceAccount(int $cdAcPk, float $currentBalance): ?array + { + $cdMasterModel = new CDMasterModel(); + $cdMaster = $cdMasterModel->getCdMasterWithLowAlertThresholdById($cdAcPk); + if ($cdMaster === null) { + return null; + } + + $threshold = (float) ($cdMaster['cd_balance_low_alert_amount'] ?? 0); + if ($threshold <= 0 || $currentBalance >= $threshold) { + return null; + } + + return array_merge($cdMaster, [ + 'current_balance' => $currentBalance, + 'alert_amount' => $threshold, + ]); + } + + private static function resolveCurrentBalance(ClientDepositModel $depositModel, int $cdAcPk, float $openingBalance): float + { + $lastDeposit = $depositModel + ->select('balance') + ->where('cd_ac_pk', $cdAcPk) + ->where('is_active', 1) + ->orderBy('id', 'DESC') + ->first(); + + if (! empty($lastDeposit)) { + return (float) ($lastDeposit['balance'] ?? 0); + } + + return $openingBalance; + } + + /** + * @param array $account + * @return array{sent: int, errors: array, data: array>} + */ + private static function sendAccountManagerAlert(array $account): array + { + $clientId = (int) ($account['client_id'] ?? 0); + $cdAcPk = (int) ($account['id'] ?? 0); + $cdAcNo = (string) ($account['cd_ac_no'] ?? ''); + + if ($clientId <= 0) { + log_message('error', 'cronCdLowBalanceAlert ACM mail: skipped, invalid client_id for cd_ac_pk=' . $cdAcPk); + + return ['sent' => 0, 'errors' => [], 'data' => []]; + } + + $recipients = self::getAccountManagerRecipients($clientId); + if (empty($recipients)) { + $error = "Client {$clientId}: No account manager found."; + log_message('error', 'cronCdLowBalanceAlert ACM mail: ' . $error); + + return ['sent' => 0, 'errors' => [$error], 'data' => []]; + } + + $sent = 0; + $errors = []; + $data = []; + $subject = 'CD Low Balance Alert - ' . ($account['client_name'] ?? 'Client'); + + foreach ($recipients as $recipient) { + $acmEmail = (string) ($recipient['email'] ?? ''); + $acmName = (string) ($recipient['name'] ?? 'Account Manager'); + $message = self::buildAccountManagerMailContent($account, $acmName); + + $mailResponse = MailHelper::send_email([ + 'mail' => $acmEmail, + 'subject' => $subject, + 'message' => $message, + 'common' => [ + 'mail_type' => 'account_manager_cd_low_balance_alert', + 'client_id' => $clientId, + ], + ]); + + $decoded = is_string($mailResponse) ? json_decode($mailResponse, true) : $mailResponse; + $mailStatus = is_array($decoded) && ($decoded['status'] ?? '') === 'success' ? 'success' : 'failed'; + + $data[] = [ + 'client_id' => $clientId, + 'cd_ac_pk' => $cdAcPk, + 'cd_ac_no' => $cdAcNo, + 'acm_name' => $acmName, + 'acm_email' => $acmEmail, + 'mail_status' => $mailStatus, + ]; + + if ($mailStatus === 'success') { + $sent++; + } else { + $errors[] = "Client {$clientId}: Failed to send alert to {$acmEmail}."; + } + } + + return ['sent' => $sent, 'errors' => $errors, 'data' => $data]; + } + + /** + * @return array + */ + private static function getAccountManagerRecipients(int $clientId): array + { + $rows = (new ClientRMModel()) + ->select('user_profiles.first_name, user_profiles.email') + ->join('user_profiles', 'user_profiles.id = client_rm.user_id') + ->where('client_rm.client_id', $clientId) + ->where('client_rm.level', 3) + ->where('client_rm.is_active', 1) + ->where('user_profiles.is_active', 1) + ->findAll(); + + $recipients = []; + $seenEmails = []; + + foreach ($rows as $row) { + $email = strtolower(trim((string) ($row['email'] ?? ''))); + if ($email === '' || isset($seenEmails[$email])) { + continue; + } + + $seenEmails[$email] = true; + $recipients[] = [ + 'name' => trim((string) ($row['first_name'] ?? 'Account Manager')) ?: 'Account Manager', + 'email' => (string) $row['email'], + ]; + } + + return $recipients; + } + + /** + * @param array $account + */ + private static function buildAccountManagerMailContent(array $account, string $recipientName): string + { + return '

Dear ' . esc($recipientName) . ',

' + . '

The CD account balance for ' . esc((string) ($account['client_name'] ?? '')) . ' has fallen below the configured alert threshold.

' + . '' + . '' + . '' + . '' + . '' + . '' + . '
Insurer' . esc((string) ($account['insurer_name'] ?? '')) . '
Insurer Branch' . esc((string) ($account['insurer_branch_name'] ?? '')) . '
CD Account No' . esc((string) ($account['cd_ac_no'] ?? '')) . '
Current Balance' . esc(number_format((float) ($account['current_balance'] ?? 0), 2, '.', '')) . '
Alert Amount' . esc(number_format((float) ($account['alert_amount'] ?? 0), 2, '.', '')) . '
' + . '

Please review the CD account and take the required action.

'; + } +} diff --git a/app/Helpers/DepositHelper.php b/app/Helpers/DepositHelper.php index cc8fbde0..db660b25 100755 --- a/app/Helpers/DepositHelper.php +++ b/app/Helpers/DepositHelper.php @@ -76,6 +76,11 @@ class DepositHelper // Return the response if ($insertId) { + if (($data['transaction_type'] ?? 'Credit') !== 'Credit') { + $cdAcPk = self::resolveCdAcPk($data); + CdLowBalanceAlertHelper::triggerAfterBalanceReduction($cdAcPk, $newBalance); + } + return [ 'success' => true, 'message' => 'Transaction saved successfully', @@ -126,6 +131,24 @@ class DepositHelper return $lastBalance; } + + private static function resolveCdAcPk(array $data): ?int + { + if (! empty($data['cd_ac_pk'])) { + return (int) $data['cd_ac_pk']; + } + + if (empty($data['cd_ac_no'])) { + return null; + } + + $cdMaster = (new CDMasterModel()) + ->where('cd_ac_no', $data['cd_ac_no']) + ->where('is_active', 1) + ->first(); + + return $cdMaster ? (int) $cdMaster['id'] : null; + } } ?> diff --git a/app/Models/CDMasterModel.php b/app/Models/CDMasterModel.php index d5df7b00..2b1e66db 100755 --- a/app/Models/CDMasterModel.php +++ b/app/Models/CDMasterModel.php @@ -118,6 +118,34 @@ class CDMasterModel extends Model * @return array> */ public function getCdMastersWithLowAlertThreshold(): array + { + return $this->buildCdMastersWithLowAlertThresholdQuery() + ->orderBy('cd_master.id', 'desc') + ->get() + ->getResultArray(); + } + + /** + * @return array|null + */ + public function getCdMasterWithLowAlertThresholdById(int $cdAcPk): ?array + { + if ($cdAcPk <= 0) { + return null; + } + + $row = $this->buildCdMastersWithLowAlertThresholdQuery() + ->where('cd_master.id', $cdAcPk) + ->get() + ->getRowArray(); + + return $row ?: null; + } + + /** + * @return \CodeIgniter\Database\BaseBuilder + */ + private function buildCdMastersWithLowAlertThresholdQuery() { return $this->db->table('cd_master') ->select('cd_master.*, clients.client_name, clients.short_name, insurers.name AS insurer_name, insurer_branch.branch_name AS insurer_branch_name') @@ -128,9 +156,6 @@ class CDMasterModel extends Model ->where('clients.is_active', 1) ->where('insurers.is_active', 1) ->where('cd_master.cd_balance_low_alert_amount IS NOT NULL', null, false) - ->where('cd_master.cd_balance_low_alert_amount >', 0) - ->orderBy('cd_master.id', 'desc') - ->get() - ->getResultArray(); + ->where('cd_master.cd_balance_low_alert_amount >', 0); } } diff --git a/tests/smoke_cd_low_balance_alert.php b/tests/smoke_cd_low_balance_alert.php new file mode 100644 index 00000000..0c776240 --- /dev/null +++ b/tests/smoke_cd_low_balance_alert.php @@ -0,0 +1,196 @@ +systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development')); + +$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; +if (is_file($boot)) { + require_once $boot; +} + +use App\Helpers\CdLowBalanceAlertHelper; +use App\Helpers\DepositHelper; +use App\Models\CDMasterModel; +use App\Models\ClientDepositModel; +use App\Models\ClientRMModel; + +$pass = 0; +$fail = 0; +$results = []; + +function ok(string $label, bool $cond, string $detail = ''): void +{ + global $pass, $fail, $results; + if ($cond) { + $pass++; + $results[] = '[PASS] ' . $label . ($detail ? " — {$detail}" : ''); + } else { + $fail++; + $results[] = '[FAIL] ' . $label . ($detail ? " — {$detail}" : ''); + } +} + +function argValue(string $flag): ?string +{ + global $argv; + $idx = array_search($flag, $argv, true); + if ($idx === false || ! isset($argv[$idx + 1])) { + return null; + } + + return (string) $argv[$idx + 1]; +} + +function resolveCurrentBalance(ClientDepositModel $depositModel, int $cdAcPk, float $openingBalance): float +{ + $lastDeposit = $depositModel + ->select('balance') + ->where('cd_ac_pk', $cdAcPk) + ->where('is_active', 1) + ->orderBy('id', 'DESC') + ->first(); + + if (! empty($lastDeposit)) { + return (float) ($lastDeposit['balance'] ?? 0); + } + + return $openingBalance; +} + +$db = db_connect('default'); +$cdMasterModel = new CDMasterModel(); +$depositModel = new ClientDepositModel(); +$runCron = in_array('--cron', $argv, true); +$debitAmount = argValue('--debit'); +$cdAcPkArg = null; + +foreach (array_slice($argv, 1) as $arg) { + if (str_starts_with($arg, '--')) { + continue; + } + if (ctype_digit($arg)) { + $cdAcPkArg = (int) $arg; + break; + } +} + +echo PHP_EOL . '=== smoke_cd_low_balance_alert ===' . PHP_EOL; + +if ($runCron) { + $response = CdLowBalanceAlertHelper::runCron(); + ok('cron status', ($response['status'] ?? false) === true, (string) ($response['message'] ?? '')); + ok('cron response has data key', array_key_exists('data', $response)); + echo 'Cron response: ' . json_encode($response, JSON_PRETTY_PRINT) . PHP_EOL; +} else { + $configured = $cdMasterModel->getCdMastersWithLowAlertThreshold(); + ok('at least one CD master has alert threshold', count($configured) > 0, 'count=' . count($configured)); + + if ($cdAcPkArg === null && ! empty($configured)) { + $cdAcPkArg = (int) ($configured[0]['id'] ?? 0); + } + + ok('cd_ac_pk resolved', $cdAcPkArg !== null && $cdAcPkArg > 0, 'cd_ac_pk=' . ($cdAcPkArg ?? 'none')); + + if ($cdAcPkArg > 0) { + $cdMaster = $cdMasterModel->getCdMasterWithLowAlertThresholdById($cdAcPkArg); + ok('CD master found with alert threshold', $cdMaster !== null, 'cd_ac_pk=' . $cdAcPkArg); + + if ($cdMaster !== null) { + $threshold = (float) ($cdMaster['cd_balance_low_alert_amount'] ?? 0); + $currentBalance = resolveCurrentBalance($depositModel, $cdAcPkArg, (float) ($cdMaster['opening_bal'] ?? 0)); + $isLow = $currentBalance < $threshold; + + echo PHP_EOL . 'Account snapshot:' . PHP_EOL; + echo ' client_id : ' . ($cdMaster['client_id'] ?? '') . ' (' . ($cdMaster['client_name'] ?? '') . ')' . PHP_EOL; + echo ' cd_ac_pk : ' . $cdAcPkArg . PHP_EOL; + echo ' cd_ac_no : ' . ($cdMaster['cd_ac_no'] ?? '') . PHP_EOL; + echo ' balance : ' . number_format($currentBalance, 2) . PHP_EOL; + echo ' alert amt : ' . number_format($threshold, 2) . PHP_EOL; + echo ' below alert : ' . ($isLow ? 'yes' : 'no') . PHP_EOL; + + $acmRows = (new ClientRMModel()) + ->select('user_profiles.first_name, user_profiles.email') + ->join('user_profiles', 'user_profiles.id = client_rm.user_id') + ->where('client_rm.client_id', (int) $cdMaster['client_id']) + ->where('client_rm.level', 3) + ->where('client_rm.is_active', 1) + ->where('user_profiles.is_active', 1) + ->findAll(); + ok('account manager exists for client', count($acmRows) > 0, 'count=' . count($acmRows)); + + foreach ($acmRows as $row) { + echo ' ACM : ' . ($row['first_name'] ?? '') . ' <' . ($row['email'] ?? '') . '>' . PHP_EOL; + } + + if ($debitAmount !== null) { + $amount = (float) $debitAmount; + ok('debit amount > 0', $amount > 0, 'amount=' . $amount); + + $beforeBalance = $currentBalance; + $afterBalance = $beforeBalance - $amount; + + echo PHP_EOL . 'Posting test debit of ' . number_format($amount, 2) . ' ...' . PHP_EOL; + + $saveResult = DepositHelper::saveDeposit([ + 'amount' => $amount, + 'sub_type_id' => 4, + 'client_id' => (int) $cdMaster['client_id'], + 'client_policy_id' => 0, + 'endorsement_no' => 'SMOKE-TEST-' . date('YmdHis'), + 'cd_ac_no' => (string) ($cdMaster['cd_ac_no'] ?? ''), + 'insurer_id' => (int) ($cdMaster['insurer_id'] ?? 0), + 'unit' => 'SMOKE', + 'description' => 'Smoke test debit for CD low balance alert', + 'transaction_type' => 'Debit', + 'updated_by' => 1, + 'event_name' => 'smoke_test', + 'is_active' => 1, + 'cd_ac_pk' => $cdAcPkArg, + ], 1); + + ok('saveDeposit succeeded', ($saveResult['success'] ?? false) === true, json_encode($saveResult)); + + $newBalance = resolveCurrentBalance($depositModel, $cdAcPkArg, (float) ($cdMaster['opening_bal'] ?? 0)); + ok('balance reduced after debit', $newBalance < $beforeBalance, "before={$beforeBalance}, after={$newBalance}"); + + if ($afterBalance < $threshold) { + ok('balance now below alert threshold', $newBalance < $threshold); + echo 'Check writable/logs/log-' . date('Y-m-d') . '.log for CdLowBalanceAlertHelper triggerAfterBalanceReduction' . PHP_EOL; + echo 'ACM mail should be sent if mail is configured.' . PHP_EOL; + } else { + echo 'Balance still above alert threshold after debit; no mail expected.' . PHP_EOL; + echo 'Tip: use a larger --debit amount or lower cd_balance_low_alert_amount for this account.' . PHP_EOL; + } + } else { + echo PHP_EOL . 'Dry run only (no debit posted).' . PHP_EOL; + echo 'To test live debit + alert: php tests/smoke_cd_low_balance_alert.php ' . $cdAcPkArg . ' --debit 1' . PHP_EOL; + echo 'To test cron scan: php tests/smoke_cd_low_balance_alert.php --cron' . PHP_EOL; + } + } + } +} + +echo PHP_EOL . implode(PHP_EOL, $results) . PHP_EOL; +echo PHP_EOL . "Summary: {$pass} passed, {$fail} failed" . PHP_EOL; + +exit($fail > 0 ? 1 : 0);