FIX_RENEWAL_REMIND_MAIL

This commit is contained in:
VENKATESHWARAN 2026-08-12 17:31:52 +05:30
parent e69f4b282f
commit cff90ed42e
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
# renewal_date | policy_end_date
pt.renewalReminder.dateField = renewal_date
pt.renewalReminder.daysBefore = 30
pt.renewalReminder.daysBefore = 30, 15, 7, 1, -2, -5, -10
pt.renewalReminder.includeClientEmail = true
# When true, also send for policies whose dateField is already past (overdue)
pt.renewalReminder.includeOverdue = false

View File

@ -20,7 +20,8 @@ class PtRenewalReminderConfig extends BaseConfig
/** Whitelisted: renewal_date | policy_end_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;
@ -61,7 +62,7 @@ class PtRenewalReminderConfig extends BaseConfig
'days' => env('pt.renewalReminder.days', 'mon,tue,wed,thu,fri'),
'clientTypes' => env('pt.renewalReminder.clientTypes', '2'),
'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'),
'includeOverdue' => env('pt.renewalReminder.includeOverdue', 'false'),
'toEmails' => env('pt.renewalReminder.toEmails', ''),
@ -165,7 +166,7 @@ class PtRenewalReminderConfig extends BaseConfig
}
$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->includeOverdue = $this->toBool($merged['includeOverdue'] ?? false);
@ -196,9 +197,74 @@ class PtRenewalReminderConfig extends BaseConfig
return $this->dateField;
}
/**
* First / primary offset (legacy callers).
*/
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),
'clientTypes' => $this->clientTypes,
'dateField' => $this->dateField,
'daysBefore' => $this->daysBefore,
'daysBefore' => $this->getDaysBeforeDisplay(),
'includeClientEmail' => $this->includeClientEmail,
'includeOverdue' => $this->includeOverdue,
'toEmails' => implode(',', $this->toEmails),

View File

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

View File

@ -63,6 +63,7 @@
'edate',
'renewal_date',
'renewal_status',
'renewal_last_reminder_offset',
'rollover_date',
'policy_holder_name',
'same_as_proposer',
@ -2131,15 +2132,15 @@
/**
* Candidates for policy_transaction renewal reminder cron.
*
* @param string $dateField Whitelisted column: renewal_date | policy_end_date
* @param int $daysBefore Days ahead of today to match
* @param int[] $clientTypes Client type ids (1=Group, 2=Individual)
* @param bool $includeOverdue When true, also include rows where dateField < today
* @param string $dateField Whitelisted column: renewal_date | policy_end_date
* @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 bool $includeOverdue When true, also include rows where dateField < today
* @return array
*/
public function getRenewalReminderCandidates(
string $dateField,
int $daysBefore,
$daysBefore,
array $clientTypes,
bool $includeOverdue = false
): array {
@ -2156,10 +2157,10 @@
return [];
}
$daysBefore = max(0, (int) $daysBefore);
$targetDate = date('Y-m-d', strtotime('+' . $daysBefore . ' days'));
$today = date('Y-m-d');
$column = 'pt.' . $dateField;
$offsets = $this->normalizeReminderOffsets($daysBefore);
$today = date('Y-m-d');
$targetDates = $this->reminderTargetDates($offsets, $today);
$column = 'pt.' . $dateField;
$query = $this->db->table('policy_transaction pt')
->select("
@ -2177,6 +2178,7 @@
c.common_mails,
policy_type.policy_type,
i.name AS insurer_name,
DATEDIFF({$column}, CURDATE()) AS reminder_offset,
CASE WHEN {$column} < '{$today}' THEN 1 ELSE 0 END AS is_overdue
")
->join('clients c', 'pt.client_id = c.id', 'left')
@ -2185,18 +2187,9 @@
->join('policy_type', 'pt.policy_type_id = policy_type.id', 'left')
->where('pt.is_active', 1)
->where('pt.action_type', 'inception')
->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)");
->whereIn('c.client_type', $clientTypes);
if ($includeOverdue) {
$query->groupStart()
->where($column, $targetDate)
->orWhere("{$column} <", $today)
->groupEnd();
} else {
$query->where($column, $targetDate);
}
$this->applyRenewalReminderCandidateFilters($query, $column, $targetDates, $today, $includeOverdue);
$query->groupBy('pt.id')
->orderBy('pt.id', 'DESC');
@ -2207,12 +2200,13 @@
/**
* Explain why renewal reminder candidates may be empty (for dry-run diagnostics).
*
* @param int|string|int[] $daysBefore
* @param int[] $clientTypes
* @return array<string, mixed>
*/
public function diagnoseRenewalReminderCandidates(
string $dateField,
int $daysBefore,
$daysBefore,
array $clientTypes,
bool $includeOverdue = false
): array {
@ -2225,10 +2219,11 @@
return $type > 0;
})));
$daysBefore = max(0, (int) $daysBefore);
$targetDate = date('Y-m-d', strtotime('+' . $daysBefore . ' days'));
$today = date('Y-m-d');
$column = 'pt.' . $dateField;
$offsets = $this->normalizeReminderOffsets($daysBefore);
$today = date('Y-m-d');
$targetDates = $this->reminderTargetDates($offsets, $today);
$column = 'pt.' . $dateField;
$targetLabel = implode(', ', $targetDates);
$base = function () {
return $this->db->table('policy_transaction pt')
@ -2255,25 +2250,25 @@
$countAfterStatus = (int) $base()
->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();
$countAfterNotRenewedExists = (int) $base()
->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)")
->countAllResults();
$countExactTargetDate = (int) $base()
->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($column, $targetDate)
->whereIn($column, $targetDates)
->countAllResults();
$countOverdue = (int) $base()
->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("{$column} <", $today)
->where("{$column} IS NOT NULL", null, false)
@ -2281,27 +2276,16 @@
->countAllResults();
$finalBuilder = $base()
->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) {
$finalBuilder->groupStart()
->where($column, $targetDate)
->orWhere("{$column} <", $today)
->groupEnd();
} else {
$finalBuilder->where($column, $targetDate);
}
->whereIn('c.client_type', $clientTypes);
$this->applyRenewalReminderCandidateFilters($finalBuilder, $column, $targetDates, $today, $includeOverdue);
$countFinal = (int) $finalBuilder->countAllResults();
// Sample nearest upcoming renewals (next 60 days) for debugging
$from = $today;
$to = date('Y-m-d', strtotime('+60 days'));
$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)
->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("{$column} >=", $from)
->where("{$column} <=", $to)
@ -2318,16 +2302,16 @@
if ($countAfterClientType === 0) {
$reasons[] = 'No active inception policies found for configured client type(s).';
} 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) {
$reasons[] = 'All remaining policies already have an active renewed policy linked via source_client_policy_id.';
} 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) {
$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) {
$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) {
@ -2336,14 +2320,15 @@
return [
'today' => $today,
'target_date' => $targetDate,
'target_date' => $targetDates[0] ?? null,
'target_dates' => $targetDates,
'date_field' => $dateField,
'days_before' => $daysBefore,
'days_before' => $offsets,
'include_overdue' => $includeOverdue,
'client_types' => $clientTypes,
'counts' => [
'active_inception_for_client_types' => $countAfterClientType,
'after_excluding_sent_or_renewed' => $countAfterStatus,
'after_excluding_renewed' => $countAfterStatus,
'after_excluding_already_renewed_policy' => $countAfterNotRenewedExists,
'exact_target_date_matches' => $countExactTargetDate,
'overdue_matches' => $countOverdue,
@ -2352,11 +2337,106 @@
'reasons' => $reasons,
'nearest_upcoming_sample_60_days' => $nearestSample,
'hint' => $includeOverdue
? 'Candidates = exact target 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 = any configured offset date OR overdue on selected date field.'
: "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).
*

View File

@ -261,12 +261,14 @@
<label class="custom-control-label" for="cfg_includeClientEmail">Include Client Email</label>
</div>
</div>
<!--
<div class="form-group col-md-3 col-sm-6">
<div class="custom-control custom-switch">
<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>
</div>
</div>
-->
</div>
<div class="form-row">
@ -283,11 +285,14 @@
</div>
</div>
</div>
<div class="form-group col-md-3">
<div class="form-group col-md-8">
<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 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>
<input type="text" class="form-control" id="cfg_days" name="days" placeholder="mon,tue,wed,thu,fri">
</div>
@ -478,7 +483,7 @@
$('#cfg_daily').prop('checked', boolVal(data.daily));
$('#cfg_includeClientEmail').prop('checked', boolVal(data.includeClientEmail));
$('#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');
$('#cfg_days').val(listVal(data.days) || 'mon,tue,wed,thu,fri');
$('#cfg_toEmails').val(listVal(data.toEmails));

View File

@ -2,10 +2,16 @@
-- Run manually against the application database.
-- 1) policy_transaction.renewal_status
-- Skip this ALTER if the column already exists.
ALTER TABLE `policy_transaction`
ADD COLUMN `renewal_status` VARCHAR(50) NULL DEFAULT NULL
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
ALTER TABLE `notifications`
ADD COLUMN `config_json` LONGTEXT NULL
@ -26,7 +32,7 @@ SELECT
'Policy Renewal Reminder',
'',
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
WHERE NOT EXISTS (
SELECT 1