Compare commits

..

2 Commits

Author SHA1 Message Date
Aaditya Jaiswal
fd6032f21b Merge branch 'laxman_dev' updates
Made-with: Cursor
2026-03-24 11:24:02 +05:30
Aaditya Jaiswal
4f36428593 fixed data pull issue for OUTGOING and pending submission mail template 2026-03-24 11:16:19 +05:30
5 changed files with 180 additions and 15 deletions

View File

@ -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
* Returns a storage URL for the SAP response CSV if available.
@ -397,11 +440,18 @@ export class Form16Controller {
if (Number.isNaN(id)) {
return ResponseHandler.error(res, 'Invalid debit note id', 400);
}
const url = await form16Service.getDebitNoteSapResponseUrl(id);
if (!url) {
const sapResponse = await form16Service.getDebitNoteSapResponse(id);
if (!sapResponse) {
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) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[Form16Controller] viewDebitNoteSapResponse error:', error);

View File

@ -87,6 +87,11 @@ router.get(
requireForm16SubmissionAccess,
asyncHandler(form16Controller.viewDebitNoteSapResponse.bind(form16Controller))
);
router.get(
'/credit-notes/:id/sap-response',
requireForm16SubmissionAccess,
asyncHandler(form16Controller.viewCreditNoteSapResponse.bind(form16Controller))
);
router.get(
'/credit-notes/:id',
requireForm16SubmissionAccess,

View File

@ -1499,6 +1499,56 @@ export async function getCreditNoteSapResponseUrl(creditNoteId: number): Promise
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> {
const row = await (Form16DebitNoteSapResponse as any).findOne({
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;
}
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.
* RE/Admin (non-dealer users) can download any credit note.
@ -1529,6 +1596,21 @@ export async function getCreditNoteSapResponseUrlForUser(creditNoteId: number, u
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) ----------
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).
* 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({
attributes: ['id', 'requestId'],
raw: true,
@ -1776,10 +1858,14 @@ export async function getDealersWithPendingForm16Submissions(): Promise<{ userId
const { WorkflowStatus: WS } = await import('../types/common.types');
const requests = await WorkflowRequest.findAll({
where: { requestId: pendingRequestIds, templateType: 'FORM_16', status: { [Op.ne]: WS.CLOSED } },
attributes: ['requestId', 'initiatorId'],
attributes: ['requestId', 'requestNumber', 'initiatorId'],
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) ----------

View File

@ -64,10 +64,10 @@ const defaults: Form16Config = {
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.',
reminderNotificationTemplate: 'Reminder: Dear [Name], your Form 16A submission is pending for request [Request ID]. Please complete it.',
reminderRunAtTime: '10:00',
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',
alertSubmitForm16AfterQuarterEndDays: 0,
alertSubmitForm16EveryDays: 7,

View File

@ -142,9 +142,31 @@ function replacePlaceholders(template: string, replacements: Record<string, stri
for (const [key, value] of Object.entries(replacements)) {
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;
}
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' } {
const year = d.getFullYear();
const month = d.getMonth() + 1; // 1..12
@ -290,8 +312,9 @@ export async function triggerForm16AlertSubmit(dealerUserIds: string[], placehol
if (dealerUserIds.length === 0) return;
try {
const config = await getForm16Config();
if (!config.alertSubmitForm16Enabled || !config.alertSubmitForm16Template) return;
const body = replacePlaceholders(config.alertSubmitForm16Template, {
if (!config.alertSubmitForm16Enabled) return;
const template = resolveAlertSubmitTemplate(config.alertSubmitForm16Template);
const body = replacePlaceholders(template, {
Name: placeholders?.name ?? 'Dealer',
DueDate: placeholders?.dueDate ?? '—',
});
@ -315,8 +338,9 @@ export async function triggerForm16Reminder(dealerUserIds: string[], placeholder
if (dealerUserIds.length === 0) return;
try {
const config = await getForm16Config();
if (!config.reminderNotificationEnabled || !config.reminderNotificationTemplate) return;
const body = replacePlaceholders(config.reminderNotificationTemplate, {
if (!config.reminderNotificationEnabled) return;
const template = resolveReminderTemplate(config.reminderNotificationTemplate);
const body = replacePlaceholders(template, {
Name: placeholders?.name ?? 'Dealer',
'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');
return;
}
for (const { userId, requestId } of pending) {
await triggerForm16Reminder([userId], { name: 'Dealer', requestId });
for (const { userId, requestNumber } of pending) {
await triggerForm16Reminder([userId], { name: 'Dealer', requestId: requestNumber });
}
logger.info(`[Form16Notification] Reminder job completed: sent ${pending.length} reminder(s)`);
} catch (e) {