Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cff90ed42e |
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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' => '',
|
||||
|
||||
@ -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).
|
||||
*
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user