FEAT_RETAIL_RENEWAL_REMINDER_MAIL

This commit is contained in:
VENKATESHWARAN 2026-08-12 11:39:57 +05:30
parent b8b0651b32
commit e69f4b282f
13 changed files with 4056 additions and 4 deletions

View File

@ -180,6 +180,23 @@ bds.installmentReminder.daily = true
# Comma-separated weekdays (mon,tue,wed,thu,fri,sat,sun) — used only when daily = false
bds.installmentReminder.days = mon,tue,wed,thu,fri
# Policy Transaction Renewal Reminder — used by sendRenewalReminderMail cron
pt.renewalReminder.enabled = true
pt.renewalReminder.daily = true
pt.renewalReminder.days = mon,tue,wed,thu,fri
# Client types: 1=Group, 2=Individual
pt.renewalReminder.clientTypes = 1,2
# renewal_date | policy_end_date
pt.renewalReminder.dateField = renewal_date
pt.renewalReminder.daysBefore = 30
pt.renewalReminder.includeClientEmail = true
# When true, also send for policies whose dateField is already past (overdue)
pt.renewalReminder.includeOverdue = false
pt.renewalReminder.toEmails =
pt.renewalReminder.ccEmails =
pt.renewalReminder.bccEmails =
pt.renewalReminder.fromMail =
#--------------------------------------------------------------------
# MEDI ASSIST WELLNESS SSO Configuration

View File

@ -70,7 +70,7 @@ class Filters extends BaseConfig
'HttpRequestLog' => ['except' => 'cli/*'],
'Cors',
'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','employeeRest/*','processjob','getCommission','downloadEmployeeEcardZip', 'downloadClaimFile/*', 'api/v1/*', 'cronCdLowBalanceAlert', 'sendCroneRemainderMail']],
'SecurityInputFilter' => ['except' => ['/client/notification/create','/ticket/crud_mail_template/*','test_mail','leads/sendMail', 'ticket/reply'] ],
'SecurityInputFilter' => ['except' => ['/client/notification/create','/ticket/crud_mail_template/*','test_mail','leads/sendMail', 'ticket/reply', 'policy_tranction/retail-policy/template', 'policy_tranction/retail-policy/send-test-mail'] ],
'GlobalPostFileUploadGuard'
// 'csrf',
// 'invalidchars',

View File

@ -0,0 +1,321 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use Config\Database;
class PtRenewalReminderConfig extends BaseConfig
{
public bool $enabled = false;
public bool $daily = false;
/** @var string[] Lowercase three-letter day abbreviations, e.g. mon, tue */
public array $days = [];
/** @var int[] Client type ids: 1=Group, 2=Individual */
public array $clientTypes = [];
/** Whitelisted: renewal_date | policy_end_date */
public string $dateField = 'renewal_date';
public int $daysBefore = 30;
public bool $includeClientEmail = false;
/** When true, also include policies whose dateField is already past */
public bool $includeOverdue = false;
/** @var string[] */
public array $toEmails = [];
/** @var string[] */
public array $ccEmails = [];
/** @var string[] */
public array $bccEmails = [];
public string $fromMail = '';
/** Loaded from notifications.subject when present */
public string $mailSubject = '';
/** Loaded from notifications.mail_content when present */
public string $mailContent = '';
public ?int $notificationId = null;
private const ALLOWED_DATE_FIELDS = ['renewal_date', 'policy_end_date'];
public const TEMPLATE_NAME = 'retail_reminder_mail';
public function __construct()
{
parent::__construct();
// Env defaults (seed / fallback)
$defaults = [
'enabled' => env('pt.renewalReminder.enabled', 'true'),
'daily' => env('pt.renewalReminder.daily', 'true'),
'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),
'includeClientEmail' => env('pt.renewalReminder.includeClientEmail', 'true'),
'includeOverdue' => env('pt.renewalReminder.includeOverdue', 'false'),
'toEmails' => env('pt.renewalReminder.toEmails', ''),
'ccEmails' => env('pt.renewalReminder.ccEmails', ''),
'bccEmails' => env('pt.renewalReminder.bccEmails', ''),
'fromMail' => env('pt.renewalReminder.fromMail', ''),
];
$dbConfig = $this->loadConfigFromNotification();
$merged = array_merge($defaults, $dbConfig);
$this->applyMergedConfig($merged);
}
/**
* @return array<string, mixed>
*/
private function loadConfigFromNotification(): array
{
try {
$db = Database::connect();
$row = $db->table('notifications')
->where('template_name', self::TEMPLATE_NAME)
->groupStart()
->where('client_id', null)
->orWhere('client_id', 0)
->orWhere('client_id', '')
->groupEnd()
->orderBy('id', 'DESC')
->get()
->getRowArray();
if (empty($row)) {
log_message('error', '[PtRenewalReminderConfig] No notifications row for retail_reminder_mail; using env defaults');
return [];
}
$this->notificationId = isset($row['id']) ? (int) $row['id'] : null;
$this->mailSubject = trim((string) ($row['subject'] ?? ''));
$this->mailContent = (string) ($row['mail_content'] ?? '');
$json = trim((string) ($row['config_json'] ?? ''));
if ($json === '') {
log_message('error', '[PtRenewalReminderConfig] config_json empty for notification_id=' . ($this->notificationId ?? 'n/a'));
return [];
}
$decoded = json_decode($json, true);
if (! is_array($decoded)) {
log_message('error', '[PtRenewalReminderConfig] Invalid config_json for notification_id=' . ($this->notificationId ?? 'n/a'));
return [];
}
return $decoded;
} catch (\Throwable $e) {
log_message(
'error',
'[PtRenewalReminderConfig] Failed loading notification config: ' . $e->getMessage()
);
return [];
}
}
/**
* @param array<string, mixed> $merged
*/
private function applyMergedConfig(array $merged): void
{
$this->enabled = $this->toBool($merged['enabled'] ?? false);
$this->daily = $this->toBool($merged['daily'] ?? false);
$daysRaw = $merged['days'] ?? 'mon,tue,wed,thu,fri';
if (is_array($daysRaw)) {
$daysRaw = implode(',', $daysRaw);
}
$this->days = array_values(array_filter(array_map(
static fn (string $day): string => strtolower(substr(trim($day), 0, 3)),
explode(',', (string) $daysRaw)
)));
$clientTypesRaw = $merged['clientTypes'] ?? '2';
if (is_array($clientTypesRaw)) {
$this->clientTypes = array_values(array_unique(array_filter(array_map(
static fn ($type): int => (int) $type,
$clientTypesRaw
), static fn (int $type): bool => $type > 0)));
} else {
$this->clientTypes = array_values(array_unique(array_filter(array_map(
static fn (string $type): int => (int) trim($type),
explode(',', (string) $clientTypesRaw)
), static fn (int $type): bool => $type > 0)));
}
$dateField = strtolower(trim((string) ($merged['dateField'] ?? 'renewal_date')));
if (! in_array($dateField, self::ALLOWED_DATE_FIELDS, true)) {
log_message(
'error',
'[PtRenewalReminderConfig] Invalid dateField "' . $dateField . '", falling back to renewal_date'
);
$dateField = 'renewal_date';
}
$this->dateField = $dateField;
$this->daysBefore = max(0, (int) ($merged['daysBefore'] ?? 30));
$this->includeClientEmail = $this->toBool($merged['includeClientEmail'] ?? true);
$this->includeOverdue = $this->toBool($merged['includeOverdue'] ?? false);
$this->toEmails = $this->parseEmailList($merged['toEmails'] ?? '');
$this->ccEmails = $this->parseEmailList($merged['ccEmails'] ?? '');
$this->bccEmails = $this->parseEmailList($merged['bccEmails'] ?? '');
$this->fromMail = trim((string) ($merged['fromMail'] ?? ''));
}
public function shouldRunToday(): bool
{
if (! $this->enabled) {
return false;
}
if ($this->daily) {
return true;
}
$today = strtolower(date('D'));
return in_array($today, $this->days, true);
}
public function getDateField(): string
{
return $this->dateField;
}
public function getDaysBefore(): int
{
return $this->daysBefore;
}
/**
* @return int[]
*/
public function getClientTypes(): array
{
return $this->clientTypes;
}
/**
* @return string[]
*/
public function getToEmails(): array
{
return $this->toEmails;
}
/**
* @return string[]
*/
public function getCcEmails(): array
{
return $this->ccEmails;
}
/**
* @return string[]
*/
public function getBccEmails(): array
{
return $this->bccEmails;
}
public function getFromMail(): string
{
return $this->fromMail;
}
public function includeClientEmail(): bool
{
return $this->includeClientEmail;
}
public function includeOverdue(): bool
{
return $this->includeOverdue;
}
public function getMailSubject(): string
{
return $this->mailSubject;
}
public function getMailContent(): string
{
return $this->mailContent;
}
/**
* Flatten config for UI / API response.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'enabled' => $this->enabled,
'daily' => $this->daily,
'days' => implode(',', $this->days),
'clientTypes' => $this->clientTypes,
'dateField' => $this->dateField,
'daysBefore' => $this->daysBefore,
'includeClientEmail' => $this->includeClientEmail,
'includeOverdue' => $this->includeOverdue,
'toEmails' => implode(',', $this->toEmails),
'ccEmails' => implode(',', $this->ccEmails),
'bccEmails' => implode(',', $this->bccEmails),
'fromMail' => $this->fromMail,
];
}
/**
* @return string[]
*/
private function parseEmailList(mixed $value): array
{
if ($value === null || $value === '') {
return [];
}
if (is_array($value)) {
$emails = array_map('trim', $value);
} else {
$emails = array_map('trim', explode(',', (string) $value));
}
$unique = [];
foreach ($emails as $email) {
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
continue;
}
$key = strtolower($email);
if (! isset($unique[$key])) {
$unique[$key] = $email;
}
}
return array_values($unique);
}
private function toBool(mixed $value): bool
{
if (is_bool($value)) {
return $value;
}
return in_array(strtolower((string) $value), ['1', 'true', 'yes', 'on'], true);
}
}

View File

@ -541,6 +541,8 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) {
$routes->get("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
$routes->cli("cli/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
$routes->get("policy_tranction/sendRenewalReminderMail","PolicyTransactionController::sendRenewalReminderMail");
$routes->cli("cli/sendRenewalReminderMail","PolicyTransactionController::sendRenewalReminderMail");
$routes->cli("cli/cronDailyBDSReport", "PolicyTransactionController::cronDailyBDSReport");
$routes->cli('cli/croneDailyActivityReport', 'DashboardController::croneDailyActivityReport');
@ -561,6 +563,17 @@ $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) {
$routes->post("getMoreInfo","PolicyTransactionController::getMoreInfo");
$routes->post("saveInstallment","PolicyTransactionController::saveInstallment");
$routes->group("retail-policy", ["filter" => "authMVC"], function ($routes) {
$routes->get('list', 'PolicyTransactionController::retailPolicyList');
$routes->post('list-data', 'PolicyTransactionController::retailPolicyListData');
$routes->post('mark-renewed', 'PolicyTransactionController::markRetailPolicyRenewed');
$routes->get('config', 'PolicyTransactionController::getRetailReminderConfig');
$routes->post('config', 'PolicyTransactionController::saveRetailReminderConfig');
$routes->get('template', 'PolicyTransactionController::getRetailReminderTemplate');
$routes->post('template', 'PolicyTransactionController::saveRetailReminderTemplate');
$routes->post('send-test-mail', 'PolicyTransactionController::sendRetailReminderTestMail');
});
$routes->group("inception", ["filter" => "authMVC"], function ($routes) {
$routes->match(['get', 'post'], 'list', 'PolicyTransactionController::viewInception');
$routes->post('list/datatable', 'PolicyTransactionController::inceptionListDataTable');

File diff suppressed because it is too large Load Diff

View File

@ -8,5 +8,5 @@ class NotificationModel extends Model
{
protected $table = 'notifications';
protected $primaryKey = 'id';
protected $allowedFields = ["id","client_id","template_name","subject","mail_content","enabled","created_by","updated_by","is_active",'mail_content_json','common_mail'];
protected $allowedFields = ["id","client_id","template_name","subject","mail_content","enabled","created_by","updated_by","is_active",'mail_content_json','common_mail','config_json'];
}

View File

@ -62,6 +62,7 @@
'etat_band',
'edate',
'renewal_date',
'renewal_status',
'rollover_date',
'policy_holder_name',
'same_as_proposer',
@ -2126,6 +2127,311 @@
// print($this->db->getLastQuery()); die;
return $results;
}
/**
* 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
* @return array
*/
public function getRenewalReminderCandidates(
string $dateField,
int $daysBefore,
array $clientTypes,
bool $includeOverdue = false
): array {
$allowedDateFields = ['renewal_date', 'policy_end_date'];
if (! in_array($dateField, $allowedDateFields, true)) {
$dateField = 'renewal_date';
}
$clientTypes = array_values(array_unique(array_filter(array_map('intval', $clientTypes), static function ($type) {
return $type > 0;
})));
if (empty($clientTypes)) {
return [];
}
$daysBefore = max(0, (int) $daysBefore);
$targetDate = date('Y-m-d', strtotime('+' . $daysBefore . ' days'));
$today = date('Y-m-d');
$column = 'pt.' . $dateField;
$query = $this->db->table('policy_transaction pt')
->select("
pt.id,
pt.policy_no,
pt.client_id,
pt.client_branch_id,
pt.policy_start_date,
pt.policy_end_date,
pt.renewal_date,
c.client_name,
c.short_name AS client_short_name,
c.client_type,
c.email AS client_email,
c.common_mails,
policy_type.policy_type,
i.name AS insurer_name,
CASE WHEN {$column} < '{$today}' THEN 1 ELSE 0 END AS is_overdue
")
->join('clients c', 'pt.client_id = c.id', 'left')
->join('pt_co_share_details pt_co', 'pt.id = pt_co.pt_id and pt_co.co_share_type = 1', 'left')
->join('insurers i', 'pt_co.insurer_id = i.id', 'left')
->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)");
if ($includeOverdue) {
$query->groupStart()
->where($column, $targetDate)
->orWhere("{$column} <", $today)
->groupEnd();
} else {
$query->where($column, $targetDate);
}
$query->groupBy('pt.id')
->orderBy('pt.id', 'DESC');
return $query->get()->getResultArray();
}
/**
* Explain why renewal reminder candidates may be empty (for dry-run diagnostics).
*
* @param int[] $clientTypes
* @return array<string, mixed>
*/
public function diagnoseRenewalReminderCandidates(
string $dateField,
int $daysBefore,
array $clientTypes,
bool $includeOverdue = false
): array {
$allowedDateFields = ['renewal_date', 'policy_end_date'];
if (! in_array($dateField, $allowedDateFields, true)) {
$dateField = 'renewal_date';
}
$clientTypes = array_values(array_unique(array_filter(array_map('intval', $clientTypes), static function ($type) {
return $type > 0;
})));
$daysBefore = max(0, (int) $daysBefore);
$targetDate = date('Y-m-d', strtotime('+' . $daysBefore . ' days'));
$today = date('Y-m-d');
$column = 'pt.' . $dateField;
$base = function () {
return $this->db->table('policy_transaction pt')
->join('clients c', 'pt.client_id = c.id', 'left')
->where('pt.is_active', 1)
->where('pt.action_type', 'inception');
};
$countRetailActive = 0;
$countAfterClientType = 0;
$countAfterStatus = 0;
$countAfterNotRenewedExists = 0;
$countExactTargetDate = 0;
$countOverdue = 0;
$countFinal = 0;
$nearestSample = [];
if (! empty($clientTypes)) {
$countRetailActive = (int) $base()
->whereIn('c.client_type', $clientTypes)
->countAllResults();
$countAfterClientType = $countRetailActive;
$countAfterStatus = (int) $base()
->whereIn('c.client_type', $clientTypes)
->where("IFNULL(pt.renewal_status, '') NOT IN ('renewal_mail_send', 'renewed', 'reminded')", null, false)
->countAllResults();
$countAfterNotRenewedExists = (int) $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)")
->countAllResults();
$countExactTargetDate = (int) $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)")
->where($column, $targetDate)
->countAllResults();
$countOverdue = (int) $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)")
->where("{$column} <", $today)
->where("{$column} IS NOT NULL", null, false)
->where("{$column} !=", '0000-00-00')
->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);
}
$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")
->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)")
->where("{$column} >=", $from)
->where("{$column} <=", $to)
->orderBy($column, 'ASC')
->limit(10)
->get()
->getResultArray();
}
$reasons = [];
if (empty($clientTypes)) {
$reasons[] = 'No clientTypes configured.';
}
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.';
} 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.";
if ($countOverdue > 0) {
$reasons[] = "There are {$countOverdue} overdue policies that would be included if includeOverdue=true.";
}
} elseif ($includeOverdue && $countFinal === 0) {
$reasons[] = "No policies match target date {$targetDate} or overdue (< {$today}) on {$dateField}.";
}
if (empty($reasons) && $countFinal === 0) {
$reasons[] = 'No candidates matched the current filters.';
}
return [
'today' => $today,
'target_date' => $targetDate,
'date_field' => $dateField,
'days_before' => $daysBefore,
'include_overdue' => $includeOverdue,
'client_types' => $clientTypes,
'counts' => [
'active_inception_for_client_types' => $countAfterClientType,
'after_excluding_sent_or_renewed' => $countAfterStatus,
'after_excluding_already_renewed_policy' => $countAfterNotRenewedExists,
'exact_target_date_matches' => $countExactTargetDate,
'overdue_matches' => $countOverdue,
'final_candidates' => $countFinal,
],
'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.",
];
}
/**
* Server-side DataTables list for Retail Policy (individual clients only).
*
* @param int $draw
* @param int $start
* @param int $length
* @param string $search
* @return array{draw:int,recordsTotal:int,recordsFiltered:int,data:array}
*/
public function getRetailPolicyList(int $draw, int $start, int $length, string $search = ''): array
{
$exportAll = $length < 0;
$length = $exportAll ? 0 : ($length > 0 ? $length : 10);
$start = max(0, $start);
$search = trim($search);
$applyBase = static function ($builder) {
return $builder->join('clients c', 'pt.client_id = c.id', 'left')
->join('policy_type', 'pt.policy_type_id = policy_type.id', 'left')
->where('pt.is_active', 1)
->where('pt.action_type', 'inception')
->where('c.client_type', 2)
->where("IFNULL(pt.renewal_status, '') !=", 'renewed');
};
$applySearch = static function ($builder) use ($search) {
if ($search !== '') {
$builder->groupStart()
->like('pt.policy_no', $search)
->orLike('policy_type.policy_type', $search)
->orLike('c.client_name', $search)
->orLike('c.phone', $search)
->orLike('c.email', $search)
->groupEnd();
}
return $builder;
};
$totalBuilder = $applyBase($this->db->table('policy_transaction pt'));
$recordsTotal = (int) $totalBuilder->countAllResults();
$filteredBuilder = $applySearch($applyBase($this->db->table('policy_transaction pt')));
$recordsFiltered = (int) $filteredBuilder->countAllResults();
$dataBuilder = $applySearch($applyBase($this->db->table('policy_transaction pt')));
$dataBuilder = $dataBuilder
->select('
pt.id,
pt.policy_no,
pt.renewal_date,
pt.policy_start_date,
pt.policy_end_date,
pt.renewal_status,
pt.client_id,
c.client_name,
c.phone AS client_mobile,
c.email AS client_email,
policy_type.policy_type
')
->orderBy("CASE WHEN LOWER(IFNULL(pt.renewal_status, '')) IN ('renewal_mail_send', 'reminded') THEN 0 ELSE 1 END", 'ASC', false)
->orderBy('pt.id', 'DESC');
if (! $exportAll) {
$dataBuilder->limit($length, $start);
}
$rows = $dataBuilder->get()->getResultArray();
return [
'draw' => $draw,
'recordsTotal' => $recordsTotal,
'recordsFiltered' => $recordsFiltered,
'data' => $rows,
];
}
public function getRenewalReportDataOld($start_date = null, $end_date = null, $client_type = null, $client = null, $issuer = null)
{

View File

@ -2276,6 +2276,13 @@ body[data-sidebar-size="condensed"] .footer {
<?php } ?>
<li>
<a href="<?= base_url('/policy_tranction/retail-policy/list') ?>">
<i class="ri-user-line"></i>
<span> Retail Policy</span>
</a>
</li>
<!-- This Menu Hide because of Role Merge REF : SVM,SVR,KV.
<?php if ((get_role_id() == 1 || get_role_id() == 5) || (in_array(MANAGEMENT_TEAM_ID, user_team()) || in_array(FINANCE_TEAM_ID, user_team()) || in_array(BUSINESS_TEAM_ID, user_team()))) { ?>
<li>

View File

@ -524,6 +524,9 @@
return;
}
$('#gpa_si_add_more').empty();
$('.removeDom').remove();
setTimeout(function() {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
@ -567,7 +570,8 @@
$('#police-tab').css('display', 'none');
$('.nav.nav-pills.navtab-bg').css('display', 'none');
$('.nav.nav-pills.navtab-bg').prev().css('display', 'none');
$('.removeDom').remove()
$('.removeDom').remove();
$('#gpa_si_add_more').empty();
$('.loader').fadeIn();
$('.loader-mask').fadeIn();
@ -687,7 +691,9 @@
if (Array.isArray(gpaJsonObjectForSpecialCondition[key])) {
gpaJsonObjectForSpecialCondition[key].forEach((value,
index) => {
appendGPASIAddMore(value);
if (value !== null && value !== undefined && String(value).trim() !== '') {
appendGPASIAddMore(value);
}
});
} else {
console.error(`${key} is not an array.`);
@ -892,6 +898,8 @@
$('.nav.nav-pills.navtab-bg').css('display', '');
$('.nav.nav-pills.navtab-bg').prev().css('display', '');
$('#policyGPATerms')[0].reset();
$('#gpa_si_add_more').empty();
$('.removeDom').remove();
$('.text-danger-2').html('');
});

View File

@ -0,0 +1,223 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f7f6;
color: #333;
-webkit-text-size-adjust: 100%;
}
.container {
width: 100%;
max-width: 640px;
margin: 0 auto;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.06);
}
.header {
background: linear-gradient(135deg, #1f618d, #21618c, #117a65);
color: #ffffff;
padding: 24px 20px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 22px;
letter-spacing: 0.3px;
}
.header-subtitle {
margin-top: 6px;
font-size: 13px;
opacity: 0.92;
}
.content {
padding: 24px 20px;
}
.alert-banner {
padding: 14px 16px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
line-height: 1.5;
background: #eaf4fb;
border-left: 4px solid #2980b9;
color: #1f4e79;
}
.intro {
font-size: 14px;
line-height: 1.7;
color: #2c3e50;
margin: 0 0 18px;
}
.section-title {
font-size: 16px;
color: #2c3e50;
border-bottom: 2px solid #3498db;
display: inline-block;
margin: 0 0 12px;
padding-bottom: 4px;
}
.details-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.details-table td {
padding: 10px 12px;
border-bottom: 1px solid #ecf0f1;
font-size: 13px;
vertical-align: top;
}
.details-table td.label {
width: 38%;
color: #7f8c8d;
font-weight: 600;
background: #f8f9fa;
}
.details-table td.value {
color: #2c3e50;
}
.next-steps {
background: #f8f9fa;
border-radius: 6px;
padding: 14px 16px;
margin-top: 8px;
}
.next-steps h4 {
margin: 0 0 8px;
font-size: 14px;
color: #2c3e50;
}
.next-steps ul {
margin: 0;
padding-left: 18px;
font-size: 13px;
line-height: 1.7;
color: #566573;
}
.note {
font-size: 12px;
color: #7f8c8d;
line-height: 1.6;
margin-top: 16px;
font-style: italic;
}
.footer {
background: #f8f9fa;
padding: 14px 18px;
text-align: center;
font-size: 11px;
color: #95a5a6;
line-height: 1.5;
}
@media only screen and (max-width: 480px) {
.content {
padding: 16px 14px;
}
.header h1 {
font-size: 19px;
}
.details-table td.label {
width: 42%;
}
}
</style>
</head>
<body>
<div class="container" style="width:100%;max-width:640px;margin:0 auto;background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 4px 10px rgba(0,0,0,0.06);">
<div class="header" style="background:#1f618d;color:#ffffff;padding:24px 20px;text-align:center;">
<h1>Policy Renewal Reminder</h1>
<div class="header-subtitle">Nhance India Insurance</div>
</div>
<div class="content" style="padding:24px 20px;">
<div class="alert-banner" style="padding:14px 16px;border-radius:6px;margin-bottom:20px;font-size:14px;line-height:1.5;background:<?= !empty($is_overdue) ? '#fdecea' : '#eaf4fb' ?>;border-left:4px solid <?= !empty($is_overdue) ? '#c0392b' : '#2980b9' ?>;color:<?= !empty($is_overdue) ? '#922b21' : '#1f4e79' ?>;">
<?php if (!empty($is_overdue)): ?>
<strong>Overdue Renewal:</strong> The policy for <strong><?= esc($client_name ?? '') ?></strong>
was due for renewal on <strong><?= esc($reminder_date ?? '') ?></strong>. Please renew at the earliest.
<?php else: ?>
<strong>Renewal Reminder:</strong> The policy for <strong><?= esc($client_name ?? '') ?></strong>
is due for renewal on <strong><?= esc($reminder_date ?? '') ?></strong>
(in <strong><?= esc((string) ($days_before ?? '')) ?></strong> day(s)).
<?php endif; ?>
</div>
<p class="intro" style="font-size:14px;line-height:1.7;color:#2c3e50;margin:0 0 18px;">
This is a reminder that a policy renewal is <?= !empty($is_overdue) ? 'overdue' : 'upcoming' ?> for
<strong><?= esc($client_name ?? '') ?></strong>.
Please review the details below and initiate the renewal process in time to ensure uninterrupted coverage.
</p>
<h3 class="section-title" style="font-size:16px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:0 0 12px;padding-bottom:4px;">
Policy Details
</h3>
<table class="details-table" style="width:100%;border-collapse:collapse;margin-bottom:20px;">
<tr>
<td class="label" style="width:38%;padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#7f8c8d;font-weight:600;background:#f8f9fa;">Client</td>
<td class="value" style="padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#2c3e50;"><?= esc($client_name ?? '-') ?></td>
</tr>
<tr>
<td class="label" style="width:38%;padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#7f8c8d;font-weight:600;background:#f8f9fa;">Client Type</td>
<td class="value" style="padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#2c3e50;"><?= esc($client_type_label ?? '-') ?></td>
</tr>
<tr>
<td class="label" style="width:38%;padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#7f8c8d;font-weight:600;background:#f8f9fa;">Policy Number</td>
<td class="value" style="padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#2c3e50;"><?= esc($policy_no ?? 'Not Assigned') ?></td>
</tr>
<?php if (!empty($policy_type)): ?>
<tr>
<td class="label" style="width:38%;padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#7f8c8d;font-weight:600;background:#f8f9fa;">Policy Type</td>
<td class="value" style="padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#2c3e50;"><?= esc($policy_type) ?></td>
</tr>
<?php endif; ?>
<?php if (!empty($insurer_name)): ?>
<tr>
<td class="label" style="width:38%;padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#7f8c8d;font-weight:600;background:#f8f9fa;">Insurance Company</td>
<td class="value" style="padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#2c3e50;"><?= esc($insurer_name) ?></td>
</tr>
<?php endif; ?>
<?php if (!empty($policy_period)): ?>
<tr>
<td class="label" style="width:38%;padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#7f8c8d;font-weight:600;background:#f8f9fa;">Policy Period</td>
<td class="value" style="padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#2c3e50;"><?= esc($policy_period) ?></td>
</tr>
<?php endif; ?>
<tr>
<td class="label" style="width:38%;padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#7f8c8d;font-weight:600;background:#f8f9fa;">Renewal / End Date</td>
<td class="value" style="padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#2c3e50;"><strong><?= esc($reminder_date ?? '-') ?></strong></td>
</tr>
<tr>
<td class="label" style="width:38%;padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#7f8c8d;font-weight:600;background:#f8f9fa;">Days Before</td>
<td class="value" style="padding:10px 12px;border-bottom:1px solid #ecf0f1;font-size:13px;color:#2c3e50;"><?= esc((string) ($days_before ?? '-')) ?></td>
</tr>
</table>
<div class="next-steps" style="background:#f8f9fa;border-radius:6px;padding:14px 16px;margin-top:8px;">
<h4 style="margin:0 0 8px;font-size:14px;color:#2c3e50;">Next Steps</h4>
<ul style="margin:0;padding-left:18px;font-size:13px;line-height:1.7;color:#566573;">
<li>Please initiate the renewal process before <strong><?= esc($reminder_date ?? '') ?></strong>.</li>
<li>Confirm coverage requirements and share any updated details with the concerned team.</li>
<li>For clarification regarding the renewal, please coordinate with your Nhance contact.</li>
</ul>
</div>
<p class="note" style="font-size:12px;color:#7f8c8d;line-height:1.6;margin-top:16px;font-style:italic;">
Timely renewal helps ensure continuous insurance coverage. Thank you for your cooperation.
</p>
</div>
<div class="footer" style="background:#f8f9fa;padding:14px 18px;text-align:center;font-size:11px;color:#95a5a6;line-height:1.5;">
This is an automated notification from Nhance India Insurance. Please do not reply to this email.
<br>Generated on <?= esc($generated_on ?? date('d-m-Y H:i')) ?>
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,612 @@
-- =============================================================================
-- NHANCE DB changes: 2026-07-01 through 2026-08-12
-- Consolidated from app/Database/*.sql and Migrations (chronological).
-- Safe-ish to re-run where noted (IF NOT EXISTS / information_schema checks).
-- Demo seed (digit_motor_demo_seed.sql) omitted — schema only + retail reminder seed.
-- =============================================================================
-- -----------------------------------------------------------------------------
-- 2026-07-06 | Insurer claim form columns
-- Source: app/Database/insurers_add_claim_form_column.sql
-- -----------------------------------------------------------------------------
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'insurers'
AND column_name = 'insurer_claim_form'
);
SET @sql := IF(@col = 0,
'ALTER TABLE insurers ADD COLUMN insurer_claim_form VARCHAR(255) NULL DEFAULT NULL AFTER insurer_logo',
'SELECT ''insurers.insurer_claim_form already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'insurers'
AND column_name = 'insurer_claim_form_original_name'
);
SET @sql := IF(@col = 0,
'ALTER TABLE insurers ADD COLUMN insurer_claim_form_original_name VARCHAR(255) NULL DEFAULT NULL AFTER insurer_claim_form',
'SELECT ''insurers.insurer_claim_form_original_name already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- -----------------------------------------------------------------------------
-- 2026-07-13 | BDS report performance indexes
-- Source: app/Database/bds_report_performance_indexes.sql
-- -----------------------------------------------------------------------------
SET @idx := (
SELECT COUNT(1) FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'policy_transaction'
AND index_name = 'idx_pt_active_created'
);
SET @sql := IF(@idx = 0,
'CREATE INDEX idx_pt_active_created ON policy_transaction (is_active, created_at)',
'SELECT ''idx_pt_active_created already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @idx := (
SELECT COUNT(1) FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'pt_co_share_details'
AND index_name = 'idx_pcsd_pt_active'
);
SET @sql := IF(@idx = 0,
'CREATE INDEX idx_pcsd_pt_active ON pt_co_share_details (pt_id, is_active)',
'SELECT ''idx_pcsd_pt_active already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @idx := (
SELECT COUNT(1) FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'co_share_stmt_details'
AND index_name = 'idx_cssd_coshare_stmt_active'
);
SET @sql := IF(@idx = 0,
'CREATE INDEX idx_cssd_coshare_stmt_active ON co_share_stmt_details (co_share_id, statement_id, is_active)',
'SELECT ''idx_cssd_coshare_stmt_active already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @idx := (
SELECT COUNT(1) FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'insurer_statements'
AND index_name = 'idx_insq_active_month_invoice'
);
SET @sql := IF(@idx = 0,
'CREATE INDEX idx_insq_active_month_invoice ON insurer_statements (is_active, month, invoice_status)',
'SELECT ''idx_insq_active_month_invoice already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- -----------------------------------------------------------------------------
-- 2026-07-21 / 2026-07-23 / 2026-07-30 | Digit Motor module tables (final schema)
-- Sources: digit_motor_tables.sql, digit_motor_master_tables.sql
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `motor_token` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`environment` VARCHAR(20) NOT NULL DEFAULT 'staging',
`access_token` TEXT NOT NULL,
`refresh_token` TEXT,
`expires_at` TIMESTAMP NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uq_motor_token_env` (`environment`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_quote` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`enquiry_id` VARCHAR(64) NOT NULL,
`quote_number` VARCHAR(32) DEFAULT NULL,
`application_id` VARCHAR(255) DEFAULT NULL,
`policy_holder_type` VARCHAR(20) NOT NULL DEFAULT 'INDIVIDUAL',
`insurance_product_code` VARCHAR(10) NOT NULL,
`sub_insurance_product_code` VARCHAR(10) NOT NULL DEFAULT 'PB',
`previous_insurer_code` SMALLINT DEFAULT NULL,
`previous_policy_expiry_date` DATE DEFAULT NULL,
`external_policy_number` VARCHAR(32) DEFAULT NULL,
`is_ncb_transfer` TINYINT(1) DEFAULT 0,
`start_date` DATE DEFAULT NULL,
`end_date` DATE DEFAULT NULL,
`pincode` VARCHAR(6) NOT NULL,
`coverage_details` JSON DEFAULT NULL,
`policyholder_details` JSON DEFAULT NULL,
`premium` DECIMAL(12,2) DEFAULT NULL,
`idv` DECIMAL(12,2) DEFAULT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
`created_by` INT DEFAULT NULL,
`updated_by` INT DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uq_motor_quote_enquiry` (`enquiry_id`),
KEY `idx_motor_quote_status` (`status`),
KEY `idx_motor_quote_quote_number` (`quote_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_vehicle` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`quote_id` BIGINT NOT NULL,
`is_vehicle_new` TINYINT(1) NOT NULL DEFAULT 0,
`vehicle_maincode` VARCHAR(30) NOT NULL,
`license_plate_number` VARCHAR(12) NOT NULL,
`vehicle_identification_number` VARCHAR(30) DEFAULT NULL,
`engine_number` VARCHAR(30) DEFAULT NULL,
`manufacture_date` DATE NOT NULL,
`registration_date` DATE NOT NULL,
`registration_authority` VARCHAR(10) DEFAULT NULL,
`idv` DECIMAL(12,2) DEFAULT NULL,
`default_idv` DECIMAL(12,2) DEFAULT NULL,
`minimum_idv` DECIMAL(12,2) DEFAULT NULL,
`maximum_idv` DECIMAL(12,2) DEFAULT NULL,
UNIQUE KEY `uq_motor_vehicle_quote` (`quote_id`),
CONSTRAINT `fk_motor_vehicle_quote` FOREIGN KEY (`quote_id`) REFERENCES `motor_quote`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_kyc` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`quote_id` BIGINT NOT NULL,
`kyc_id` VARCHAR(64) DEFAULT NULL,
`kyc_verification_status` VARCHAR(20) DEFAULT NULL,
`reference_id` VARCHAR(64) DEFAULT NULL,
`link` VARCHAR(512) DEFAULT NULL,
`mismatch_type` VARCHAR(40) DEFAULT NULL,
`id_verification_doc_type` VARCHAR(40) DEFAULT NULL,
`address_verification_doc_type` VARCHAR(40) DEFAULT NULL,
`mode` CHAR(1) DEFAULT 'O',
`checked_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY `idx_motor_kyc_quote` (`quote_id`),
CONSTRAINT `fk_motor_kyc_quote` FOREIGN KEY (`quote_id`) REFERENCES `motor_quote`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_payment` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`quote_id` BIGINT NOT NULL,
`application_id` VARCHAR(255) NOT NULL,
`digit_payment_id` VARCHAR(64) DEFAULT NULL,
`request_reference` VARCHAR(64) DEFAULT NULL,
`payment_mode` VARCHAR(5) DEFAULT 'EB',
`cancel_return_url` VARCHAR(512) DEFAULT NULL,
`success_return_url` VARCHAR(512) DEFAULT NULL,
`dispatcher_response` VARCHAR(512) DEFAULT NULL,
`premium` DECIMAL(12,2) DEFAULT NULL,
`payment_status` VARCHAR(20) DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY `idx_motor_payment_quote` (`quote_id`),
CONSTRAINT `fk_motor_payment_quote` FOREIGN KEY (`quote_id`) REFERENCES `motor_quote`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_policy` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`quote_id` BIGINT NOT NULL,
`policy_number` VARCHAR(32) DEFAULT NULL,
`policy_status` VARCHAR(20) DEFAULT NULL,
`schedule_path` VARCHAR(512) DEFAULT NULL,
`proposal_path` VARCHAR(512) DEFAULT NULL,
`response_code` VARCHAR(10) DEFAULT NULL,
`response_message` VARCHAR(255) DEFAULT NULL,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uq_motor_policy_quote` (`quote_id`),
CONSTRAINT `fk_motor_policy_quote` FOREIGN KEY (`quote_id`) REFERENCES `motor_quote`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_api_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`quote_id` BIGINT DEFAULT NULL,
`integration_id` VARCHAR(20) DEFAULT NULL,
`endpoint` VARCHAR(120) DEFAULT NULL,
`request_body` JSON DEFAULT NULL,
`response_body` JSON DEFAULT NULL,
`http_status` SMALLINT DEFAULT NULL,
`error_code` VARCHAR(10) DEFAULT NULL,
`duration_ms` INT DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY `idx_motor_api_log_quote` (`quote_id`),
KEY `idx_motor_api_log_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Digit Motor master / lookup tables
CREATE TABLE IF NOT EXISTS `motor_master_vehicle` (
`vehicle_code` VARCHAR(30) NOT NULL,
`make` VARCHAR(80) NOT NULL,
`model` VARCHAR(120) NOT NULL,
`variant` VARCHAR(120) DEFAULT NULL,
`body_type` VARCHAR(60) DEFAULT NULL,
`seating_capacity` SMALLINT DEFAULT NULL,
`power` DECIMAL(10,2) DEFAULT NULL,
`cubic_capacity` DECIMAL(10,2) DEFAULT NULL,
`gross_vehicle_weight` DECIMAL(12,2) DEFAULT NULL,
`fuel_type` VARCHAR(40) DEFAULT NULL,
`no_of_wheels` TINYINT DEFAULT NULL,
`abs` CHAR(1) DEFAULT NULL,
`air_bags` SMALLINT DEFAULT NULL,
`length_m` DECIMAL(10,3) DEFAULT NULL,
`ex_showroom_price` DECIMAL(14,2) DEFAULT NULL,
`price_year` SMALLINT DEFAULT NULL,
`production_status` VARCHAR(60) DEFAULT NULL,
`manufacturing` VARCHAR(40) DEFAULT NULL,
`vehicle_type` VARCHAR(40) DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`vehicle_code`),
KEY `idx_mmv_make` (`make`),
KEY `idx_mmv_make_model` (`make`, `model`),
KEY `idx_mmv_make_model_variant` (`make`, `model`, `variant`),
KEY `idx_mmv_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_previous_insurer` (
`insurer_code` VARCHAR(10) NOT NULL,
`insurer_name` VARCHAR(180) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`insurer_code`),
KEY `idx_mmpi_name` (`insurer_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_product` (
`product_code` VARCHAR(10) NOT NULL,
`product_name` VARCHAR(120) NOT NULL,
`vehicle_class` VARCHAR(10) DEFAULT NULL COMMENT '2W / 4W / CV',
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`product_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_sub_product` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`business_type` VARCHAR(20) NOT NULL COMMENT 'NEW / ROLLOVER',
`product_label` VARCHAR(120) NOT NULL,
`sub_product_code` VARCHAR(20) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uq_mmsp` (`business_type`, `product_label`, `sub_product_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_pincode` (
`pincode` VARCHAR(6) NOT NULL,
`city` VARCHAR(120) DEFAULT NULL,
`district` VARCHAR(120) DEFAULT NULL,
`street` VARCHAR(180) DEFAULT NULL,
`taluk` VARCHAR(120) DEFAULT NULL,
`state_code` VARCHAR(10) DEFAULT NULL,
`segment` VARCHAR(40) DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`pincode`),
KEY `idx_mmp_city` (`city`),
KEY `idx_mmp_state` (`state_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_rto` (
`rto_code` VARCHAR(10) NOT NULL,
`city_state` VARCHAR(180) DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`rto_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_ncb` (
`ncb_code` VARCHAR(30) NOT NULL,
`sort_order` SMALLINT NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`ncb_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_previous_policy_type` (
`policy_type_code` VARCHAR(20) NOT NULL,
`description` VARCHAR(120) DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`policy_type_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_voluntary_deductible` (
`deductible_code` VARCHAR(40) NOT NULL,
`sort_order` SMALLINT NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`deductible_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_doc_type` (
`doc_code` VARCHAR(10) NOT NULL,
`doc_type` VARCHAR(60) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`doc_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_nominee_relation` (
`relation_code` VARCHAR(40) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`relation_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_state` (
`state_code` VARCHAR(10) NOT NULL,
`state_name` VARCHAR(120) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`state_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_addon_age_limit` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`addon_name` VARCHAR(120) NOT NULL,
`age_limit_4w` VARCHAR(120) DEFAULT NULL,
`age_limit_2w` VARCHAR(255) DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uq_mmaal_addon` (`addon_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `motor_master_import_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`master_key` VARCHAR(60) NOT NULL,
`source_file` VARCHAR(255) DEFAULT NULL,
`rows_upserted` INT NOT NULL DEFAULT 0,
`status` VARCHAR(20) NOT NULL DEFAULT 'OK',
`message` TEXT DEFAULT NULL,
`imported_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY `idx_mmil_master` (`master_key`),
KEY `idx_mmil_imported` (`imported_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Align existing Digit Motor tables if created from an earlier draft schema
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'motor_quote'
AND column_name = 'policyholder_details'
);
SET @sql := IF(@col = 0,
'ALTER TABLE motor_quote ADD COLUMN policyholder_details JSON DEFAULT NULL AFTER coverage_details',
'SELECT ''motor_quote.policyholder_details already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := (
SELECT IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'motor_quote'
AND column_name = 'application_id'
AND character_maximum_length < 255
),
'ALTER TABLE motor_quote MODIFY COLUMN application_id VARCHAR(255) DEFAULT NULL',
'SELECT ''motor_quote.application_id already wide enough'' AS info'
)
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := (
SELECT IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'motor_payment'
AND column_name = 'application_id'
AND character_maximum_length < 255
),
'ALTER TABLE motor_payment MODIFY COLUMN application_id VARCHAR(255) NOT NULL',
'SELECT ''motor_payment.application_id already wide enough'' AS info'
)
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- -----------------------------------------------------------------------------
-- 2026-07-28 | claim_report table
-- Sources: claim_report.sql / Migration 2026-07-28-090700_CreateClaimReportTable
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `claim_report` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`tpa_id` INT UNSIGNED NULL,
`client_id` INT UNSIGNED NULL,
`client_policy_id` INT UNSIGNED NOT NULL,
`file_id` INT UNSIGNED NULL,
`ticket_id` BIGINT UNSIGNED NULL,
`source_table` VARCHAR(64) NULL,
`source_row_id` BIGINT UNSIGNED NULL,
`claim_number` VARCHAR(191) NOT NULL,
`emp_code` VARCHAR(100) NULL,
`tpa_no` VARCHAR(100) NULL,
`emp_id` INT UNSIGNED NULL,
`insured_emp_id` INT UNSIGNED NULL,
`claim_amount` VARCHAR(50) NULL,
`approved_amount` VARCHAR(50) NULL,
`incurred_amount` VARCHAR(50) NULL,
`si_amt` VARCHAR(50) NULL,
`tpa_claim_status` VARCHAR(191) NULL,
`claim_status_id` INT UNSIGNED NULL,
`tpa_claim_type` VARCHAR(100) NULL,
`tpa_ailments` TEXT NULL,
`doa` DATE NULL,
`dod` DATE NULL,
`date_of_intimat` DATE NULL,
`settled_date` DATE NULL,
`approved_date` DATE NULL,
`claim_dump_date` DATETIME NULL,
`hospital_name` VARCHAR(255) NULL,
`hospital_city` VARCHAR(150) NULL,
`hospital_state` VARCHAR(150) NULL,
`hospital_pin_code` VARCHAR(20) NULL,
`hospital_address` TEXT NULL,
`gender` VARCHAR(30) NULL,
`age` VARCHAR(20) NULL,
`relation` VARCHAR(50) NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NULL,
`updated_at` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_claim_report_policy_claim` (`client_policy_id`, `claim_number`),
KEY `idx_claim_report_policy_active` (`client_policy_id`, `is_active`),
KEY `idx_claim_report_ticket` (`ticket_id`),
KEY `idx_claim_report_source` (`source_table`, `source_row_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- -----------------------------------------------------------------------------
-- 2026-08-01 / 2026-08-04 | Dependent add approval tracking
-- Migrations:
-- 2026-08-01-043000_AddDependentApprovalTrackingColumns
-- 2026-08-04-064500_AddRejectReasonToEmployeesAndEmployeePolices
-- 2026-08-04-065000_RenameApprovedByToProcessedBy
-- -----------------------------------------------------------------------------
-- employees
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employees' AND column_name = 'emp_created_by'
);
SET @sql := IF(@col = 0,
'ALTER TABLE employees ADD COLUMN emp_created_by VARCHAR(50) NULL DEFAULT NULL COMMENT ''Creator role: HR / USER'' AFTER emp_status',
'SELECT ''employees.emp_created_by already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- Rename approved_by -> processed_by when needed
SET @sql := (
SELECT IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employees' AND column_name = 'approved_by'
)
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employees' AND column_name = 'processed_by'
),
'ALTER TABLE employees CHANGE COLUMN approved_by processed_by VARCHAR(50) NULL DEFAULT NULL COMMENT ''Processor role: HR / ACM (approve or reject)''',
'SELECT ''employees.processed_by rename skipped'' AS info'
)
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employees' AND column_name = 'processed_by'
);
SET @sql := IF(@col = 0,
'ALTER TABLE employees ADD COLUMN processed_by VARCHAR(50) NULL DEFAULT NULL COMMENT ''Processor role: HR / ACM (approve or reject)'' AFTER emp_created_by',
'SELECT ''employees.processed_by already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employees' AND column_name = 'reject_reason'
);
SET @sql := IF(@col = 0,
'ALTER TABLE employees ADD COLUMN reject_reason TEXT NULL DEFAULT NULL COMMENT ''Reason when dependent addition is rejected'' AFTER processed_by',
'SELECT ''employees.reject_reason already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- employee_polices
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employee_polices' AND column_name = 'emp_policy_created_by'
);
SET @sql := IF(@col = 0,
'ALTER TABLE employee_polices ADD COLUMN emp_policy_created_by VARCHAR(50) NULL DEFAULT NULL COMMENT ''Creator role: HR / USER'' AFTER status',
'SELECT ''employee_polices.emp_policy_created_by already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := (
SELECT IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employee_polices' AND column_name = 'approved_by'
)
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employee_polices' AND column_name = 'processed_by'
),
'ALTER TABLE employee_polices CHANGE COLUMN approved_by processed_by VARCHAR(50) NULL DEFAULT NULL COMMENT ''Processor role: HR / ACM (approve or reject)''',
'SELECT ''employee_polices.processed_by rename skipped'' AS info'
)
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employee_polices' AND column_name = 'processed_by'
);
SET @sql := IF(@col = 0,
'ALTER TABLE employee_polices ADD COLUMN processed_by VARCHAR(50) NULL DEFAULT NULL COMMENT ''Processor role: HR / ACM (approve or reject)'' AFTER emp_policy_created_by',
'SELECT ''employee_polices.processed_by already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'employee_polices' AND column_name = 'reject_reason'
);
SET @sql := IF(@col = 0,
'ALTER TABLE employee_polices ADD COLUMN reject_reason TEXT NULL DEFAULT NULL COMMENT ''Reason when dependent addition is rejected'' AFTER processed_by',
'SELECT ''employee_polices.reject_reason already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- -----------------------------------------------------------------------------
-- 2026-08-11 | Retail policy renewal reminder
-- Source: writable/sql/retail_policy_reminder_schema.sql
-- -----------------------------------------------------------------------------
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'policy_transaction'
AND column_name = 'renewal_status'
);
SET @sql := IF(@col = 0,
'ALTER TABLE policy_transaction ADD COLUMN renewal_status VARCHAR(50) NULL DEFAULT NULL AFTER renewal_date',
'SELECT ''policy_transaction.renewal_status already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'notifications'
AND column_name = 'config_json'
);
SET @sql := IF(@col = 0,
'ALTER TABLE notifications ADD COLUMN config_json LONGTEXT NULL AFTER common_mail',
'SELECT ''notifications.config_json already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
INSERT INTO `notifications` (
`client_id`,
`template_name`,
`subject`,
`mail_content`,
`enabled`,
`config_json`
)
SELECT
NULL,
'retail_reminder_mail',
'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":""}'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM `notifications`
WHERE `template_name` = 'retail_reminder_mail'
AND (`client_id` IS NULL OR `client_id` = 0)
);
-- =============================================================================
-- END
-- Optional demo data (not included): app/Database/digit_motor_demo_seed.sql
-- =============================================================================

View File

@ -0,0 +1,36 @@
-- Retail Policy Reminder: schema + seed
-- Run manually against the application database.
-- 1) policy_transaction.renewal_status
ALTER TABLE `policy_transaction`
ADD COLUMN `renewal_status` VARCHAR(50) NULL DEFAULT NULL
AFTER `renewal_date`;
-- 2) notifications.config_json
ALTER TABLE `notifications`
ADD COLUMN `config_json` LONGTEXT NULL
AFTER `common_mail`;
-- 3) Seed global retail reminder notification (skip if row already exists)
INSERT INTO `notifications` (
`client_id`,
`template_name`,
`subject`,
`mail_content`,
`enabled`,
`config_json`
)
SELECT
NULL,
'retail_reminder_mail',
'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":""}'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM `notifications`
WHERE `template_name` = 'retail_reminder_mail'
AND (`client_id` IS NULL OR `client_id` = 0)
);