Compare commits

...

1 Commits
test ... dev

Author SHA1 Message Date
cff90ed42e FIX_RENEWAL_REMIND_MAIL 2026-08-12 17:31:52 +05:30
6 changed files with 283 additions and 82 deletions

View File

@ -188,7 +188,7 @@ pt.renewalReminder.days = mon,tue,wed,thu,fri
pt.renewalReminder.clientTypes = 1,2 pt.renewalReminder.clientTypes = 1,2
# renewal_date | policy_end_date # renewal_date | policy_end_date
pt.renewalReminder.dateField = renewal_date pt.renewalReminder.dateField = renewal_date
pt.renewalReminder.daysBefore = 30 pt.renewalReminder.daysBefore = 30, 15, 7, 1, -2, -5, -10
pt.renewalReminder.includeClientEmail = true pt.renewalReminder.includeClientEmail = true
# When true, also send for policies whose dateField is already past (overdue) # When true, also send for policies whose dateField is already past (overdue)
pt.renewalReminder.includeOverdue = false pt.renewalReminder.includeOverdue = false

View File

@ -20,7 +20,8 @@ class PtRenewalReminderConfig extends BaseConfig
/** Whitelisted: renewal_date | policy_end_date */ /** Whitelisted: renewal_date | policy_end_date */
public string $dateField = 'renewal_date'; public string $dateField = 'renewal_date';
public int $daysBefore = 30; /** @var int[] Reminder offsets: positive = days before due, negative = days after due */
public array $daysBeforeList = [30];
public bool $includeClientEmail = false; public bool $includeClientEmail = false;
@ -61,7 +62,7 @@ class PtRenewalReminderConfig extends BaseConfig
'days' => env('pt.renewalReminder.days', 'mon,tue,wed,thu,fri'), 'days' => env('pt.renewalReminder.days', 'mon,tue,wed,thu,fri'),
'clientTypes' => env('pt.renewalReminder.clientTypes', '2'), 'clientTypes' => env('pt.renewalReminder.clientTypes', '2'),
'dateField' => env('pt.renewalReminder.dateField', 'renewal_date'), 'dateField' => env('pt.renewalReminder.dateField', 'renewal_date'),
'daysBefore' => env('pt.renewalReminder.daysBefore', 30), 'daysBefore' => env('pt.renewalReminder.daysBefore', '30'),
'includeClientEmail' => env('pt.renewalReminder.includeClientEmail', 'true'), 'includeClientEmail' => env('pt.renewalReminder.includeClientEmail', 'true'),
'includeOverdue' => env('pt.renewalReminder.includeOverdue', 'false'), 'includeOverdue' => env('pt.renewalReminder.includeOverdue', 'false'),
'toEmails' => env('pt.renewalReminder.toEmails', ''), 'toEmails' => env('pt.renewalReminder.toEmails', ''),
@ -165,7 +166,7 @@ class PtRenewalReminderConfig extends BaseConfig
} }
$this->dateField = $dateField; $this->dateField = $dateField;
$this->daysBefore = max(0, (int) ($merged['daysBefore'] ?? 30)); $this->daysBeforeList = self::parseDaysBeforeList($merged['daysBefore'] ?? 30);
$this->includeClientEmail = $this->toBool($merged['includeClientEmail'] ?? true); $this->includeClientEmail = $this->toBool($merged['includeClientEmail'] ?? true);
$this->includeOverdue = $this->toBool($merged['includeOverdue'] ?? false); $this->includeOverdue = $this->toBool($merged['includeOverdue'] ?? false);
@ -196,9 +197,74 @@ class PtRenewalReminderConfig extends BaseConfig
return $this->dateField; return $this->dateField;
} }
/**
* First / primary offset (legacy callers).
*/
public function getDaysBefore(): int public function getDaysBefore(): int
{ {
return $this->daysBefore; return $this->daysBeforeList[0] ?? 30;
}
/**
* @return int[]
*/
public function getDaysBeforeList(): array
{
return $this->daysBeforeList;
}
public function getDaysBeforeDisplay(): string
{
return implode(', ', $this->daysBeforeList);
}
/**
* Map each offset to the calendar date it matches (today + offset).
*
* @return array<int, string> offset => Y-m-d
*/
public function getTargetDates(?string $today = null): array
{
$today = $today ?: date('Y-m-d');
$map = [];
foreach ($this->daysBeforeList as $offset) {
$map[(int) $offset] = date('Y-m-d', strtotime($today . ' ' . sprintf('%+d days', (int) $offset)));
}
return $map;
}
/**
* Parse "30, 15, 7, 1, -2, -5, -10" (or a single int / array) into unique offsets.
*
* @return int[]
*/
public static function parseDaysBeforeList(mixed $value): array
{
if (is_int($value) || is_float($value)) {
$raw = [(int) $value];
} elseif (is_array($value)) {
$raw = $value;
} else {
$raw = preg_split('/[,\s]+/', trim((string) $value), -1, PREG_SPLIT_NO_EMPTY) ?: [];
}
$out = [];
$seen = [];
foreach ($raw as $item) {
$item = trim((string) $item);
if ($item === '' || ! is_numeric($item)) {
continue;
}
$n = (int) $item;
if ($n > 365 || $n < -365 || isset($seen[$n])) {
continue;
}
$seen[$n] = true;
$out[] = $n;
}
return $out !== [] ? $out : [30];
} }
/** /**
@ -271,7 +337,7 @@ class PtRenewalReminderConfig extends BaseConfig
'days' => implode(',', $this->days), 'days' => implode(',', $this->days),
'clientTypes' => $this->clientTypes, 'clientTypes' => $this->clientTypes,
'dateField' => $this->dateField, 'dateField' => $this->dateField,
'daysBefore' => $this->daysBefore, 'daysBefore' => $this->getDaysBeforeDisplay(),
'includeClientEmail' => $this->includeClientEmail, 'includeClientEmail' => $this->includeClientEmail,
'includeOverdue' => $this->includeOverdue, 'includeOverdue' => $this->includeOverdue,
'toEmails' => implode(',', $this->toEmails), 'toEmails' => implode(',', $this->toEmails),

View File

@ -6014,7 +6014,7 @@ class PolicyTransactionController extends BaseController
$this->logRenewalReminderStep( $this->logRenewalReminderStep(
$logPrefix . ' Fetching candidates for dateField=' $logPrefix . ' Fetching candidates for dateField='
. $config->getDateField() . $config->getDateField()
. ', daysBefore=' . $config->getDaysBefore() . ', daysBefore=' . $config->getDaysBeforeDisplay()
. ', includeOverdue=' . ($config->includeOverdue() ? 'true' : 'false') . ', includeOverdue=' . ($config->includeOverdue() ? 'true' : 'false')
. ', clientTypes=' . json_encode($config->getClientTypes()) . ', clientTypes=' . json_encode($config->getClientTypes())
); );
@ -6023,7 +6023,7 @@ class PolicyTransactionController extends BaseController
try { try {
$candidates = $this->policyTransactionModel->getRenewalReminderCandidates( $candidates = $this->policyTransactionModel->getRenewalReminderCandidates(
$config->getDateField(), $config->getDateField(),
$config->getDaysBefore(), $config->getDaysBeforeList(),
$config->getClientTypes(), $config->getClientTypes(),
$config->includeOverdue() $config->includeOverdue()
); );
@ -6066,7 +6066,7 @@ class PolicyTransactionController extends BaseController
$this->logRenewalReminderStep( $this->logRenewalReminderStep(
$logPrefix . ' ' . $message $logPrefix . ' ' . $message
. ' | dateField=' . $config->getDateField() . ' | dateField=' . $config->getDateField()
. ' | daysBefore=' . $config->getDaysBefore() . ' | daysBefore=' . $config->getDaysBeforeDisplay()
. ' | includeOverdue=' . ($config->includeOverdue() ? 'true' : 'false') . ' | includeOverdue=' . ($config->includeOverdue() ? 'true' : 'false')
); );
if ($isCli) { if ($isCli) {
@ -6081,7 +6081,7 @@ class PolicyTransactionController extends BaseController
try { try {
$diagnostics = $this->policyTransactionModel->diagnoseRenewalReminderCandidates( $diagnostics = $this->policyTransactionModel->diagnoseRenewalReminderCandidates(
$config->getDateField(), $config->getDateField(),
$config->getDaysBefore(), $config->getDaysBeforeList(),
$config->getClientTypes(), $config->getClientTypes(),
$config->includeOverdue() $config->includeOverdue()
); );
@ -6119,6 +6119,9 @@ class PolicyTransactionController extends BaseController
$emptyPayload['config'] = $configSnapshot; $emptyPayload['config'] = $configSnapshot;
$emptyPayload['detailed_summary'] = $detailedSummary; $emptyPayload['detailed_summary'] = $detailedSummary;
$emptyPayload['why_empty'] = $diagnostics; $emptyPayload['why_empty'] = $diagnostics;
if (! empty($detailedSummary['client_mail'])) {
$emptyPayload['summary']['client_mail'] = $detailedSummary['client_mail'];
}
} }
return $this->renewalReminderResponse($emptyPayload, $isCli); return $this->renewalReminderResponse($emptyPayload, $isCli);
@ -6225,6 +6228,8 @@ class PolicyTransactionController extends BaseController
'policy_start_date' => $row['policy_start_date'] ?? null, 'policy_start_date' => $row['policy_start_date'] ?? null,
'policy_end_date' => $row['policy_end_date'] ?? null, 'policy_end_date' => $row['policy_end_date'] ?? null,
'reminder_date' => ! empty($reminderRaw) ? date('Y-m-d', strtotime((string) $reminderRaw)) : null, 'reminder_date' => ! empty($reminderRaw) ? date('Y-m-d', strtotime((string) $reminderRaw)) : null,
'days_before' => isset($row['reminder_offset']) ? (int) $row['reminder_offset'] : null,
'reminder_offset' => isset($row['reminder_offset']) ? (int) $row['reminder_offset'] : null,
'is_overdue' => $isOverdue, 'is_overdue' => $isOverdue,
'status' => 'dry_run', 'status' => 'dry_run',
'message' => 'Dry run — mail not sent, status not updated', 'message' => 'Dry run — mail not sent, status not updated',
@ -6285,9 +6290,10 @@ class PolicyTransactionController extends BaseController
if (isset($res->status) && $res->status === 'success') { if (isset($res->status) && $res->status === 'success') {
$successCount++; $successCount++;
try { try {
$this->policyTransactionModel->update((int) $ptId, [ $this->policyTransactionModel->markRenewalReminderSent(
'renewal_status' => 'renewal_mail_send', (int) $ptId,
]); isset($row['reminder_offset']) ? (int) $row['reminder_offset'] : null
);
$this->logRenewalReminderStep( $this->logRenewalReminderStep(
$logPrefix . " PT {$ptId}: renewal_status set to renewal_mail_send" $logPrefix . " PT {$ptId}: renewal_status set to renewal_mail_send"
); );
@ -6407,6 +6413,9 @@ class PolicyTransactionController extends BaseController
$responsePayload['config'] = $configSnapshot; $responsePayload['config'] = $configSnapshot;
$responsePayload['detailed_summary'] = $detailedSummary; $responsePayload['detailed_summary'] = $detailedSummary;
$responsePayload['message'] = $detailedSummary['headline'] ?? $summaryMessage; $responsePayload['message'] = $detailedSummary['headline'] ?? $summaryMessage;
if (! empty($detailedSummary['client_mail'])) {
$responsePayload['summary']['client_mail'] = $detailedSummary['client_mail'];
}
} }
return $this->renewalReminderResponse($responsePayload, $isCli); return $this->renewalReminderResponse($responsePayload, $isCli);
@ -6495,6 +6504,8 @@ class PolicyTransactionController extends BaseController
$policiesWithoutPolicyNo = 0; $policiesWithoutPolicyNo = 0;
$policiesWithClientEmailInTo = 0; $policiesWithClientEmailInTo = 0;
$policiesMissingClientEmail = 0; $policiesMissingClientEmail = 0;
$policiesWithClientEmailOnRecord = 0;
$policiesConfiguredToOnly = 0;
$clientEmailsInTo = []; $clientEmailsInTo = [];
foreach ($results as $item) { foreach ($results as $item) {
@ -6526,6 +6537,11 @@ class PolicyTransactionController extends BaseController
$policiesWithoutPolicyNo++; $policiesWithoutPolicyNo++;
} }
$clientEmailRaw = trim((string) ($item['client_email'] ?? ''));
if ($clientEmailRaw !== '') {
$policiesWithClientEmailOnRecord++;
}
$clientEmailIncluded = ! empty($item['client_email_included']); $clientEmailIncluded = ! empty($item['client_email_included']);
if ($clientEmailIncluded) { if ($clientEmailIncluded) {
$policiesWithClientEmailInTo++; $policiesWithClientEmailInTo++;
@ -6539,15 +6555,20 @@ class PolicyTransactionController extends BaseController
$policiesMissingClientEmail++; $policiesMissingClientEmail++;
} }
if ($status === 'dry_run' && ! $clientEmailIncluded && (int) ($item['to_count'] ?? count($item['to'] ?? [])) > 0) {
$policiesConfiguredToOnly++;
}
$compact = [ $compact = [
'pt_id' => $item['pt_id'] ?? null, 'pt_id' => $item['pt_id'] ?? null,
'policy_no' => $policyNo !== '' ? $policyNo : 'Not Assigned', 'policy_no' => $policyNo !== '' ? $policyNo : 'Not Assigned',
'client_name' => $item['client_name'] ?? null, 'client_name' => $item['client_name'] ?? null,
'client_email' => $item['client_email'] ?? null, 'client_email' => $clientEmailRaw !== '' ? $clientEmailRaw : null,
'client_email_included' => $clientEmailIncluded, 'client_email_included' => $clientEmailIncluded,
'include_client_email_enabled' => ! empty($item['include_client_email_enabled']), 'include_client_email_enabled' => ! empty($item['include_client_email_enabled']),
'policy_type' => $item['policy_type'] ?? null, 'policy_type' => $item['policy_type'] ?? null,
'reminder_date' => $item['reminder_date'] ?? ($item['renewal_date'] ?? $item['policy_end_date'] ?? null), 'reminder_date' => $item['reminder_date'] ?? ($item['renewal_date'] ?? $item['policy_end_date'] ?? null),
'days_before' => $item['days_before'] ?? ($item['reminder_offset'] ?? null),
'is_overdue' => $isOverdue, 'is_overdue' => $isOverdue,
'to' => $item['to'] ?? [], 'to' => $item['to'] ?? [],
'to_count' => (int) ($item['to_count'] ?? count($item['to'] ?? [])), 'to_count' => (int) ($item['to_count'] ?? count($item['to'] ?? [])),
@ -6590,7 +6611,8 @@ class PolicyTransactionController extends BaseController
} }
} }
$targetDate = date('Y-m-d', strtotime('+' . max(0, (int) $config->getDaysBefore()) . ' days')); $targetDates = $config->getTargetDates();
$targetDate = implode(', ', array_values($targetDates));
$headline = sprintf( $headline = sprintf(
'DRY RUN — %d policy(ies) pending for renewal reminder: %d ready to mail, %d skipped (no email), %d failed | client email in TO: %d, missing client email: %d', 'DRY RUN — %d policy(ies) pending for renewal reminder: %d ready to mail, %d skipped (no email), %d failed | client email in TO: %d, missing client email: %d',
@ -6611,7 +6633,7 @@ class PolicyTransactionController extends BaseController
'Policies with client email in TO: ' . $policiesWithClientEmailInTo, 'Policies with client email in TO: ' . $policiesWithClientEmailInTo,
'Policies missing/invalid client email: ' . $policiesMissingClientEmail, 'Policies missing/invalid client email: ' . $policiesMissingClientEmail,
'Unique client emails in TO: ' . count($clientEmailsInTo), 'Unique client emails in TO: ' . count($clientEmailsInTo),
'Due on target date (' . $targetDate . '): ' . $dueCount, 'Due on target date(s) (' . $targetDate . '): ' . $dueCount,
'Overdue policies included: ' . $overdueCount, 'Overdue policies included: ' . $overdueCount,
'Unique TO recipients: ' . count($uniqueTo), 'Unique TO recipients: ' . count($uniqueTo),
'Unique CC recipients: ' . count($uniqueCc), 'Unique CC recipients: ' . count($uniqueCc),
@ -6619,13 +6641,24 @@ class PolicyTransactionController extends BaseController
'Total TO deliveries (per mail): ' . $totalToHits, 'Total TO deliveries (per mail): ' . $totalToHits,
'Policies without policy no: ' . $policiesWithoutPolicyNo, 'Policies without policy no: ' . $policiesWithoutPolicyNo,
'Date field: ' . $config->getDateField(), 'Date field: ' . $config->getDateField(),
'Days before: ' . $config->getDaysBefore(), 'Days before: ' . $config->getDaysBeforeDisplay(),
'Include overdue: ' . ($config->includeOverdue() ? 'yes' : 'no'), 'Include overdue: ' . ($config->includeOverdue() ? 'yes' : 'no'),
]; ];
$clientMailSummary = [
'enabled' => $config->includeClientEmail(),
'policies_with_email_on_record' => $policiesWithClientEmailOnRecord,
'policies_included_in_to' => $policiesWithClientEmailInTo,
'policies_missing_or_invalid' => $policiesMissingClientEmail,
'policies_configured_to_only' => $policiesConfiguredToOnly,
'unique_client_emails_in_to' => count($clientEmailsInTo),
'client_emails_in_to' => array_keys($clientEmailsInTo),
];
return [ return [
'headline' => $headline, 'headline' => $headline,
'lines' => $lines, 'lines' => $lines,
'client_mail' => $clientMailSummary,
'counts' => [ 'counts' => [
'policies_pending_renewal' => $candidateCount, 'policies_pending_renewal' => $candidateCount,
'policies_ready_to_mail' => $wouldSendCount, 'policies_ready_to_mail' => $wouldSendCount,
@ -6636,8 +6669,10 @@ class PolicyTransactionController extends BaseController
'policies_without_policy_no' => $policiesWithoutPolicyNo, 'policies_without_policy_no' => $policiesWithoutPolicyNo,
'policies_without_email' => $policiesWithoutEmail, 'policies_without_email' => $policiesWithoutEmail,
'include_client_email_enabled' => $config->includeClientEmail(), 'include_client_email_enabled' => $config->includeClientEmail(),
'policies_with_client_email_on_record' => $policiesWithClientEmailOnRecord,
'policies_with_client_email_in_to' => $policiesWithClientEmailInTo, 'policies_with_client_email_in_to' => $policiesWithClientEmailInTo,
'policies_missing_client_email' => $policiesMissingClientEmail, 'policies_missing_client_email' => $policiesMissingClientEmail,
'policies_configured_to_only' => $policiesConfiguredToOnly,
'unique_client_emails_in_to' => count($clientEmailsInTo), 'unique_client_emails_in_to' => count($clientEmailsInTo),
'unique_to_recipients' => count($uniqueTo), 'unique_to_recipients' => count($uniqueTo),
'unique_cc_recipients' => count($uniqueCc), 'unique_cc_recipients' => count($uniqueCc),
@ -6649,8 +6684,9 @@ class PolicyTransactionController extends BaseController
], ],
'filters' => [ 'filters' => [
'date_field' => $config->getDateField(), 'date_field' => $config->getDateField(),
'days_before' => $config->getDaysBefore(), 'days_before' => $config->getDaysBeforeList(),
'target_date' => $targetDate, 'target_date' => $targetDates[$config->getDaysBefore()] ?? reset($targetDates),
'target_dates' => $targetDates,
'include_overdue' => $config->includeOverdue(), 'include_overdue' => $config->includeOverdue(),
'include_client_email' => $config->includeClientEmail(), 'include_client_email' => $config->includeClientEmail(),
'client_types' => $config->getClientTypes(), 'client_types' => $config->getClientTypes(),
@ -6722,7 +6758,9 @@ class PolicyTransactionController extends BaseController
$dateField = $config->getDateField(); $dateField = $config->getDateField();
$reminderRaw = $data[$dateField] ?? ($data['renewal_date'] ?? $data['policy_end_date'] ?? null); $reminderRaw = $data[$dateField] ?? ($data['renewal_date'] ?? $data['policy_end_date'] ?? null);
$reminderDate = ! empty($reminderRaw) ? date('d-m-Y', strtotime((string) $reminderRaw)) : ''; $reminderDate = ! empty($reminderRaw) ? date('d-m-Y', strtotime((string) $reminderRaw)) : '';
$daysBefore = $config->getDaysBefore(); $daysBefore = isset($data['reminder_offset']) && $data['reminder_offset'] !== '' && $data['reminder_offset'] !== null
? (int) $data['reminder_offset']
: $config->getDaysBefore();
$isOverdue = ! empty($data['is_overdue']) $isOverdue = ! empty($data['is_overdue'])
|| (! empty($reminderRaw) && strtotime((string) $reminderRaw) < strtotime(date('Y-m-d'))); || (! empty($reminderRaw) && strtotime((string) $reminderRaw) < strtotime(date('Y-m-d')));
$policyLabel = $policyNo !== '' ? "Policy #{$policyNo}" : 'Policy Pending'; $policyLabel = $policyNo !== '' ? "Policy #{$policyNo}" : 'Policy Pending';
@ -7077,7 +7115,7 @@ class PolicyTransactionController extends BaseController
'days' => trim((string) $this->request->getPost('days')), 'days' => trim((string) $this->request->getPost('days')),
'clientTypes' => [2], 'clientTypes' => [2],
'dateField' => trim((string) $this->request->getPost('dateField')), 'dateField' => trim((string) $this->request->getPost('dateField')),
'daysBefore' => (int) $this->request->getPost('daysBefore'), 'daysBefore' => PtRenewalReminderConfig::parseDaysBeforeList($this->request->getPost('daysBefore')),
'includeClientEmail' => $this->request->getPost('includeClientEmail'), 'includeClientEmail' => $this->request->getPost('includeClientEmail'),
'includeOverdue' => $this->request->getPost('includeOverdue'), 'includeOverdue' => $this->request->getPost('includeOverdue'),
'toEmails' => trim((string) $this->request->getPost('toEmails')), 'toEmails' => trim((string) $this->request->getPost('toEmails')),
@ -7094,7 +7132,7 @@ class PolicyTransactionController extends BaseController
if (! in_array($payload['dateField'], ['renewal_date', 'policy_end_date'], true)) { if (! in_array($payload['dateField'], ['renewal_date', 'policy_end_date'], true)) {
$payload['dateField'] = 'renewal_date'; $payload['dateField'] = 'renewal_date';
} }
$payload['daysBefore'] = max(0, $payload['daysBefore']); $payload['daysBefore'] = implode(', ', $payload['daysBefore']);
$updated = $this->notificationModel->update((int) $row['id'], [ $updated = $this->notificationModel->update((int) $row['id'], [
'config_json' => json_encode($payload), 'config_json' => json_encode($payload),
@ -7126,6 +7164,12 @@ class PolicyTransactionController extends BaseController
$config['days'] = implode(',', $config['days']); $config['days'] = implode(',', $config['days']);
} }
if (is_array($config['daysBefore'] ?? null)) {
$config['daysBefore'] = implode(', ', $config['daysBefore']);
} elseif (isset($config['daysBefore'])) {
$config['daysBefore'] = implode(', ', PtRenewalReminderConfig::parseDaysBeforeList($config['daysBefore']));
}
foreach (['toEmails', 'ccEmails', 'bccEmails'] as $field) { foreach (['toEmails', 'ccEmails', 'bccEmails'] as $field) {
if (is_array($config[$field] ?? null)) { if (is_array($config[$field] ?? null)) {
$config[$field] = implode(',', $config[$field]); $config[$field] = implode(',', $config[$field]);
@ -7357,7 +7401,7 @@ class PolicyTransactionController extends BaseController
'days' => 'mon,tue,wed,thu,fri', 'days' => 'mon,tue,wed,thu,fri',
'clientTypes' => [2], 'clientTypes' => [2],
'dateField' => 'renewal_date', 'dateField' => 'renewal_date',
'daysBefore' => 30, 'daysBefore' => '30',
'includeClientEmail' => true, 'includeClientEmail' => true,
'includeOverdue' => false, 'includeOverdue' => false,
'toEmails' => '', 'toEmails' => '',

View File

@ -63,6 +63,7 @@
'edate', 'edate',
'renewal_date', 'renewal_date',
'renewal_status', 'renewal_status',
'renewal_last_reminder_offset',
'rollover_date', 'rollover_date',
'policy_holder_name', 'policy_holder_name',
'same_as_proposer', 'same_as_proposer',
@ -2131,15 +2132,15 @@
/** /**
* Candidates for policy_transaction renewal reminder cron. * Candidates for policy_transaction renewal reminder cron.
* *
* @param string $dateField Whitelisted column: renewal_date | policy_end_date * @param string $dateField Whitelisted column: renewal_date | policy_end_date
* @param int $daysBefore Days ahead of today to match * @param int|string|int[] $daysBefore Comma-separated or list of offsets (negative = days after due)
* @param int[] $clientTypes Client type ids (1=Group, 2=Individual) * @param int[] $clientTypes Client type ids (1=Group, 2=Individual)
* @param bool $includeOverdue When true, also include rows where dateField < today * @param bool $includeOverdue When true, also include rows where dateField < today
* @return array * @return array
*/ */
public function getRenewalReminderCandidates( public function getRenewalReminderCandidates(
string $dateField, string $dateField,
int $daysBefore, $daysBefore,
array $clientTypes, array $clientTypes,
bool $includeOverdue = false bool $includeOverdue = false
): array { ): array {
@ -2156,10 +2157,10 @@
return []; return [];
} }
$daysBefore = max(0, (int) $daysBefore); $offsets = $this->normalizeReminderOffsets($daysBefore);
$targetDate = date('Y-m-d', strtotime('+' . $daysBefore . ' days')); $today = date('Y-m-d');
$today = date('Y-m-d'); $targetDates = $this->reminderTargetDates($offsets, $today);
$column = 'pt.' . $dateField; $column = 'pt.' . $dateField;
$query = $this->db->table('policy_transaction pt') $query = $this->db->table('policy_transaction pt')
->select(" ->select("
@ -2177,6 +2178,7 @@
c.common_mails, c.common_mails,
policy_type.policy_type, policy_type.policy_type,
i.name AS insurer_name, i.name AS insurer_name,
DATEDIFF({$column}, CURDATE()) AS reminder_offset,
CASE WHEN {$column} < '{$today}' THEN 1 ELSE 0 END AS is_overdue CASE WHEN {$column} < '{$today}' THEN 1 ELSE 0 END AS is_overdue
") ")
->join('clients c', 'pt.client_id = c.id', 'left') ->join('clients c', 'pt.client_id = c.id', 'left')
@ -2185,18 +2187,9 @@
->join('policy_type', 'pt.policy_type_id = policy_type.id', 'left') ->join('policy_type', 'pt.policy_type_id = policy_type.id', 'left')
->where('pt.is_active', 1) ->where('pt.is_active', 1)
->where('pt.action_type', 'inception') ->where('pt.action_type', 'inception')
->whereIn('c.client_type', $clientTypes) ->whereIn('c.client_type', $clientTypes);
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false)
->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)");
if ($includeOverdue) { $this->applyRenewalReminderCandidateFilters($query, $column, $targetDates, $today, $includeOverdue);
$query->groupStart()
->where($column, $targetDate)
->orWhere("{$column} <", $today)
->groupEnd();
} else {
$query->where($column, $targetDate);
}
$query->groupBy('pt.id') $query->groupBy('pt.id')
->orderBy('pt.id', 'DESC'); ->orderBy('pt.id', 'DESC');
@ -2207,12 +2200,13 @@
/** /**
* Explain why renewal reminder candidates may be empty (for dry-run diagnostics). * Explain why renewal reminder candidates may be empty (for dry-run diagnostics).
* *
* @param int|string|int[] $daysBefore
* @param int[] $clientTypes * @param int[] $clientTypes
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function diagnoseRenewalReminderCandidates( public function diagnoseRenewalReminderCandidates(
string $dateField, string $dateField,
int $daysBefore, $daysBefore,
array $clientTypes, array $clientTypes,
bool $includeOverdue = false bool $includeOverdue = false
): array { ): array {
@ -2225,10 +2219,11 @@
return $type > 0; return $type > 0;
}))); })));
$daysBefore = max(0, (int) $daysBefore); $offsets = $this->normalizeReminderOffsets($daysBefore);
$targetDate = date('Y-m-d', strtotime('+' . $daysBefore . ' days')); $today = date('Y-m-d');
$today = date('Y-m-d'); $targetDates = $this->reminderTargetDates($offsets, $today);
$column = 'pt.' . $dateField; $column = 'pt.' . $dateField;
$targetLabel = implode(', ', $targetDates);
$base = function () { $base = function () {
return $this->db->table('policy_transaction pt') return $this->db->table('policy_transaction pt')
@ -2255,25 +2250,25 @@
$countAfterStatus = (int) $base() $countAfterStatus = (int) $base()
->whereIn('c.client_type', $clientTypes) ->whereIn('c.client_type', $clientTypes)
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false) ->where("IFNULL(pt.renewal_status, '') !=", 'renewed')
->countAllResults(); ->countAllResults();
$countAfterNotRenewedExists = (int) $base() $countAfterNotRenewedExists = (int) $base()
->whereIn('c.client_type', $clientTypes) ->whereIn('c.client_type', $clientTypes)
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false) ->where("IFNULL(pt.renewal_status, '') !=", 'renewed')
->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)") ->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)")
->countAllResults(); ->countAllResults();
$countExactTargetDate = (int) $base() $countExactTargetDate = (int) $base()
->whereIn('c.client_type', $clientTypes) ->whereIn('c.client_type', $clientTypes)
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false) ->where("IFNULL(pt.renewal_status, '') !=", 'renewed')
->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)") ->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)")
->where($column, $targetDate) ->whereIn($column, $targetDates)
->countAllResults(); ->countAllResults();
$countOverdue = (int) $base() $countOverdue = (int) $base()
->whereIn('c.client_type', $clientTypes) ->whereIn('c.client_type', $clientTypes)
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false) ->where("IFNULL(pt.renewal_status, '') !=", 'renewed')
->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)") ->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)")
->where("{$column} <", $today) ->where("{$column} <", $today)
->where("{$column} IS NOT NULL", null, false) ->where("{$column} IS NOT NULL", null, false)
@ -2281,27 +2276,16 @@
->countAllResults(); ->countAllResults();
$finalBuilder = $base() $finalBuilder = $base()
->whereIn('c.client_type', $clientTypes) ->whereIn('c.client_type', $clientTypes);
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false) $this->applyRenewalReminderCandidateFilters($finalBuilder, $column, $targetDates, $today, $includeOverdue);
->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)");
if ($includeOverdue) {
$finalBuilder->groupStart()
->where($column, $targetDate)
->orWhere("{$column} <", $today)
->groupEnd();
} else {
$finalBuilder->where($column, $targetDate);
}
$countFinal = (int) $finalBuilder->countAllResults(); $countFinal = (int) $finalBuilder->countAllResults();
// Sample nearest upcoming renewals (next 60 days) for debugging
$from = $today; $from = $today;
$to = date('Y-m-d', strtotime('+60 days')); $to = date('Y-m-d', strtotime('+60 days'));
$nearestSample = $base() $nearestSample = $base()
->select("pt.id, pt.policy_no, c.client_name, c.email AS client_email, pt.renewal_date, pt.policy_end_date, pt.renewal_status, {$column} AS match_date") ->select("pt.id, pt.policy_no, c.client_name, c.email AS client_email, pt.renewal_date, pt.policy_end_date, pt.renewal_status, {$column} AS match_date, DATEDIFF({$column}, CURDATE()) AS reminder_offset")
->whereIn('c.client_type', $clientTypes) ->whereIn('c.client_type', $clientTypes)
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false) ->where("IFNULL(pt.renewal_status, '') !=", 'renewed')
->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)") ->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)")
->where("{$column} >=", $from) ->where("{$column} >=", $from)
->where("{$column} <=", $to) ->where("{$column} <=", $to)
@ -2318,16 +2302,16 @@
if ($countAfterClientType === 0) { if ($countAfterClientType === 0) {
$reasons[] = 'No active inception policies found for configured client type(s).'; $reasons[] = 'No active inception policies found for configured client type(s).';
} elseif ($countAfterStatus === 0) { } elseif ($countAfterStatus === 0) {
$reasons[] = 'All matching policies already have renewal_status of renewal_mail_send/renewed/reminded.'; $reasons[] = 'All matching policies already have renewal_status of renewed.';
} elseif ($countAfterNotRenewedExists === 0) { } elseif ($countAfterNotRenewedExists === 0) {
$reasons[] = 'All remaining policies already have an active renewed policy linked via source_client_policy_id.'; $reasons[] = 'All remaining policies already have an active renewed policy linked via source_client_policy_id.';
} elseif (! $includeOverdue && $countExactTargetDate === 0) { } elseif (! $includeOverdue && $countExactTargetDate === 0) {
$reasons[] = "No policies have {$dateField} exactly equal to target date {$targetDate} (today + {$daysBefore} days). includeOverdue is false, so only exact date matches are selected."; $reasons[] = "No policies have {$dateField} equal to any target date ({$targetLabel}) for offsets [" . implode(', ', $offsets) . '].';
if ($countOverdue > 0) { if ($countOverdue > 0) {
$reasons[] = "There are {$countOverdue} overdue policies that would be included if includeOverdue=true."; $reasons[] = "There are {$countOverdue} overdue policies that would be included if includeOverdue=true, or if a negative offset matches today.";
} }
} elseif ($includeOverdue && $countFinal === 0) { } elseif ($includeOverdue && $countFinal === 0) {
$reasons[] = "No policies match target date {$targetDate} or overdue (< {$today}) on {$dateField}."; $reasons[] = "No policies match target dates ({$targetLabel}) or overdue (< {$today}) on {$dateField}.";
} }
if (empty($reasons) && $countFinal === 0) { if (empty($reasons) && $countFinal === 0) {
@ -2336,14 +2320,15 @@
return [ return [
'today' => $today, 'today' => $today,
'target_date' => $targetDate, 'target_date' => $targetDates[0] ?? null,
'target_dates' => $targetDates,
'date_field' => $dateField, 'date_field' => $dateField,
'days_before' => $daysBefore, 'days_before' => $offsets,
'include_overdue' => $includeOverdue, 'include_overdue' => $includeOverdue,
'client_types' => $clientTypes, 'client_types' => $clientTypes,
'counts' => [ 'counts' => [
'active_inception_for_client_types' => $countAfterClientType, 'active_inception_for_client_types' => $countAfterClientType,
'after_excluding_sent_or_renewed' => $countAfterStatus, 'after_excluding_renewed' => $countAfterStatus,
'after_excluding_already_renewed_policy' => $countAfterNotRenewedExists, 'after_excluding_already_renewed_policy' => $countAfterNotRenewedExists,
'exact_target_date_matches' => $countExactTargetDate, 'exact_target_date_matches' => $countExactTargetDate,
'overdue_matches' => $countOverdue, 'overdue_matches' => $countOverdue,
@ -2352,11 +2337,106 @@
'reasons' => $reasons, 'reasons' => $reasons,
'nearest_upcoming_sample_60_days' => $nearestSample, 'nearest_upcoming_sample_60_days' => $nearestSample,
'hint' => $includeOverdue 'hint' => $includeOverdue
? 'Candidates = exact target date OR overdue on selected date field.' ? 'Candidates = any configured offset date OR overdue on selected date field.'
: "Candidates require {$dateField} = {$targetDate} exactly. Enable includeOverdue to also include past dates, or change daysBefore / dateField in Mail Config.", : "Candidates require {$dateField} IN ({$targetLabel}). Negative offsets send after the due date. Change daysBefore in Mail Config.",
]; ];
} }
/**
* Mark a policy as mailed for the current reminder offset.
*/
public function markRenewalReminderSent(int $ptId, ?int $offset = null): bool
{
$data = ['renewal_status' => 'renewal_mail_send'];
if ($offset !== null && $this->hasRenewalLastReminderOffsetColumn()) {
$data['renewal_last_reminder_offset'] = $offset;
}
return (bool) $this->update($ptId, $data);
}
/**
* @param int|string|int[] $daysBefore
* @return int[]
*/
private function normalizeReminderOffsets($daysBefore): array
{
return \Config\PtRenewalReminderConfig::parseDaysBeforeList($daysBefore);
}
/**
* @param int[] $offsets
* @return string[]
*/
private function reminderTargetDates(array $offsets, string $today): array
{
$dates = [];
foreach ($offsets as $offset) {
$dates[] = date('Y-m-d', strtotime($today . ' ' . sprintf('%+d days', (int) $offset)));
}
return array_values(array_unique($dates));
}
private function hasRenewalLastReminderOffsetColumn(): bool
{
static $exists = null;
if ($exists === null) {
$exists = $this->db->fieldExists('renewal_last_reminder_offset', $this->table);
}
return $exists;
}
/**
* @param object $query Query builder
* @param string[] $targetDates
*/
private function applyRenewalReminderCandidateFilters(
$query,
string $column,
array $targetDates,
string $today,
bool $includeOverdue
): void {
$query->where("NOT EXISTS (SELECT 1 FROM policy_transaction p2 WHERE p2.source_client_policy_id = pt.client_policy_id AND p2.is_active = 1)");
if ($this->hasRenewalLastReminderOffsetColumn()) {
$query->where("IFNULL(pt.renewal_status, '') !=", 'renewed');
$offsetSkip = "(pt.renewal_last_reminder_offset IS NULL OR pt.renewal_last_reminder_offset != DATEDIFF({$column}, CURDATE()))";
if ($includeOverdue) {
$query->groupStart()
->groupStart()
->whereIn($column, $targetDates)
->where($offsetSkip, null, false)
->groupEnd()
->orGroupStart()
->where("{$column} <", $today)
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'reminded')", null, false)
->where('pt.renewal_last_reminder_offset', null)
->groupEnd()
->groupEnd();
} else {
$query->whereIn($column, $targetDates)
->where($offsetSkip, null, false);
}
return;
}
$query->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false);
if ($includeOverdue) {
$query->groupStart()
->whereIn($column, $targetDates)
->orWhere("{$column} <", $today)
->groupEnd();
} else {
$query->whereIn($column, $targetDates);
}
}
/** /**
* Server-side DataTables list for Retail Policy (individual clients only). * Server-side DataTables list for Retail Policy (individual clients only).
* *

View File

@ -261,12 +261,14 @@
<label class="custom-control-label" for="cfg_includeClientEmail">Include Client Email</label> <label class="custom-control-label" for="cfg_includeClientEmail">Include Client Email</label>
</div> </div>
</div> </div>
<!--
<div class="form-group col-md-3 col-sm-6"> <div class="form-group col-md-3 col-sm-6">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="cfg_includeOverdue" name="includeOverdue" value="1"> <input type="checkbox" class="custom-control-input" id="cfg_includeOverdue" name="includeOverdue" value="1">
<label class="custom-control-label" for="cfg_includeOverdue">Include Overdue</label> <label class="custom-control-label" for="cfg_includeOverdue">Include Overdue</label>
</div> </div>
</div> </div>
-->
</div> </div>
<div class="form-row"> <div class="form-row">
@ -283,11 +285,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group col-md-3"> <div class="form-group col-md-8">
<label for="cfg_daysBefore">Days Before</label> <label for="cfg_daysBefore">Days Before</label>
<input type="number" min="0" class="form-control" id="cfg_daysBefore" name="daysBefore" value="30"> <input type="text" class="form-control" id="cfg_daysBefore" name="daysBefore" value="30" placeholder="30, 15, 7, 1, -2, -5, -10">
<small class="form-text text-muted">Comma-separated offsets. Positive = days before due, negative = days after due.</small>
</div> </div>
<div class="form-group col-md-5"> </div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="cfg_days">Weekdays (used when daily is off)</label> <label for="cfg_days">Weekdays (used when daily is off)</label>
<input type="text" class="form-control" id="cfg_days" name="days" placeholder="mon,tue,wed,thu,fri"> <input type="text" class="form-control" id="cfg_days" name="days" placeholder="mon,tue,wed,thu,fri">
</div> </div>
@ -478,7 +483,7 @@
$('#cfg_daily').prop('checked', boolVal(data.daily)); $('#cfg_daily').prop('checked', boolVal(data.daily));
$('#cfg_includeClientEmail').prop('checked', boolVal(data.includeClientEmail)); $('#cfg_includeClientEmail').prop('checked', boolVal(data.includeClientEmail));
$('#cfg_includeOverdue').prop('checked', boolVal(data.includeOverdue)); $('#cfg_includeOverdue').prop('checked', boolVal(data.includeOverdue));
$('#cfg_daysBefore').val(data.daysBefore != null ? data.daysBefore : 30); $('#cfg_daysBefore').val(listVal(data.daysBefore) || '30');
setRetailDateField(data.dateField || 'renewal_date'); setRetailDateField(data.dateField || 'renewal_date');
$('#cfg_days').val(listVal(data.days) || 'mon,tue,wed,thu,fri'); $('#cfg_days').val(listVal(data.days) || 'mon,tue,wed,thu,fri');
$('#cfg_toEmails').val(listVal(data.toEmails)); $('#cfg_toEmails').val(listVal(data.toEmails));

View File

@ -2,10 +2,16 @@
-- Run manually against the application database. -- Run manually against the application database.
-- 1) policy_transaction.renewal_status -- 1) policy_transaction.renewal_status
-- Skip this ALTER if the column already exists.
ALTER TABLE `policy_transaction` ALTER TABLE `policy_transaction`
ADD COLUMN `renewal_status` VARCHAR(50) NULL DEFAULT NULL ADD COLUMN `renewal_status` VARCHAR(50) NULL DEFAULT NULL
AFTER `renewal_date`; AFTER `renewal_date`;
-- Required for multi-offset reminders (30, 15, 7, ...). Skip if the column already exists.
ALTER TABLE `policy_transaction`
ADD COLUMN `renewal_last_reminder_offset` INT NULL DEFAULT NULL
AFTER `renewal_status`;
-- 2) notifications.config_json -- 2) notifications.config_json
ALTER TABLE `notifications` ALTER TABLE `notifications`
ADD COLUMN `config_json` LONGTEXT NULL ADD COLUMN `config_json` LONGTEXT NULL
@ -26,7 +32,7 @@ SELECT
'Policy Renewal Reminder', 'Policy Renewal Reminder',
'', '',
1, 1,
'{"enabled":true,"daily":true,"days":"mon,tue,wed,thu,fri","clientTypes":[2],"dateField":"renewal_date","daysBefore":30,"includeClientEmail":true,"includeOverdue":false,"toEmails":"","ccEmails":"","bccEmails":"","fromMail":""}' '{"enabled":true,"daily":true,"days":"mon,tue,wed,thu,fri","clientTypes":[2],"dateField":"renewal_date","daysBefore":"30, 15, 7, 1, -2, -5, -10","includeClientEmail":true,"includeOverdue":false,"toEmails":"","ccEmails":"","bccEmails":"","fromMail":""}'
FROM DUAL FROM DUAL
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 SELECT 1