From 4f364285936438eb18e817394cdb0f882048f5c1 Mon Sep 17 00:00:00 2001 From: Aaditya Jaiswal Date: Tue, 24 Mar 2026 11:16:19 +0530 Subject: [PATCH] fixed data pull issue for OUTGOING and pending submission mail template --- src/controllers/form16.controller.ts | 56 ++++++++++++- src/routes/form16.routes.ts | 5 ++ src/services/form16.service.ts | 94 +++++++++++++++++++++- src/services/form16Config.service.ts | 4 +- src/services/form16Notification.service.ts | 36 +++++++-- 5 files changed, 180 insertions(+), 15 deletions(-) diff --git a/src/controllers/form16.controller.ts b/src/controllers/form16.controller.ts index 591fd33..e78d3bd 100644 --- a/src/controllers/form16.controller.ts +++ b/src/controllers/form16.controller.ts @@ -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 { + 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); diff --git a/src/routes/form16.routes.ts b/src/routes/form16.routes.ts index 26ef82e..d87c690 100644 --- a/src/routes/form16.routes.ts +++ b/src/routes/form16.routes.ts @@ -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, diff --git a/src/services/form16.service.ts b/src/services/form16.service.ts index a326510..080f0c7 100644 --- a/src/services/form16.service.ts +++ b/src/services/form16.service.ts @@ -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 { + 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 { const row = await (Form16DebitNoteSapResponse as any).findOne({ where: { debitNoteId, storageUrl: { [Op.ne]: null } }, @@ -1509,6 +1559,23 @@ export async function getDebitNoteSapResponseUrl(debitNoteId: number): Promise { + 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 { + 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) ---------- diff --git a/src/services/form16Config.service.ts b/src/services/form16Config.service.ts index c8d0092..806a205 100644 --- a/src/services/form16Config.service.ts +++ b/src/services/form16Config.service.ts @@ -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, diff --git a/src/services/form16Notification.service.ts b/src/services/form16Notification.service.ts index d7ab713..8c32a86 100644 --- a/src/services/form16Notification.service.ts +++ b/src/services/form16Notification.service.ts @@ -142,9 +142,31 @@ function replacePlaceholders(template: string, replacements: Record { 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) {