/** * Form 16 admin config reader for server-side use (notifications, cron). * Reads FORM16_ADMIN_CONFIG from admin_configurations and returns merged config with defaults. */ import { sequelize } from '../config/database'; import { QueryTypes } from 'sequelize'; import logger from '@utils/logger'; const FORM16_CONFIG_KEY = 'FORM16_ADMIN_CONFIG'; function normalizeRunAtTime(s: string): string { const [h, m] = s.split(':').map((x) => parseInt(x, 10)); if (Number.isNaN(h) || Number.isNaN(m)) return '09:00'; const hh = Math.max(0, Math.min(23, h)); const mm = Math.max(0, Math.min(59, m)); return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}`; } export interface Form16Notification26AsConfig { enabled: boolean; templateRe: string; templateDealers: string; } export interface Form16Config { notification26AsDataAdded: Form16Notification26AsConfig; /** RE reminder: prompt 26AS viewers to upload 26AS if missing for the most recently ended quarter. */ reminder26AsUploadEnabled: boolean; reminder26AsUploadTemplate: string; /** Start reminders after quarter ends + N days. */ reminder26AsUploadAfterQuarterEndDays: number; /** Repeat reminders every N days after start. */ reminder26AsUploadEveryDays: number; notificationForm16SuccessCreditNote: { enabled: boolean; template: string }; notificationForm16Unsuccessful: { enabled: boolean; template: string }; reminderNotificationEnabled: boolean; reminderNotificationTemplate: string; /** When to run the reminder job daily (HH:mm, 24h, server timezone). Empty = no scheduled run. */ reminderRunAtTime: string; alertSubmitForm16Enabled: boolean; alertSubmitForm16Template: string; /** When to run the alert job daily (HH:mm, 24h, server timezone). Empty = no scheduled run. */ alertSubmitForm16RunAtTime: string; /** Dealer reminder: start after quarter ends + N days. */ alertSubmitForm16AfterQuarterEndDays: number; /** Dealer reminder: repeat every N days after start. */ alertSubmitForm16EveryDays: number; twentySixAsViewerEmails: string[]; } const default26As = (): Form16Notification26AsConfig => ({ enabled: true, templateRe: '26AS data has been added. Please review and use for matching dealer Form 16 submissions.', templateDealers: 'New 26AS data has been uploaded. You can now submit your Form 16 for the relevant quarter if you haven’t already.', }); const defaults: Form16Config = { notification26AsDataAdded: default26As(), reminder26AsUploadEnabled: true, reminder26AsUploadTemplate: 'Reminder: 26AS data for [FinancialYear] [Quarter] is not uploaded yet. Please upload it in 26AS Management.', reminder26AsUploadAfterQuarterEndDays: 0, reminder26AsUploadEveryDays: 7, notificationForm16SuccessCreditNote: { enabled: true, template: 'Form 16 submitted successfully. Credit note: [CreditNoteRef].' }, notificationForm16Unsuccessful: { enabled: true, template: 'Form 16 submission was unsuccessful. Issue: [Issue].' }, reminderNotificationEnabled: true, reminderNotificationTemplate: 'Reminder: Form 16 submission is pending. [Name], [Request ID]. Please review.', reminderRunAtTime: '10:00', alertSubmitForm16Enabled: true, alertSubmitForm16Template: 'Please submit your Form 16 at your earliest. [Name], due date: [DueDate].', alertSubmitForm16RunAtTime: '09:00', alertSubmitForm16AfterQuarterEndDays: 0, alertSubmitForm16EveryDays: 7, twentySixAsViewerEmails: [], }; /** * Load Form 16 config from admin_configurations for server-side use. */ export async function getForm16Config(): Promise { try { const result = await sequelize.query<{ config_value: string }>( `SELECT config_value FROM admin_configurations WHERE config_key = :configKey LIMIT 1`, { replacements: { configKey: FORM16_CONFIG_KEY }, type: QueryTypes.SELECT } ); if (!result?.length || !result[0].config_value) { return defaults; } const parsed = JSON.parse(result[0].config_value) as Record; const merge26As = (): Form16Notification26AsConfig => { const v = parsed.notification26AsDataAdded; if (v && typeof v === 'object' && typeof (v as any).enabled === 'boolean') { const val = v as any; return { enabled: val.enabled, templateRe: typeof val.templateRe === 'string' ? val.templateRe : (typeof val.template === 'string' ? val.template : defaults.notification26AsDataAdded.templateRe), templateDealers: typeof val.templateDealers === 'string' ? val.templateDealers : defaults.notification26AsDataAdded.templateDealers, }; } return defaults.notification26AsDataAdded; }; return { notification26AsDataAdded: merge26As(), reminder26AsUploadEnabled: typeof parsed.reminder26AsUploadEnabled === 'boolean' ? parsed.reminder26AsUploadEnabled : defaults.reminder26AsUploadEnabled, reminder26AsUploadTemplate: typeof parsed.reminder26AsUploadTemplate === 'string' ? parsed.reminder26AsUploadTemplate : defaults.reminder26AsUploadTemplate, reminder26AsUploadAfterQuarterEndDays: typeof parsed.reminder26AsUploadAfterQuarterEndDays === 'number' ? Math.max(0, Math.min(365, parsed.reminder26AsUploadAfterQuarterEndDays)) : defaults.reminder26AsUploadAfterQuarterEndDays, reminder26AsUploadEveryDays: typeof parsed.reminder26AsUploadEveryDays === 'number' ? Math.max(1, Math.min(365, parsed.reminder26AsUploadEveryDays)) : defaults.reminder26AsUploadEveryDays, notificationForm16SuccessCreditNote: parsed.notificationForm16SuccessCreditNote && typeof (parsed.notificationForm16SuccessCreditNote as any).template === 'string' ? { enabled: (parsed.notificationForm16SuccessCreditNote as any).enabled !== false, template: (parsed.notificationForm16SuccessCreditNote as any).template } : defaults.notificationForm16SuccessCreditNote, notificationForm16Unsuccessful: parsed.notificationForm16Unsuccessful && typeof (parsed.notificationForm16Unsuccessful as any).template === 'string' ? { enabled: (parsed.notificationForm16Unsuccessful as any).enabled !== false, template: (parsed.notificationForm16Unsuccessful as any).template } : defaults.notificationForm16Unsuccessful, reminderNotificationEnabled: typeof parsed.reminderNotificationEnabled === 'boolean' ? parsed.reminderNotificationEnabled : defaults.reminderNotificationEnabled, reminderNotificationTemplate: typeof parsed.reminderNotificationTemplate === 'string' ? parsed.reminderNotificationTemplate : defaults.reminderNotificationTemplate, reminderRunAtTime: typeof parsed.reminderRunAtTime === 'string' && parsed.reminderRunAtTime.trim() ? (/^\d{1,2}:\d{2}$/.test(parsed.reminderRunAtTime.trim()) ? normalizeRunAtTime(parsed.reminderRunAtTime.trim()) : defaults.reminderRunAtTime) : '', alertSubmitForm16Enabled: typeof parsed.alertSubmitForm16Enabled === 'boolean' ? parsed.alertSubmitForm16Enabled : defaults.alertSubmitForm16Enabled, alertSubmitForm16Template: typeof parsed.alertSubmitForm16Template === 'string' ? parsed.alertSubmitForm16Template : defaults.alertSubmitForm16Template, alertSubmitForm16RunAtTime: typeof parsed.alertSubmitForm16RunAtTime === 'string' && parsed.alertSubmitForm16RunAtTime.trim() ? (/^\d{1,2}:\d{2}$/.test(parsed.alertSubmitForm16RunAtTime.trim()) ? normalizeRunAtTime(parsed.alertSubmitForm16RunAtTime.trim()) : defaults.alertSubmitForm16RunAtTime) : '', alertSubmitForm16AfterQuarterEndDays: typeof parsed.alertSubmitForm16AfterQuarterEndDays === 'number' ? Math.max(0, Math.min(365, parsed.alertSubmitForm16AfterQuarterEndDays)) : defaults.alertSubmitForm16AfterQuarterEndDays, alertSubmitForm16EveryDays: typeof parsed.alertSubmitForm16EveryDays === 'number' ? Math.max(1, Math.min(365, parsed.alertSubmitForm16EveryDays)) : defaults.alertSubmitForm16EveryDays, twentySixAsViewerEmails: Array.isArray(parsed.twentySixAsViewerEmails) ? (parsed.twentySixAsViewerEmails as string[]).map((e) => String(e).trim().toLowerCase()).filter(Boolean) : defaults.twentySixAsViewerEmails, }; } catch (e) { logger.warn('[Form16Config] Failed to load config, using defaults:', e); return defaults; } }