fixed data pull issue for OUTGOING and pending submission mail template
This commit is contained in:
parent
d3ff1791ac
commit
4f36428593
@ -350,6 +350,49 @@ export class Form16Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/form16/credit-notes/:id/sap-response
|
||||||
|
* Returns persisted SAP response fields for the credit note from DB (and optional CSV URL).
|
||||||
|
* If not yet available, returns 409 so UI can show "being generated, wait".
|
||||||
|
*/
|
||||||
|
async viewCreditNoteSapResponse(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = (req as AuthenticatedRequest).user?.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return ResponseHandler.unauthorized(res, 'Authentication required');
|
||||||
|
}
|
||||||
|
const id = parseInt((req.params as { id: string }).id, 10);
|
||||||
|
if (Number.isNaN(id)) {
|
||||||
|
return ResponseHandler.error(res, 'Invalid credit note id', 400);
|
||||||
|
}
|
||||||
|
let sapResponse = null;
|
||||||
|
try {
|
||||||
|
sapResponse = await form16Service.getCreditNoteSapResponseForUser(id, userId);
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = String(e?.message || '');
|
||||||
|
if (msg.toLowerCase().includes('not found')) {
|
||||||
|
return ResponseHandler.error(res, 'Credit note not found', 404);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
if (!sapResponse) {
|
||||||
|
return ResponseHandler.error(res, 'The credit note is being generated. Please wait.', 409);
|
||||||
|
}
|
||||||
|
return ResponseHandler.success(
|
||||||
|
res,
|
||||||
|
{
|
||||||
|
sapResponse,
|
||||||
|
url: sapResponse.storageUrl || null,
|
||||||
|
},
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('[Form16Controller] viewCreditNoteSapResponse error:', error);
|
||||||
|
return ResponseHandler.error(res, 'Failed to fetch credit note SAP response', 500, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/form16/credit-notes/:id/download
|
* GET /api/v1/form16/credit-notes/:id/download
|
||||||
* Returns a storage URL for the SAP response CSV if available.
|
* Returns a storage URL for the SAP response CSV if available.
|
||||||
@ -397,11 +440,18 @@ export class Form16Controller {
|
|||||||
if (Number.isNaN(id)) {
|
if (Number.isNaN(id)) {
|
||||||
return ResponseHandler.error(res, 'Invalid debit note id', 400);
|
return ResponseHandler.error(res, 'Invalid debit note id', 400);
|
||||||
}
|
}
|
||||||
const url = await form16Service.getDebitNoteSapResponseUrl(id);
|
const sapResponse = await form16Service.getDebitNoteSapResponse(id);
|
||||||
if (!url) {
|
if (!sapResponse) {
|
||||||
return ResponseHandler.error(res, 'The debit note is being generated. Please wait.', 409);
|
return ResponseHandler.error(res, 'The debit note is being generated. Please wait.', 409);
|
||||||
}
|
}
|
||||||
return ResponseHandler.success(res, { url }, 'OK');
|
return ResponseHandler.success(
|
||||||
|
res,
|
||||||
|
{
|
||||||
|
sapResponse,
|
||||||
|
url: sapResponse.storageUrl || null,
|
||||||
|
},
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
logger.error('[Form16Controller] viewDebitNoteSapResponse error:', error);
|
logger.error('[Form16Controller] viewDebitNoteSapResponse error:', error);
|
||||||
|
|||||||
@ -87,6 +87,11 @@ router.get(
|
|||||||
requireForm16SubmissionAccess,
|
requireForm16SubmissionAccess,
|
||||||
asyncHandler(form16Controller.viewDebitNoteSapResponse.bind(form16Controller))
|
asyncHandler(form16Controller.viewDebitNoteSapResponse.bind(form16Controller))
|
||||||
);
|
);
|
||||||
|
router.get(
|
||||||
|
'/credit-notes/:id/sap-response',
|
||||||
|
requireForm16SubmissionAccess,
|
||||||
|
asyncHandler(form16Controller.viewCreditNoteSapResponse.bind(form16Controller))
|
||||||
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/credit-notes/:id',
|
'/credit-notes/:id',
|
||||||
requireForm16SubmissionAccess,
|
requireForm16SubmissionAccess,
|
||||||
|
|||||||
@ -1499,6 +1499,56 @@ export async function getCreditNoteSapResponseUrl(creditNoteId: number): Promise
|
|||||||
return url && String(url).trim() ? String(url) : null;
|
return url && String(url).trim() ? String(url) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Form16SapResponseView {
|
||||||
|
fileName: string | null;
|
||||||
|
trnsUniqNo: string | null;
|
||||||
|
tdsTransId: string | null;
|
||||||
|
claimNumber: string | null;
|
||||||
|
sapDocumentNumber: string | null;
|
||||||
|
msgTyp: string | null;
|
||||||
|
message: string | null;
|
||||||
|
docDate: string | null;
|
||||||
|
tdsAmt: string | null;
|
||||||
|
storageUrl: string | null;
|
||||||
|
createdAt: Date | null;
|
||||||
|
updatedAt: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSapResponseView(row: any): Form16SapResponseView {
|
||||||
|
return {
|
||||||
|
fileName: row?.fileName ?? null,
|
||||||
|
trnsUniqNo: row?.trnsUniqNo ?? null,
|
||||||
|
tdsTransId: row?.tdsTransId ?? null,
|
||||||
|
claimNumber: row?.claimNumber ?? null,
|
||||||
|
sapDocumentNumber: row?.sapDocumentNumber ?? null,
|
||||||
|
msgTyp: row?.msgTyp ?? null,
|
||||||
|
message: row?.message ?? null,
|
||||||
|
docDate: row?.docDate ?? null,
|
||||||
|
tdsAmt: row?.tdsAmt ?? null,
|
||||||
|
storageUrl: row?.storageUrl ?? null,
|
||||||
|
createdAt: row?.createdAt ?? null,
|
||||||
|
updatedAt: row?.updatedAt ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCreditNoteSapResponse(creditNoteId: number): Promise<Form16SapResponseView | null> {
|
||||||
|
const row = await (Form16SapResponse as any).findOne({
|
||||||
|
where: {
|
||||||
|
type: 'credit',
|
||||||
|
creditNoteId,
|
||||||
|
[Op.or]: [
|
||||||
|
{ storageUrl: { [Op.ne]: null } },
|
||||||
|
{ sapDocumentNumber: { [Op.ne]: null } },
|
||||||
|
{ trnsUniqNo: { [Op.ne]: null } },
|
||||||
|
{ tdsTransId: { [Op.ne]: null } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
attributes: ['fileName', 'trnsUniqNo', 'tdsTransId', 'claimNumber', 'sapDocumentNumber', 'msgTyp', 'message', 'docDate', 'tdsAmt', 'storageUrl', 'createdAt', 'updatedAt'],
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
});
|
||||||
|
return row ? mapSapResponseView(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDebitNoteSapResponseUrl(debitNoteId: number): Promise<string | null> {
|
export async function getDebitNoteSapResponseUrl(debitNoteId: number): Promise<string | null> {
|
||||||
const row = await (Form16DebitNoteSapResponse as any).findOne({
|
const row = await (Form16DebitNoteSapResponse as any).findOne({
|
||||||
where: { debitNoteId, storageUrl: { [Op.ne]: null } },
|
where: { debitNoteId, storageUrl: { [Op.ne]: null } },
|
||||||
@ -1509,6 +1559,23 @@ export async function getDebitNoteSapResponseUrl(debitNoteId: number): Promise<s
|
|||||||
return url && String(url).trim() ? String(url) : null;
|
return url && String(url).trim() ? String(url) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDebitNoteSapResponse(debitNoteId: number): Promise<Form16SapResponseView | null> {
|
||||||
|
const row = await (Form16DebitNoteSapResponse as any).findOne({
|
||||||
|
where: {
|
||||||
|
debitNoteId,
|
||||||
|
[Op.or]: [
|
||||||
|
{ storageUrl: { [Op.ne]: null } },
|
||||||
|
{ sapDocumentNumber: { [Op.ne]: null } },
|
||||||
|
{ trnsUniqNo: { [Op.ne]: null } },
|
||||||
|
{ tdsTransId: { [Op.ne]: null } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
attributes: ['fileName', 'trnsUniqNo', 'tdsTransId', 'claimNumber', 'sapDocumentNumber', 'msgTyp', 'message', 'docDate', 'tdsAmt', 'storageUrl', 'createdAt', 'updatedAt'],
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
});
|
||||||
|
return row ? mapSapResponseView(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dealer-safe download: dealers can only download their own credit note.
|
* Dealer-safe download: dealers can only download their own credit note.
|
||||||
* RE/Admin (non-dealer users) can download any credit note.
|
* RE/Admin (non-dealer users) can download any credit note.
|
||||||
@ -1529,6 +1596,21 @@ export async function getCreditNoteSapResponseUrlForUser(creditNoteId: number, u
|
|||||||
return getCreditNoteSapResponseUrl(creditNoteId);
|
return getCreditNoteSapResponseUrl(creditNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCreditNoteSapResponseForUser(creditNoteId: number, userId: string): Promise<Form16SapResponseView | null> {
|
||||||
|
const dealerCode = await getDealerCodeForUser(userId);
|
||||||
|
if (dealerCode) {
|
||||||
|
const note = await Form16CreditNote.findByPk(creditNoteId, {
|
||||||
|
attributes: ['id', 'submissionId'],
|
||||||
|
include: [{ model: Form16aSubmission, as: 'submission', attributes: ['dealerCode'] }],
|
||||||
|
});
|
||||||
|
const noteDealerCode = (note as any)?.submission?.dealerCode;
|
||||||
|
if (!note || !noteDealerCode || String(noteDealerCode).trim() !== String(dealerCode).trim()) {
|
||||||
|
throw new Error('Credit note not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getCreditNoteSapResponse(creditNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Non-submitted dealers (RE only) ----------
|
// ---------- Non-submitted dealers (RE only) ----------
|
||||||
const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4'] as const;
|
const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4'] as const;
|
||||||
|
|
||||||
@ -1756,9 +1838,9 @@ export async function getDealerUserIdsMissingQuarter(financialYear: string, quar
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get dealers (initiator user IDs) who have at least one pending Form 16 submission (no credit note yet, request open).
|
* Get dealers (initiator user IDs) who have at least one pending Form 16 submission (no credit note yet, request open).
|
||||||
* Returns one entry per (userId, requestId) so the reminder can include the request ID. Used by the reminder scheduled job.
|
* Returns one entry per pending request so reminders can include a human-readable request reference.
|
||||||
*/
|
*/
|
||||||
export async function getDealersWithPendingForm16Submissions(): Promise<{ userId: string; requestId: string }[]> {
|
export async function getDealersWithPendingForm16Submissions(): Promise<{ userId: string; requestId: string; requestNumber: string }[]> {
|
||||||
const submissions = await Form16aSubmission.findAll({
|
const submissions = await Form16aSubmission.findAll({
|
||||||
attributes: ['id', 'requestId'],
|
attributes: ['id', 'requestId'],
|
||||||
raw: true,
|
raw: true,
|
||||||
@ -1776,10 +1858,14 @@ export async function getDealersWithPendingForm16Submissions(): Promise<{ userId
|
|||||||
const { WorkflowStatus: WS } = await import('../types/common.types');
|
const { WorkflowStatus: WS } = await import('../types/common.types');
|
||||||
const requests = await WorkflowRequest.findAll({
|
const requests = await WorkflowRequest.findAll({
|
||||||
where: { requestId: pendingRequestIds, templateType: 'FORM_16', status: { [Op.ne]: WS.CLOSED } },
|
where: { requestId: pendingRequestIds, templateType: 'FORM_16', status: { [Op.ne]: WS.CLOSED } },
|
||||||
attributes: ['requestId', 'initiatorId'],
|
attributes: ['requestId', 'requestNumber', 'initiatorId'],
|
||||||
raw: true,
|
raw: true,
|
||||||
});
|
});
|
||||||
return requests.map((r) => ({ userId: (r as any).initiatorId, requestId: (r as any).requestId }));
|
return requests.map((r) => ({
|
||||||
|
userId: (r as any).initiatorId,
|
||||||
|
requestId: (r as any).requestId,
|
||||||
|
requestNumber: (r as any).requestNumber || (r as any).requestId,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 26AS (RE admin) ----------
|
// ---------- 26AS (RE admin) ----------
|
||||||
|
|||||||
@ -64,10 +64,10 @@ const defaults: Form16Config = {
|
|||||||
notificationForm16SuccessCreditNote: { enabled: true, template: 'Form 16 submitted successfully. Credit note: [CreditNoteRef].' },
|
notificationForm16SuccessCreditNote: { enabled: true, template: 'Form 16 submitted successfully. Credit note: [CreditNoteRef].' },
|
||||||
notificationForm16Unsuccessful: { enabled: true, template: 'Form 16 submission was unsuccessful. Issue: [Issue].' },
|
notificationForm16Unsuccessful: { enabled: true, template: 'Form 16 submission was unsuccessful. Issue: [Issue].' },
|
||||||
reminderNotificationEnabled: true,
|
reminderNotificationEnabled: true,
|
||||||
reminderNotificationTemplate: 'Reminder: Form 16 submission is pending. [Name], [Request ID]. Please review.',
|
reminderNotificationTemplate: 'Reminder: Dear [Name], your Form 16A submission is pending for request [Request ID]. Please complete it.',
|
||||||
reminderRunAtTime: '10:00',
|
reminderRunAtTime: '10:00',
|
||||||
alertSubmitForm16Enabled: true,
|
alertSubmitForm16Enabled: true,
|
||||||
alertSubmitForm16Template: 'Please submit your Form 16 at your earliest. [Name], due date: [DueDate].',
|
alertSubmitForm16Template: 'Dear [Name], please submit Form 16A for the pending period. Due: [DueDate].',
|
||||||
alertSubmitForm16RunAtTime: '09:00',
|
alertSubmitForm16RunAtTime: '09:00',
|
||||||
alertSubmitForm16AfterQuarterEndDays: 0,
|
alertSubmitForm16AfterQuarterEndDays: 0,
|
||||||
alertSubmitForm16EveryDays: 7,
|
alertSubmitForm16EveryDays: 7,
|
||||||
|
|||||||
@ -142,9 +142,31 @@ function replacePlaceholders(template: string, replacements: Record<string, stri
|
|||||||
for (const [key, value] of Object.entries(replacements)) {
|
for (const [key, value] of Object.entries(replacements)) {
|
||||||
out = out.replace(new RegExp(`\\[${key}\\]`, 'gi'), value);
|
out = out.replace(new RegExp(`\\[${key}\\]`, 'gi'), value);
|
||||||
}
|
}
|
||||||
|
// Remove unresolved placeholders to avoid leaking raw tokens like [Request ID] in emails.
|
||||||
|
out = out.replace(/\[[^\]]+\]/g, '').replace(/\s{2,}/g, ' ').trim();
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAlertSubmitTemplate(configTemplate: string | undefined): string {
|
||||||
|
const fallback =
|
||||||
|
'Dear [Name], please submit Form 16A for the pending period. Due: [DueDate].';
|
||||||
|
const t = (configTemplate || '').trim();
|
||||||
|
if (!t) return fallback;
|
||||||
|
// Guard against legacy misconfiguration where reminder template is saved into alert template.
|
||||||
|
if (/\[request id\]/i.test(t) || /submission is pending/i.test(t)) return fallback;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveReminderTemplate(configTemplate: string | undefined): string {
|
||||||
|
const fallback =
|
||||||
|
'Reminder: Dear [Name], your Form 16A submission is pending for request [Request ID]. Please complete it.';
|
||||||
|
const t = (configTemplate || '').trim();
|
||||||
|
if (!t) return fallback;
|
||||||
|
// Guard against swapped template in config.
|
||||||
|
if (/\[duedate\]/i.test(t)) return fallback;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
function getQuarterInfoForDate(d: Date): { financialYear: string; quarter: 'Q1' | 'Q2' | 'Q3' | 'Q4' } {
|
function getQuarterInfoForDate(d: Date): { financialYear: string; quarter: 'Q1' | 'Q2' | 'Q3' | 'Q4' } {
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
const month = d.getMonth() + 1; // 1..12
|
const month = d.getMonth() + 1; // 1..12
|
||||||
@ -290,8 +312,9 @@ export async function triggerForm16AlertSubmit(dealerUserIds: string[], placehol
|
|||||||
if (dealerUserIds.length === 0) return;
|
if (dealerUserIds.length === 0) return;
|
||||||
try {
|
try {
|
||||||
const config = await getForm16Config();
|
const config = await getForm16Config();
|
||||||
if (!config.alertSubmitForm16Enabled || !config.alertSubmitForm16Template) return;
|
if (!config.alertSubmitForm16Enabled) return;
|
||||||
const body = replacePlaceholders(config.alertSubmitForm16Template, {
|
const template = resolveAlertSubmitTemplate(config.alertSubmitForm16Template);
|
||||||
|
const body = replacePlaceholders(template, {
|
||||||
Name: placeholders?.name ?? 'Dealer',
|
Name: placeholders?.name ?? 'Dealer',
|
||||||
DueDate: placeholders?.dueDate ?? '—',
|
DueDate: placeholders?.dueDate ?? '—',
|
||||||
});
|
});
|
||||||
@ -315,8 +338,9 @@ export async function triggerForm16Reminder(dealerUserIds: string[], placeholder
|
|||||||
if (dealerUserIds.length === 0) return;
|
if (dealerUserIds.length === 0) return;
|
||||||
try {
|
try {
|
||||||
const config = await getForm16Config();
|
const config = await getForm16Config();
|
||||||
if (!config.reminderNotificationEnabled || !config.reminderNotificationTemplate) return;
|
if (!config.reminderNotificationEnabled) return;
|
||||||
const body = replacePlaceholders(config.reminderNotificationTemplate, {
|
const template = resolveReminderTemplate(config.reminderNotificationTemplate);
|
||||||
|
const body = replacePlaceholders(template, {
|
||||||
Name: placeholders?.name ?? 'Dealer',
|
Name: placeholders?.name ?? 'Dealer',
|
||||||
'Request ID': placeholders?.requestId ?? '—',
|
'Request ID': placeholders?.requestId ?? '—',
|
||||||
});
|
});
|
||||||
@ -383,8 +407,8 @@ export async function runForm16ReminderJob(): Promise<void> {
|
|||||||
logger.info('[Form16Notification] No pending Form 16 submissions for reminder, skipping');
|
logger.info('[Form16Notification] No pending Form 16 submissions for reminder, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const { userId, requestId } of pending) {
|
for (const { userId, requestNumber } of pending) {
|
||||||
await triggerForm16Reminder([userId], { name: 'Dealer', requestId });
|
await triggerForm16Reminder([userId], { name: 'Dealer', requestId: requestNumber });
|
||||||
}
|
}
|
||||||
logger.info(`[Form16Notification] Reminder job completed: sent ${pending.length} reminder(s)`);
|
logger.info(`[Form16Notification] Reminder job completed: sent ${pending.length} reminder(s)`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user