147 lines
8.2 KiB
TypeScript
147 lines
8.2 KiB
TypeScript
/**
|
||
* 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<Form16Config> {
|
||
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<string, unknown>;
|
||
|
||
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;
|
||
}
|
||
}
|