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