Compare commits
3 Commits
main
...
custom_pro
| Author | SHA1 | Date | |
|---|---|---|---|
| 02c5187232 | |||
| b5f3513bd1 | |||
| 8753c9477d |
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
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};
|
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};
|
||||||
64
build/assets/index-BmeYOVo5.js
Normal file
64
build/assets/index-BmeYOVo5.js
Normal file
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
2
build/assets/ui-vendor-3qilyUHW.js
Normal file
2
build/assets/ui-vendor-3qilyUHW.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -13,15 +13,15 @@
|
|||||||
<!-- Preload essential fonts and icons -->
|
<!-- Preload essential fonts and icons -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<script type="module" crossorigin src="/assets/index-CULgQ-8S.js"></script>
|
<script type="module" crossorigin src="/assets/index-BmeYOVo5.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-waDbLeao.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-GwO0o3Qg.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-CX5oLBI_.js">
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-3qilyUHW.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.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/redux-vendor-tbZCm13o.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-Bmv9jJki.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-XBJXaMj2.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DCUCLUmo.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -1082,31 +1082,4 @@ export class DealerClaimController {
|
|||||||
return ResponseHandler.error(res, 'Failed to download invoice CSV', 500, errorMessage);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,8 @@ export enum EmailNotificationType {
|
|||||||
COMPLETION_DOCUMENTS_SUBMITTED = 'completion_documents_submitted',
|
COMPLETION_DOCUMENTS_SUBMITTED = 'completion_documents_submitted',
|
||||||
EINVOICE_GENERATED = 'einvoice_generated',
|
EINVOICE_GENERATED = 'einvoice_generated',
|
||||||
CREDIT_NOTE_SENT = 'credit_note_sent',
|
CREDIT_NOTE_SENT = 'credit_note_sent',
|
||||||
ADDITIONAL_DOCUMENT_ADDED = 'additional_document_added'
|
ADDITIONAL_DOCUMENT_ADDED = 'additional_document_added',
|
||||||
|
SUMMARY_SHARED = 'summary_shared'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -36,4 +36,5 @@ export { getCompletionDocumentsSubmittedEmail } from './completionDocumentsSubmi
|
|||||||
export { getEInvoiceGeneratedEmail } from './einvoiceGenerated.template';
|
export { getEInvoiceGeneratedEmail } from './einvoiceGenerated.template';
|
||||||
export { getCreditNoteSentEmail } from './creditNoteSent.template';
|
export { getCreditNoteSentEmail } from './creditNoteSent.template';
|
||||||
export { getAdditionalDocumentAddedEmail } from './additionalDocumentAdded.template';
|
export { getAdditionalDocumentAddedEmail } from './additionalDocumentAdded.template';
|
||||||
|
export { getSummarySharedEmail } from './summaryShared.template';
|
||||||
|
|
||||||
|
|||||||
116
src/emailtemplates/summaryShared.template.ts
Normal file
116
src/emailtemplates/summaryShared.template.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -137,6 +137,15 @@ export interface RequestClosedData extends BaseEmailData {
|
|||||||
documentsCount: number;
|
documentsCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SummarySharedData extends BaseEmailData {
|
||||||
|
initiatorName: string;
|
||||||
|
sharedByName: string;
|
||||||
|
sharedDate: string;
|
||||||
|
sharedTime: string;
|
||||||
|
requestNumber: string;
|
||||||
|
requestType: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SpectatorAddedData extends BaseEmailData {
|
export interface SpectatorAddedData extends BaseEmailData {
|
||||||
spectatorName: string;
|
spectatorName: string;
|
||||||
addedByName?: string;
|
addedByName?: string;
|
||||||
|
|||||||
@ -256,3 +256,68 @@ 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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -39,15 +39,13 @@ interface ClaimInvoiceAttributes {
|
|||||||
pwcResponse?: any;
|
pwcResponse?: any;
|
||||||
irpResponse?: any;
|
irpResponse?: any;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
wfmPushStatus?: 'PENDING' | 'SUCCESS' | 'FAILED';
|
|
||||||
wfmPushError?: string | null;
|
|
||||||
generatedAt?: Date;
|
generatedAt?: Date;
|
||||||
description?: string;
|
description?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: 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' | 'wfmPushStatus' | 'wfmPushError' | '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' | 'generatedAt' | 'description' | 'createdAt' | 'updatedAt'> { }
|
||||||
|
|
||||||
class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAttributes> implements ClaimInvoiceAttributes {
|
class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAttributes> implements ClaimInvoiceAttributes {
|
||||||
public invoiceId!: string;
|
public invoiceId!: string;
|
||||||
@ -86,8 +84,6 @@ class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAtt
|
|||||||
public pwcResponse?: any;
|
public pwcResponse?: any;
|
||||||
public irpResponse?: any;
|
public irpResponse?: any;
|
||||||
public errorMessage?: string;
|
public errorMessage?: string;
|
||||||
public wfmPushStatus?: 'PENDING' | 'SUCCESS' | 'FAILED';
|
|
||||||
public wfmPushError?: string | null;
|
|
||||||
public generatedAt?: Date;
|
public generatedAt?: Date;
|
||||||
public description?: string;
|
public description?: string;
|
||||||
public createdAt!: Date;
|
public createdAt!: Date;
|
||||||
@ -284,17 +280,6 @@ ClaimInvoice.init(
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'error_message',
|
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: {
|
generatedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@ -98,7 +98,6 @@ 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.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.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
|
* @route PUT /api/v1/dealer-claims/:requestId/credit-note
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { validateBody, validateParams } from '../middlewares/validate.middleware
|
|||||||
import { createWorkflowSchema, updateWorkflowSchema, workflowParamsSchema } from '../validators/workflow.validator';
|
import { createWorkflowSchema, updateWorkflowSchema, workflowParamsSchema } from '../validators/workflow.validator';
|
||||||
import { approvalActionSchema, approvalParamsSchema } from '../validators/approval.validator';
|
import { approvalActionSchema, approvalParamsSchema } from '../validators/approval.validator';
|
||||||
import { asyncHandler } from '../middlewares/errorHandler.middleware';
|
import { asyncHandler } from '../middlewares/errorHandler.middleware';
|
||||||
import { requireParticipantTypes } from '../middlewares/authorization.middleware';
|
import { requireParticipantTypes, requireApproverAdditionRights } from '../middlewares/authorization.middleware';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
@ -737,6 +737,7 @@ router.get('/work-notes/attachments/:attachmentId/download',
|
|||||||
router.post('/:id/participants/approver',
|
router.post('/:id/participants/approver',
|
||||||
authenticateToken,
|
authenticateToken,
|
||||||
validateParams(workflowParamsSchema),
|
validateParams(workflowParamsSchema),
|
||||||
|
requireApproverAdditionRights(),
|
||||||
asyncHandler(async (req: any, res: Response) => {
|
asyncHandler(async (req: any, res: Response) => {
|
||||||
const workflowService = new WorkflowService();
|
const workflowService = new WorkflowService();
|
||||||
const wf = await (workflowService as any).findWorkflowByIdentifier(req.params.id);
|
const wf = await (workflowService as any).findWorkflowByIdentifier(req.params.id);
|
||||||
@ -814,7 +815,7 @@ router.post('/:id/approvals/:levelId/skip',
|
|||||||
// Add approver at specific level with level shifting
|
// Add approver at specific level with level shifting
|
||||||
router.post('/:id/approvers/at-level',
|
router.post('/:id/approvers/at-level',
|
||||||
authenticateToken,
|
authenticateToken,
|
||||||
requireParticipantTypes(['INITIATOR', 'APPROVER']), // Only initiator or approvers can add new approvers
|
requireApproverAdditionRights(),
|
||||||
validateParams(workflowParamsSchema),
|
validateParams(workflowParamsSchema),
|
||||||
asyncHandler(async (req: any, res: Response) => {
|
asyncHandler(async (req: any, res: Response) => {
|
||||||
const workflowService = new WorkflowService();
|
const workflowService = new WorkflowService();
|
||||||
|
|||||||
@ -162,7 +162,6 @@ async function runMigrations(): Promise<void> {
|
|||||||
const m47 = require('../migrations/20260216-create-api-tokens');
|
const m47 = require('../migrations/20260216-create-api-tokens');
|
||||||
const m48 = require('../migrations/20260216-add-qty-hsn-to-expenses'); // Added new migration
|
const m48 = require('../migrations/20260216-add-qty-hsn-to-expenses'); // Added new migration
|
||||||
const m49 = require('../migrations/20260302-refine-dealer-claim-schema');
|
const m49 = require('../migrations/20260302-refine-dealer-claim-schema');
|
||||||
const m50 = require('../migrations/20260309-add-wfm-push-fields');
|
|
||||||
|
|
||||||
const migrations = [
|
const migrations = [
|
||||||
{ name: '2025103000-create-users', module: m0 },
|
{ name: '2025103000-create-users', module: m0 },
|
||||||
@ -217,7 +216,6 @@ async function runMigrations(): Promise<void> {
|
|||||||
{ name: '20260216-create-api-tokens', module: m47 },
|
{ name: '20260216-create-api-tokens', module: m47 },
|
||||||
{ name: '20260216-add-qty-hsn-to-expenses', module: m48 }, // Added to array
|
{ name: '20260216-add-qty-hsn-to-expenses', module: m48 }, // Added to array
|
||||||
{ name: '20260302-refine-dealer-claim-schema', module: m49 },
|
{ name: '20260302-refine-dealer-claim-schema', module: m49 },
|
||||||
{ name: '20260309-add-wfm-push-fields', module: m50 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Dynamically import sequelize after secrets are loaded
|
// Dynamically import sequelize after secrets are loaded
|
||||||
|
|||||||
@ -52,7 +52,6 @@ import * as m46 from '../migrations/20260216-add-qty-hsn-to-expenses';
|
|||||||
import * as m47 from '../migrations/20260217-add-is-service-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 m48 from '../migrations/20260217-create-claim-invoice-items';
|
||||||
import * as m49 from '../migrations/20260302-refine-dealer-claim-schema';
|
import * as m49 from '../migrations/20260302-refine-dealer-claim-schema';
|
||||||
import * as m50 from '../migrations/20260309-add-wfm-push-fields';
|
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
name: string;
|
name: string;
|
||||||
@ -66,8 +65,7 @@ const migrations: Migration[] = [
|
|||||||
{ name: '20260216-add-qty-hsn-to-expenses', module: m46 },
|
{ name: '20260216-add-qty-hsn-to-expenses', module: m46 },
|
||||||
{ name: '20260217-add-is-service-to-expenses', module: m47 },
|
{ name: '20260217-add-is-service-to-expenses', module: m47 },
|
||||||
{ name: '20260217-create-claim-invoice-items', module: m48 },
|
{ 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 }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
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();
|
|
||||||
@ -10,7 +10,6 @@ import { InternalOrder, IOStatus } from '../models/InternalOrder';
|
|||||||
import { ClaimBudgetTracking, BudgetStatus } from '../models/ClaimBudgetTracking';
|
import { ClaimBudgetTracking, BudgetStatus } from '../models/ClaimBudgetTracking';
|
||||||
import { ClaimInvoice } from '../models/ClaimInvoice';
|
import { ClaimInvoice } from '../models/ClaimInvoice';
|
||||||
import { ClaimCreditNote } from '../models/ClaimCreditNote';
|
import { ClaimCreditNote } from '../models/ClaimCreditNote';
|
||||||
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
|
|
||||||
import { DealerCompletionExpense } from '../models/DealerCompletionExpense';
|
import { DealerCompletionExpense } from '../models/DealerCompletionExpense';
|
||||||
import { ApprovalLevel } from '../models/ApprovalLevel';
|
import { ApprovalLevel } from '../models/ApprovalLevel';
|
||||||
import { Participant } from '../models/Participant';
|
import { Participant } from '../models/Participant';
|
||||||
@ -25,7 +24,6 @@ import { generateRequestNumber } from '../utils/helpers';
|
|||||||
import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types';
|
import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types';
|
||||||
import { sapIntegrationService } from './sapIntegration.service';
|
import { sapIntegrationService } from './sapIntegration.service';
|
||||||
import { pwcIntegrationService } from './pwcIntegration.service';
|
import { pwcIntegrationService } from './pwcIntegration.service';
|
||||||
import { wfmFileService } from './wfmFile.service';
|
|
||||||
import { findDealerLocally } from './dealer.service';
|
import { findDealerLocally } from './dealer.service';
|
||||||
import { notificationService } from './notification.service';
|
import { notificationService } from './notification.service';
|
||||||
import { activityService } from './activity.service';
|
import { activityService } from './activity.service';
|
||||||
@ -2082,11 +2080,6 @@ export class DealerClaimService {
|
|||||||
logger.info(`[DealerClaimService] E-Invoice details manually updated for request: ${requestId}`);
|
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
|
// Generate PDF Invoice
|
||||||
try {
|
try {
|
||||||
const { pdfService } = require('./pdf.service');
|
const { pdfService } = require('./pdf.service');
|
||||||
@ -3570,81 +3563,5 @@ export class DealerClaimService {
|
|||||||
return plain;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
getSpectatorAddedEmail,
|
getSpectatorAddedEmail,
|
||||||
getApproverSkippedEmail,
|
getApproverSkippedEmail,
|
||||||
getRequestClosedEmail,
|
getRequestClosedEmail,
|
||||||
|
getSummarySharedEmail,
|
||||||
getDealerProposalSubmittedEmail,
|
getDealerProposalSubmittedEmail,
|
||||||
getDealerProposalRequiredEmail,
|
getDealerProposalRequiredEmail,
|
||||||
getDealerCompletionRequiredEmail,
|
getDealerCompletionRequiredEmail,
|
||||||
@ -44,6 +45,7 @@ import {
|
|||||||
SpectatorAddedData,
|
SpectatorAddedData,
|
||||||
ApproverSkippedData,
|
ApproverSkippedData,
|
||||||
RequestClosedData,
|
RequestClosedData,
|
||||||
|
SummarySharedData,
|
||||||
DealerProposalSubmittedData,
|
DealerProposalSubmittedData,
|
||||||
DealerProposalRequiredData,
|
DealerProposalRequiredData,
|
||||||
ActivityCreatedData,
|
ActivityCreatedData,
|
||||||
@ -1440,6 +1442,60 @@ export class EmailNotificationService {
|
|||||||
// Don't throw - email failure shouldn't block document upload
|
// 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
|
// Singleton instance
|
||||||
|
|||||||
@ -25,7 +25,7 @@ interface NotificationPayload {
|
|||||||
|
|
||||||
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
|
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
|
||||||
|
|
||||||
class NotificationService {
|
export class NotificationService {
|
||||||
private userIdToSubscriptions: Map<string, PushSubscription[]> = new Map();
|
private userIdToSubscriptions: Map<string, PushSubscription[]> = new Map();
|
||||||
|
|
||||||
configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) {
|
configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) {
|
||||||
@ -307,7 +307,8 @@ class NotificationService {
|
|||||||
'einvoice_generated': EmailNotificationType.EINVOICE_GENERATED,
|
'einvoice_generated': EmailNotificationType.EINVOICE_GENERATED,
|
||||||
'credit_note_sent': EmailNotificationType.CREDIT_NOTE_SENT,
|
'credit_note_sent': EmailNotificationType.CREDIT_NOTE_SENT,
|
||||||
'pause_retrigger_request': EmailNotificationType.WORKFLOW_PAUSED, // Use same template as pause
|
'pause_retrigger_request': EmailNotificationType.WORKFLOW_PAUSED, // Use same template as pause
|
||||||
'pause_retriggered': null
|
'pause_retriggered': null,
|
||||||
|
'summary_shared': EmailNotificationType.SUMMARY_SHARED
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailType = emailTypeMap[payload.type || ''];
|
const emailType = emailTypeMap[payload.type || ''];
|
||||||
@ -598,7 +599,7 @@ class NotificationService {
|
|||||||
const currentLevel = await ApprovalLevel.findOne({
|
const currentLevel = await ApprovalLevel.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId: payload.requestId,
|
requestId: payload.requestId,
|
||||||
status: 'PENDING'
|
status: 'IN_PROGRESS'
|
||||||
},
|
},
|
||||||
order: [['levelNumber', 'ASC']]
|
order: [['levelNumber', 'ASC']]
|
||||||
});
|
});
|
||||||
@ -808,6 +809,22 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
break;
|
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':
|
case 'pause_retrigger_request':
|
||||||
{
|
{
|
||||||
// This is when initiator requests approver to resume a paused workflow
|
// This is when initiator requests approver to resume a paused workflow
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import '@models/index'; // Ensure associations are loaded
|
|||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import logger from '@utils/logger';
|
import logger from '@utils/logger';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { NotificationService } from './notification.service';
|
||||||
|
|
||||||
export class SummaryService {
|
export class SummaryService {
|
||||||
/**
|
/**
|
||||||
@ -674,6 +675,36 @@ export class SummaryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[Summary] Shared summary ${summaryId} with ${sharedSummaries.length} users`);
|
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;
|
return sharedSummaries;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[Summary] Failed to share summary ${summaryId}:`, error);
|
logger.error(`[Summary] Failed to share summary ${summaryId}:`, error);
|
||||||
|
|||||||
@ -1,83 +0,0 @@
|
|||||||
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();
|
|
||||||
Loading…
Reference in New Issue
Block a user