Compare commits

..

1 Commits

Author SHA1 Message Date
b6ca3c7f9f CSV file upload to WFM folder implemeted 2026-03-09 15:45:04 +05:30
28 changed files with 396 additions and 382 deletions

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{a as s}from"./index-BmeYOVo5.js";import"./radix-vendor-GwO0o3Qg.js";import"./charts-vendor-waDbLeao.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-3qilyUHW.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-Bmv9jJki.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
import{a as s}from"./index-CULgQ-8S.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CX5oLBI_.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -13,15 +13,15 @@
<!-- Preload essential fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="module" crossorigin src="/assets/index-BmeYOVo5.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-waDbLeao.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-GwO0o3Qg.js">
<script type="module" crossorigin src="/assets/index-CULgQ-8S.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-3qilyUHW.js">
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-CX5oLBI_.js">
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-Bmv9jJki.js">
<link rel="stylesheet" crossorigin href="/assets/index-DCUCLUmo.css">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js">
<link rel="stylesheet" crossorigin href="/assets/index-XBJXaMj2.css">
</head>
<body>

View File

@ -1082,4 +1082,31 @@ export class DealerClaimController {
return ResponseHandler.error(res, 'Failed to download invoice CSV', 500, errorMessage);
}
}
/**
* Re-trigger WFM CSV push (Step 7)
* POST /api/v1/dealer-claims/:requestId/wfm/retrigger
*/
async retriggerWFMPush(req: Request, res: Response): Promise<void> {
try {
const { requestId: identifier } = req.params;
const workflow = await this.findWorkflowByIdentifier(identifier);
if (!workflow) {
return ResponseHandler.error(res, 'Workflow request not found', 404);
}
const requestId = (workflow as any).id || (workflow as any).requestId;
await this.dealerClaimService.pushWFMCSV(requestId);
return ResponseHandler.success(res, {
message: 'WFM CSV push re-triggered successfully'
}, 'WFM push re-triggered');
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[DealerClaimController] Error re-triggering WFM push:', error);
return ResponseHandler.error(res, 'Failed to re-trigger WFM push', 500, errorMessage);
}
}
}

View File

@ -32,8 +32,7 @@ export enum EmailNotificationType {
COMPLETION_DOCUMENTS_SUBMITTED = 'completion_documents_submitted',
EINVOICE_GENERATED = 'einvoice_generated',
CREDIT_NOTE_SENT = 'credit_note_sent',
ADDITIONAL_DOCUMENT_ADDED = 'additional_document_added',
SUMMARY_SHARED = 'summary_shared'
ADDITIONAL_DOCUMENT_ADDED = 'additional_document_added'
}
/**

View File

@ -36,5 +36,4 @@ export { getCompletionDocumentsSubmittedEmail } from './completionDocumentsSubmi
export { getEInvoiceGeneratedEmail } from './einvoiceGenerated.template';
export { getCreditNoteSentEmail } from './creditNoteSent.template';
export { getAdditionalDocumentAddedEmail } from './additionalDocumentAdded.template';
export { getSummarySharedEmail } from './summaryShared.template';

View File

@ -1,116 +0,0 @@
/**
* Summary Shared Email Template
*/
import { SummarySharedData } from './types';
import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles, getEmailContainerStyles } from './helpers';
import { getBrandedHeader } from './branding.config';
export function getSummarySharedEmail(data: SummarySharedData): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="format-detection" content="telephone=no">
<title>Request Summary Shared</title>
${getResponsiveStyles()}
</head>
<body style="margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; background-color: #f4f4f4;">
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr>
<td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({
title: 'Summary Shared',
...HeaderStyles.info
}))}
<tr>
<td style="padding: 40px 30px;">
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
Dear <strong style="color: #007bff;">${data.recipientName}</strong>,
</p>
<p style="margin: 0 0 30px; color: #666666; font-size: 16px; line-height: 1.6;">
<strong>${data.sharedByName}</strong> has shared a summary for request <strong>${data.requestNumber}</strong> with you.
</p>
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f8f9fa; border-radius: 6px; margin-bottom: 30px;" cellpadding="0" cellspacing="0">
<tr>
<td style="padding: 25px;">
<h2 style="margin: 0 0 20px; color: #333333; font-size: 18px; font-weight: 600;">Request Details</h2>
<table role="presentation" style="width: 100%; border-collapse: collapse;" cellpadding="0" cellspacing="0">
<tr>
<td style="padding: 8px 0; color: #666666; font-size: 14px; width: 140px;">
<strong>Request ID:</strong>
</td>
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
${data.requestNumber}
</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666666; font-size: 14px;">
<strong>Title:</strong>
</td>
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
${data.requestTitle || 'N/A'}
</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666666; font-size: 14px;">
<strong>Initiator:</strong>
</td>
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
${data.initiatorName}
</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666666; font-size: 14px;">
<strong>Shared On:</strong>
</td>
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
${data.sharedDate} at ${data.sharedTime}
</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666666; font-size: 14px;">
<strong>Type:</strong>
</td>
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
${data.requestType}
</td>
</tr>
</table>
</td>
</tr>
</table>
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 20px;" cellpadding="0" cellspacing="0">
<tr>
<td style="text-align: center;">
<a href="${data.viewDetailsLink}" class="cta-button" style="display: inline-block; padding: 15px 40px; background-color: #1a1a1a; color: #ffffff; text-decoration: none; text-align: center; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); min-width: 200px;">
View Shared Summary
</a>
</td>
</tr>
</table>
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6; text-align: center;">
You have been granted access to view the summary and conclusion of this request.
</p>
</td>
</tr>
${getEmailFooter(data.companyName)}
</table>
</td>
</tr>
</table>
</body>
</html>
`;
}

View File

@ -137,15 +137,6 @@ export interface RequestClosedData extends BaseEmailData {
documentsCount: number;
}
export interface SummarySharedData extends BaseEmailData {
initiatorName: string;
sharedByName: string;
sharedDate: string;
sharedTime: string;
requestNumber: string;
requestType: string;
}
export interface SpectatorAddedData extends BaseEmailData {
spectatorName: string;
addedByName?: string;

View File

@ -256,68 +256,3 @@ export function hasAdminAccess(user: any): boolean {
}
/**
* Middleware: requireApproverAdditionRights
*
* Restricts adding new approvers to:
* 1. The Initiator
* 2. The Current Active Approver (status IN_PROGRESS)
* 3. Only for CUSTOM/NON_TEMPLATIZED workflows
*/
export function requireApproverAdditionRights() {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const userId: string | undefined = (req as any).user?.userId || (req as any).user?.id;
const requestIdentifier: string | undefined = (req.params as any)?.id;
if (!userId || !requestIdentifier) {
return res.status(403).json({ success: false, error: 'Forbidden' });
}
// Resolve requestIdentifier to actual requestId (UUID)
const workflow = await findWorkflowByIdentifier(requestIdentifier);
if (!workflow) {
return res.status(404).json({ success: false, error: 'Workflow not found' });
}
// 1. Scope Check: Exclude Dealer Claims and Template-based workflows
const templateType = (workflow as any).templateType;
const workflowType = (workflow as any).workflowType;
const isCustom = templateType === 'CUSTOM' || workflowType === 'NON_TEMPLATIZED';
if (!isCustom) {
return res.status(403).json({
success: false,
error: 'Dynamic approver addition is not allowed for this workflow type.'
});
}
// 2. Permission Check: Initiator
if ((workflow as any).initiatorId === userId) {
return next();
}
// 3. Permission Check: Current Active Approver
const { ApprovalLevel } = await import('@models/ApprovalLevel');
const currentApprover = await ApprovalLevel.findOne({
where: {
requestId: (workflow as any).requestId,
approverId: userId,
status: 'IN_PROGRESS'
}
});
if (currentApprover) {
return next();
}
return res.status(403).json({
success: false,
error: 'Only the initiator or the current active approver can add new approvers.'
});
} catch (err) {
console.error('❌ Approver addition rights check failed:', err);
return res.status(500).json({ success: false, error: 'Authorization check failed' });
}
};
}

View File

@ -0,0 +1,50 @@
import { QueryInterface, DataTypes } from 'sequelize';
/**
* Helper function to check if a column exists in a table
*/
async function columnExists(
queryInterface: QueryInterface,
tableName: string,
columnName: string
): Promise<boolean> {
try {
const tableDescription = await queryInterface.describeTable(tableName);
return columnName in tableDescription;
} catch (error) {
return false;
}
}
export async function up(queryInterface: QueryInterface): Promise<void> {
const tableName = 'claim_invoices';
// Add wfm_push_status
if (!(await columnExists(queryInterface, tableName, 'wfm_push_status'))) {
await queryInterface.addColumn(tableName, 'wfm_push_status', {
type: DataTypes.STRING(20),
allowNull: true,
defaultValue: 'PENDING'
});
}
// Add wfm_push_error
if (!(await columnExists(queryInterface, tableName, 'wfm_push_error'))) {
await queryInterface.addColumn(tableName, 'wfm_push_error', {
type: DataTypes.TEXT,
allowNull: true
});
}
}
export async function down(queryInterface: QueryInterface): Promise<void> {
const tableName = 'claim_invoices';
if (await columnExists(queryInterface, tableName, 'wfm_push_status')) {
await queryInterface.removeColumn(tableName, 'wfm_push_status');
}
if (await columnExists(queryInterface, tableName, 'wfm_push_error')) {
await queryInterface.removeColumn(tableName, 'wfm_push_error');
}
}

View File

@ -39,13 +39,15 @@ interface ClaimInvoiceAttributes {
pwcResponse?: any;
irpResponse?: any;
errorMessage?: string;
wfmPushStatus?: 'PENDING' | 'SUCCESS' | 'FAILED';
wfmPushError?: string | null;
generatedAt?: Date;
description?: string;
createdAt: Date;
updatedAt: Date;
}
interface ClaimInvoiceCreationAttributes extends Optional<ClaimInvoiceAttributes, 'invoiceId' | 'invoiceNumber' | 'dmsNumber' | 'invoiceDate' | 'irn' | 'ackNo' | 'ackDate' | 'signedInvoice' | 'signedInvoiceUrl' | 'dealerClaimNumber' | 'dealerClaimDate' | 'billingNo' | 'billingDate' | 'taxableValue' | 'cgstTotal' | 'sgstTotal' | 'igstTotal' | 'utgstTotal' | 'cessTotal' | 'tcsAmt' | 'roundOffAmt' | 'placeOfSupply' | 'totalValueInWords' | 'taxValueInWords' | 'creditNature' | 'consignorGsin' | 'gstinDate' | 'filePath' | 'qrCode' | 'qrImage' | 'pwcResponse' | 'irpResponse' | 'errorMessage' | 'generatedAt' | 'description' | 'createdAt' | 'updatedAt'> { }
interface ClaimInvoiceCreationAttributes extends Optional<ClaimInvoiceAttributes, 'invoiceId' | 'invoiceNumber' | 'dmsNumber' | 'invoiceDate' | 'irn' | 'ackNo' | 'ackDate' | 'signedInvoice' | 'signedInvoiceUrl' | 'dealerClaimNumber' | 'dealerClaimDate' | 'billingNo' | 'billingDate' | 'taxableValue' | 'cgstTotal' | 'sgstTotal' | 'igstTotal' | 'utgstTotal' | 'cessTotal' | 'tcsAmt' | 'roundOffAmt' | 'placeOfSupply' | 'totalValueInWords' | 'taxValueInWords' | 'creditNature' | 'consignorGsin' | 'gstinDate' | 'filePath' | 'qrCode' | 'qrImage' | 'pwcResponse' | 'irpResponse' | 'errorMessage' | 'wfmPushStatus' | 'wfmPushError' | 'generatedAt' | 'description' | 'createdAt' | 'updatedAt'> { }
class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAttributes> implements ClaimInvoiceAttributes {
public invoiceId!: string;
@ -84,6 +86,8 @@ class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAtt
public pwcResponse?: any;
public irpResponse?: any;
public errorMessage?: string;
public wfmPushStatus?: 'PENDING' | 'SUCCESS' | 'FAILED';
public wfmPushError?: string | null;
public generatedAt?: Date;
public description?: string;
public createdAt!: Date;
@ -280,6 +284,17 @@ ClaimInvoice.init(
allowNull: true,
field: 'error_message',
},
wfmPushStatus: {
type: DataTypes.STRING(20),
allowNull: true,
defaultValue: 'PENDING',
field: 'wfm_push_status'
},
wfmPushError: {
type: DataTypes.TEXT,
allowNull: true,
field: 'wfm_push_error'
},
generatedAt: {
type: DataTypes.DATE,
allowNull: true,

View File

@ -98,6 +98,7 @@ router.put('/:requestId/io', authenticateToken, sapLimiter, validateParams(reque
*/
router.put('/:requestId/e-invoice', authenticateToken, sapLimiter, validateParams(requestIdParamsSchema), validateBody(updateEInvoiceSchema), asyncHandler(dealerClaimController.updateEInvoice.bind(dealerClaimController)));
router.get('/:requestId/e-invoice/pdf', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.downloadInvoicePdf.bind(dealerClaimController)));
router.post('/:requestId/wfm/retrigger', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.retriggerWFMPush.bind(dealerClaimController)));
/**
* @route PUT /api/v1/dealer-claims/:requestId/credit-note

View File

@ -7,7 +7,7 @@ import { validateBody, validateParams } from '../middlewares/validate.middleware
import { createWorkflowSchema, updateWorkflowSchema, workflowParamsSchema } from '../validators/workflow.validator';
import { approvalActionSchema, approvalParamsSchema } from '../validators/approval.validator';
import { asyncHandler } from '../middlewares/errorHandler.middleware';
import { requireParticipantTypes, requireApproverAdditionRights } from '../middlewares/authorization.middleware';
import { requireParticipantTypes } from '../middlewares/authorization.middleware';
import multer from 'multer';
import path from 'path';
import crypto from 'crypto';
@ -737,7 +737,6 @@ router.get('/work-notes/attachments/:attachmentId/download',
router.post('/:id/participants/approver',
authenticateToken,
validateParams(workflowParamsSchema),
requireApproverAdditionRights(),
asyncHandler(async (req: any, res: Response) => {
const workflowService = new WorkflowService();
const wf = await (workflowService as any).findWorkflowByIdentifier(req.params.id);
@ -815,7 +814,7 @@ router.post('/:id/approvals/:levelId/skip',
// Add approver at specific level with level shifting
router.post('/:id/approvers/at-level',
authenticateToken,
requireApproverAdditionRights(),
requireParticipantTypes(['INITIATOR', 'APPROVER']), // Only initiator or approvers can add new approvers
validateParams(workflowParamsSchema),
asyncHandler(async (req: any, res: Response) => {
const workflowService = new WorkflowService();

View File

@ -162,6 +162,7 @@ async function runMigrations(): Promise<void> {
const m47 = require('../migrations/20260216-create-api-tokens');
const m48 = require('../migrations/20260216-add-qty-hsn-to-expenses'); // Added new migration
const m49 = require('../migrations/20260302-refine-dealer-claim-schema');
const m50 = require('../migrations/20260309-add-wfm-push-fields');
const migrations = [
{ name: '2025103000-create-users', module: m0 },
@ -216,6 +217,7 @@ async function runMigrations(): Promise<void> {
{ name: '20260216-create-api-tokens', module: m47 },
{ name: '20260216-add-qty-hsn-to-expenses', module: m48 }, // Added to array
{ name: '20260302-refine-dealer-claim-schema', module: m49 },
{ name: '20260309-add-wfm-push-fields', module: m50 },
];
// Dynamically import sequelize after secrets are loaded

View File

@ -52,6 +52,7 @@ import * as m46 from '../migrations/20260216-add-qty-hsn-to-expenses';
import * as m47 from '../migrations/20260217-add-is-service-to-expenses';
import * as m48 from '../migrations/20260217-create-claim-invoice-items';
import * as m49 from '../migrations/20260302-refine-dealer-claim-schema';
import * as m50 from '../migrations/20260309-add-wfm-push-fields';
interface Migration {
name: string;
@ -65,7 +66,8 @@ const migrations: Migration[] = [
{ name: '20260216-add-qty-hsn-to-expenses', module: m46 },
{ name: '20260217-add-is-service-to-expenses', module: m47 },
{ name: '20260217-create-claim-invoice-items', module: m48 },
{ name: '20260302-refine-dealer-claim-schema', module: m49 }
{ name: '20260302-refine-dealer-claim-schema', module: m49 },
{ name: '20260309-add-wfm-push-fields', module: m50 }
];
/**

View File

@ -0,0 +1,48 @@
import { wfmFileService } from '../services/wfmFile.service';
import logger from '../utils/logger';
import dotenv from 'dotenv';
import path from 'path';
// Load environment variables
dotenv.config();
async function testOfficialCSVGeneration() {
try {
console.log('Starting WFM Official CSV Generation Test...');
const officialData = [{
TRNS_UNIQ_NO: '1342774290',
CLAIM_NUMBER: 'CLI000012833733',
INV_NUMBER: 'INV007593742231',
DEALER_CODE: '6059',
IO_NUMBER: '439887',
CLAIM_DOC_TYP: 'ZMBS',
CLAIM_DATE: '20190627',
CLAIM_AMT: 3931.539,
GST_AMT: '185.00',
GST_PERCENTAG: 18
}];
const fileName = `OFFICIAL_TEST_${Date.now()}.csv`;
const filePath = await wfmFileService.generateIncomingClaimCSV(officialData, fileName);
console.log(`✅ Success! Official CSV generated at: ${filePath}`);
// Verify file existence and content
const fs = require('fs');
if (fs.existsSync(filePath)) {
console.log('File existence verified on disk.');
const content = fs.readFileSync(filePath, 'utf8');
console.log('--- CSV Content ---');
console.log(content);
console.log('-------------------');
} else {
console.error('❌ Error: File not found on disk!');
}
} catch (error) {
console.error('❌ Test failed:', error);
}
}
testOfficialCSVGeneration();

View File

@ -10,6 +10,7 @@ import { InternalOrder, IOStatus } from '../models/InternalOrder';
import { ClaimBudgetTracking, BudgetStatus } from '../models/ClaimBudgetTracking';
import { ClaimInvoice } from '../models/ClaimInvoice';
import { ClaimCreditNote } from '../models/ClaimCreditNote';
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
import { DealerCompletionExpense } from '../models/DealerCompletionExpense';
import { ApprovalLevel } from '../models/ApprovalLevel';
import { Participant } from '../models/Participant';
@ -24,6 +25,7 @@ import { generateRequestNumber } from '../utils/helpers';
import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types';
import { sapIntegrationService } from './sapIntegration.service';
import { pwcIntegrationService } from './pwcIntegration.service';
import { wfmFileService } from './wfmFile.service';
import { findDealerLocally } from './dealer.service';
import { notificationService } from './notification.service';
import { activityService } from './activity.service';
@ -2080,6 +2082,11 @@ export class DealerClaimService {
logger.info(`[DealerClaimService] E-Invoice details manually updated for request: ${requestId}`);
}
// Generate CSV for WFM system (INCOMING\WFM_MAIN\DLR_INC_CLAIMS)
await this.pushWFMCSV(requestId).catch((err: Error) => {
logger.error('[DealerClaimService] Initial WFM push failed:', err);
});
// Generate PDF Invoice
try {
const { pdfService } = require('./pdf.service');
@ -3563,5 +3570,81 @@ export class DealerClaimService {
return plain;
});
}
/**
* Push CSV to WFM folder and track status
* This is used by both auto-trigger and manual re-trigger
*/
async pushWFMCSV(requestId: string): Promise<void> {
const invoice = await ClaimInvoice.findOne({ where: { requestId } });
if (!invoice) {
throw new Error('Invoice not found');
}
try {
const [invoiceItems, claimDetails, internalOrder] = await Promise.all([
ClaimInvoiceItem.findAll({ where: { requestId } }),
DealerClaimDetails.findOne({ where: { requestId } }),
InternalOrder.findOne({ where: { requestId } })
]);
if (!claimDetails) {
throw new Error('Dealer claim details not found');
}
const requestNumber = (await WorkflowRequest.findByPk(requestId))?.requestNumber || 'UNKNOWN';
if (invoiceItems.length > 0) {
let sapRefNo = '';
if (claimDetails.activityType) {
const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
sapRefNo = activity?.sapRefNo || '';
}
const formatDate = (date: any) => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
};
const csvData = invoiceItems.map((item: any) => {
const totalTax = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0);
return {
TRNS_UNIQ_NO: item.transactionCode || '',
CLAIM_NUMBER: requestNumber,
INV_NUMBER: invoice.invoiceNumber || '',
DEALER_CODE: claimDetails.dealerCode,
IO_NUMBER: internalOrder?.ioNumber || '',
CLAIM_DOC_TYP: sapRefNo,
CLAIM_DATE: formatDate(invoice.invoiceDate || new Date()),
CLAIM_AMT: item.assAmt,
GST_AMT: totalTax.toFixed(2),
GST_PERCENTAG: item.gstRt
};
});
await wfmFileService.generateIncomingClaimCSV(csvData, `CN_${claimDetails.dealerCode}_${requestNumber}.csv`);
await invoice.update({
wfmPushStatus: 'SUCCESS',
wfmPushError: null
});
logger.info(`[DealerClaimService] WFM CSV successfully pushed for request ${requestNumber}`);
} else {
logger.warn(`[DealerClaimService] No invoice items found for WFM push: ${requestNumber}`);
}
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error pushing to WFM';
await invoice.update({
wfmPushStatus: 'FAILED',
wfmPushError: errorMessage
});
throw error;
}
}
}

View File

@ -20,7 +20,6 @@ import {
getSpectatorAddedEmail,
getApproverSkippedEmail,
getRequestClosedEmail,
getSummarySharedEmail,
getDealerProposalSubmittedEmail,
getDealerProposalRequiredEmail,
getDealerCompletionRequiredEmail,
@ -45,7 +44,6 @@ import {
SpectatorAddedData,
ApproverSkippedData,
RequestClosedData,
SummarySharedData,
DealerProposalSubmittedData,
DealerProposalRequiredData,
ActivityCreatedData,
@ -1442,60 +1440,6 @@ export class EmailNotificationService {
// Don't throw - email failure shouldn't block document upload
}
}
/**
* 11. Send Summary Shared Email
*/
async sendSummaryShared(
requestData: any,
recipientData: any,
initiatorData: any,
sharedByData: any,
sharedSummaryId?: string
): Promise<void> {
try {
const canSend = await shouldSendEmail(
recipientData.userId,
EmailNotificationType.SUMMARY_SHARED
);
if (!canSend) {
logger.info(`Email skipped (preferences): Summary Shared for ${recipientData.email}`);
return;
}
const data: SummarySharedData = {
recipientName: recipientData.displayName || recipientData.email,
requestId: requestData.requestNumber,
requestTitle: requestData.title,
requestNumber: requestData.requestNumber,
initiatorName: initiatorData.displayName || initiatorData.email,
sharedByName: sharedByData.displayName || sharedByData.email,
sharedDate: this.formatDate(new Date()),
sharedTime: this.formatTime(new Date()),
requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType),
viewDetailsLink: sharedSummaryId
? `${this.frontendUrl}/shared-summaries/${sharedSummaryId}`
: `${this.frontendUrl}/request/${requestData.requestNumber}/summary`,
companyName: CompanyInfo.name
};
const html = getSummarySharedEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Summary Shared with You`;
const result = await emailService.sendEmail({
to: recipientData.email,
subject,
html
});
if (result.previewUrl) {
logger.info(`📧 Summary Shared Email Preview: ${result.previewUrl}`);
}
} catch (error) {
logger.error('Failed to send Summary Shared email:', error);
}
}
}
// Singleton instance

View File

@ -25,7 +25,7 @@ interface NotificationPayload {
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
export class NotificationService {
class NotificationService {
private userIdToSubscriptions: Map<string, PushSubscription[]> = new Map();
configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) {
@ -307,8 +307,7 @@ export class NotificationService {
'einvoice_generated': EmailNotificationType.EINVOICE_GENERATED,
'credit_note_sent': EmailNotificationType.CREDIT_NOTE_SENT,
'pause_retrigger_request': EmailNotificationType.WORKFLOW_PAUSED, // Use same template as pause
'pause_retriggered': null,
'summary_shared': EmailNotificationType.SUMMARY_SHARED
'pause_retriggered': null
};
const emailType = emailTypeMap[payload.type || ''];
@ -599,7 +598,7 @@ export class NotificationService {
const currentLevel = await ApprovalLevel.findOne({
where: {
requestId: payload.requestId,
status: 'IN_PROGRESS'
status: 'PENDING'
},
order: [['levelNumber', 'ASC']]
});
@ -809,22 +808,6 @@ export class NotificationService {
}
break;
case 'summary_shared':
{
const sharedByUserId = payload.metadata?.sharedBy;
const sharedSummaryId = payload.metadata?.sharedSummaryId;
const sharedByUser = sharedByUserId ? await User.findByPk(sharedByUserId) : null;
await emailNotificationService.sendSummaryShared(
requestData,
user, // Recipient
initiatorData, // Original request initiator
sharedByUser ? sharedByUser.toJSON() : { displayName: 'System' },
sharedSummaryId
);
}
break;
case 'pause_retrigger_request':
{
// This is when initiator requests approver to resume a paused workflow

View File

@ -3,7 +3,6 @@ import '@models/index'; // Ensure associations are loaded
import { Op } from 'sequelize';
import logger from '@utils/logger';
import dayjs from 'dayjs';
import { NotificationService } from './notification.service';
export class SummaryService {
/**
@ -675,36 +674,6 @@ export class SummaryService {
}
logger.info(`[Summary] Shared summary ${summaryId} with ${sharedSummaries.length} users`);
// 🔔 TRIGGER NOTIFICATIONS
try {
const notificationService = new NotificationService();
const request = await WorkflowRequest.findByPk((summary as any).requestId);
const sharedByUser = await User.findByPk(sharedBy);
if (request && sharedSummaries.length > 0) {
for (const shared of sharedSummaries) {
await notificationService.sendToUsers([shared.sharedWith], {
title: 'Summary Shared',
body: `${sharedByUser?.displayName || 'Someone'} shared a summary for request ${request.requestNumber} with you.`,
requestId: (request as any).requestId,
requestNumber: (request as any).requestNumber,
type: 'summary_shared',
url: `/shared-summaries/${shared.sharedSummaryId}`,
metadata: {
sharedBy,
summaryId,
sharedSummaryId: shared.sharedSummaryId
}
});
}
logger.info(`[Summary] Triggered individual notifications for ${sharedSummaries.length} users`);
}
} catch (notifyError) {
logger.error('[Summary] Failed to trigger notifications for shared summary:', notifyError);
// Don't throw - sharing was successful
}
return sharedSummaries;
} catch (error) {
logger.error(`[Summary] Failed to share summary ${summaryId}:`, error);

View File

@ -0,0 +1,83 @@
import fs from 'fs';
import path from 'path';
import logger from '../utils/logger';
/**
* WFM File Service
* Handles generation and storage of CSV files in the WFM folder structure
*/
export class WFMFileService {
private basePath: string;
private incomingClaimsPath: string;
private outgoingClaimsPath: string;
constructor() {
this.basePath = process.env.WFM_BASE_PATH || 'C:\\WFM';
this.incomingClaimsPath = process.env.WFM_INCOMING_CLAIMS_PATH || 'WFM-QRE\\INCOMING\\WFM_MAIN\\DLR_INC_CLAIMS';
this.outgoingClaimsPath = process.env.WFM_OUTGOING_CLAIMS_PATH || 'WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS';
}
/**
* Ensure the target directory exists
*/
private ensureDirectoryExists(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
logger.info(`[WFMFileService] Created directory: ${dirPath}`);
}
}
/**
* Generate a CSV file for a credit note/claim and store it in the INCOMING folder
* @param data The data to be written to the CSV
* @param fileName The name of the file (e.g., CLAIM_12345.csv)
*/
async generateIncomingClaimCSV(data: any[], fileName: string): Promise<string> {
const maxRetries = 3;
let retryCount = 0;
while (retryCount <= maxRetries) {
try {
const targetDir = path.join(this.basePath, this.incomingClaimsPath);
this.ensureDirectoryExists(targetDir);
const filePath = path.join(targetDir, fileName.endsWith('.csv') ? fileName : `${fileName}.csv`);
// Simple CSV generation logic
const headers = Object.keys(data[0] || {}).join(',');
const rows = data.map(item => Object.values(item).map(val => `"${val}"`).join(',')).join('\n');
const csvContent = `${headers}\n${rows}`;
fs.writeFileSync(filePath, csvContent);
logger.info(`[WFMFileService] Generated CSV at: ${filePath}`);
return filePath;
} catch (error: any) {
if (error.code === 'EBUSY' && retryCount < maxRetries) {
retryCount++;
const delay = retryCount * 1000;
logger.warn(`[WFMFileService] File busy/locked, retrying in ${delay}ms (Attempt ${retryCount}/${maxRetries}): ${fileName}`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (error.code === 'EBUSY') {
throw new Error(`File is locked or open in another program (e.g., Excel). Please close '${fileName}' and try again.`);
}
logger.error('[WFMFileService] Error generating incoming claim CSV:', error);
throw error;
}
}
throw new Error(`Failed to generate CSV after ${maxRetries} retries. Please ensure the file '${fileName}' is not open in any other application.`);
}
/**
* Get the absolute path for an outgoing claim file
*/
getOutgoingPath(fileName: string): string {
return path.join(this.basePath, this.outgoingClaimsPath, fileName);
}
}
export const wfmFileService = new WFMFileService();