327 lines
14 KiB
TypeScript
327 lines
14 KiB
TypeScript
/**
|
|
* Dealer Claim Email Service
|
|
*
|
|
* Dedicated service for handling email template selection and sending
|
|
* for dealer claim workflows (CLAIM_MANAGEMENT).
|
|
*
|
|
* This service is separate from the main notification service to:
|
|
* - Isolate dealer claim-specific logic
|
|
* - Prevent breaking custom workflows
|
|
* - Handle dynamic step identification (by levelName, not levelNumber)
|
|
* - Support additional approvers between steps
|
|
*/
|
|
|
|
import { ApprovalLevel } from '@models/ApprovalLevel';
|
|
import { User } from '@models/User';
|
|
import logger from '@utils/logger';
|
|
import { IWorkflowEmailService } from './workflowEmail.interface';
|
|
import { emailNotificationService } from './emailNotification.service';
|
|
|
|
export class DealerClaimEmailService implements IWorkflowEmailService {
|
|
/**
|
|
* Determine and send the appropriate email template for dealer claim assignment notifications
|
|
* Handles:
|
|
* - Dealer Proposal Step (Step 1)
|
|
* - Dealer Completion Documents Step (Step 4)
|
|
* - Standard approval steps (Steps 2, 3, 5)
|
|
* - Additional approvers (always use standard template)
|
|
*/
|
|
async sendAssignmentEmail(
|
|
requestData: any,
|
|
approverUser: User,
|
|
initiatorData: any,
|
|
currentLevel: ApprovalLevel | null,
|
|
allLevels: ApprovalLevel[]
|
|
): Promise<void> {
|
|
try {
|
|
// SAFETY CHECK: Ensure this is actually a dealer claim workflow
|
|
// This prevents dealer-specific logic from being applied to custom workflows
|
|
const workflowType = requestData.workflowType || requestData.templateType || 'CUSTOM';
|
|
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
|
logger.warn(`[DealerClaimEmail] ⚠️ Wrong workflow type (${workflowType}) - falling back to standard email. This service should only handle CLAIM_MANAGEMENT workflows.`);
|
|
// Fall back to standard approval email
|
|
const approverData = approverUser.toJSON();
|
|
if (currentLevel) {
|
|
(approverData as any).levelNumber = (currentLevel as any).levelNumber;
|
|
}
|
|
const isMultiLevel = allLevels.length > 1;
|
|
const { emailNotificationService } = await import('./emailNotification.service');
|
|
await emailNotificationService.sendApprovalRequest(
|
|
requestData,
|
|
approverData,
|
|
initiatorData,
|
|
isMultiLevel,
|
|
isMultiLevel ? allLevels.map((l: any) => l.toJSON()) : undefined
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!currentLevel) {
|
|
logger.warn(`[DealerClaimEmail] No current level found, sending standard approval email`);
|
|
await this.sendStandardApprovalEmail(requestData, approverUser, initiatorData, currentLevel);
|
|
return;
|
|
}
|
|
|
|
// Reload level from DB to ensure we have the latest levelName
|
|
const level = await ApprovalLevel.findByPk((currentLevel as any).levelId) || currentLevel;
|
|
const levelName = (level.levelName || '').toLowerCase().trim();
|
|
|
|
logger.info(`[DealerClaimEmail] Level: "${level.levelName}" (${level.levelNumber}), Approver: ${approverUser.email}`);
|
|
|
|
// Check if it's an additional approver (always use standard template)
|
|
// Additional approvers can have various levelName formats:
|
|
// - "Additional Approver" (from addApproverAtLevel)
|
|
// - "Additional Approver - Level X" (fallback)
|
|
// - "Additional Approver - ${designation}" (from addApproverAtLevel with designation)
|
|
// - Custom stepName from frontend (when isAdditional=true)
|
|
const isAdditionalApprover = levelName.includes('additional approver') ||
|
|
(levelName.includes('additional') && levelName.includes('approver'));
|
|
|
|
if (isAdditionalApprover) {
|
|
logger.info(`[DealerClaimEmail] ✅ Additional approver detected - sending standard approval email`);
|
|
await this.sendStandardApprovalEmail(requestData, approverUser, initiatorData, level);
|
|
return;
|
|
}
|
|
|
|
// SIMPLE DETECTION: Use levelName as the primary source of truth
|
|
// Level names are always set correctly:
|
|
// - "Dealer Proposal Submission" (Step 1)
|
|
// - "Dealer Completion Documents" (Step 4)
|
|
const isDealerProposalStep = levelName.includes('dealer') && levelName.includes('proposal');
|
|
const isDealerCompletionStep = levelName.includes('dealer') &&
|
|
(levelName.includes('completion') || levelName.includes('documents')) &&
|
|
!levelName.includes('proposal'); // Explicitly exclude proposal
|
|
|
|
// Route to appropriate template
|
|
if (isDealerCompletionStep) {
|
|
logger.info(`[DealerClaimEmail] ✅ DEALER COMPLETION step - sending completion documents required email`);
|
|
await this.sendDealerCompletionRequiredEmail(requestData, approverUser, initiatorData, level);
|
|
} else if (isDealerProposalStep) {
|
|
logger.info(`[DealerClaimEmail] ✅ DEALER PROPOSAL step - sending proposal required email`);
|
|
await this.sendDealerProposalRequiredEmail(requestData, approverUser, initiatorData, level);
|
|
} else {
|
|
logger.info(`[DealerClaimEmail] ✅ STANDARD approval step - sending standard approval email`);
|
|
await this.sendStandardApprovalEmail(requestData, approverUser, initiatorData, level);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`[DealerClaimEmail] Error sending assignment email:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send dealer proposal required email
|
|
*/
|
|
private async sendDealerProposalRequiredEmail(
|
|
requestData: any,
|
|
dealerUser: User,
|
|
initiatorData: any,
|
|
currentLevel: ApprovalLevel | null
|
|
): Promise<void> {
|
|
logger.info(`[DealerClaimEmail] Sending dealer proposal required email to ${dealerUser.email}`);
|
|
|
|
// Get claim details for dealer-specific data
|
|
const { DealerClaimDetails } = await import('@models/DealerClaimDetails');
|
|
const claimDetails = await DealerClaimDetails.findOne({
|
|
where: { requestId: requestData.requestId }
|
|
});
|
|
|
|
const claimData = claimDetails ? (claimDetails as any).toJSON() : {};
|
|
|
|
await emailNotificationService.sendDealerProposalRequired(
|
|
requestData,
|
|
dealerUser.toJSON(),
|
|
initiatorData,
|
|
{
|
|
activityName: claimData.activityName || requestData.title,
|
|
activityType: claimData.activityType || 'N/A',
|
|
activityDate: claimData.activityDate,
|
|
location: claimData.location,
|
|
estimatedBudget: claimData.estimatedBudget,
|
|
dealerName: claimData.dealerName,
|
|
tatHours: currentLevel ? (currentLevel as any).tatHours : undefined
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Send dealer completion documents required email
|
|
*/
|
|
private async sendDealerCompletionRequiredEmail(
|
|
requestData: any,
|
|
dealerUser: User,
|
|
initiatorData: any,
|
|
currentLevel: ApprovalLevel | null
|
|
): Promise<void> {
|
|
logger.info(`[DealerClaimEmail] Sending dealer completion documents required email to ${dealerUser.email}`);
|
|
|
|
// Get claim details for dealer-specific data
|
|
const { DealerClaimDetails } = await import('@models/DealerClaimDetails');
|
|
const claimDetails = await DealerClaimDetails.findOne({
|
|
where: { requestId: requestData.requestId }
|
|
});
|
|
|
|
const claimData = claimDetails ? (claimDetails as any).toJSON() : {};
|
|
|
|
// Use dedicated completion documents required template
|
|
await emailNotificationService.sendDealerCompletionRequired(
|
|
requestData,
|
|
dealerUser.toJSON(),
|
|
initiatorData,
|
|
{
|
|
activityName: claimData.activityName || requestData.title,
|
|
activityType: claimData.activityType || 'N/A',
|
|
activityDate: claimData.activityDate,
|
|
location: claimData.location,
|
|
estimatedBudget: claimData.estimatedBudget,
|
|
dealerName: claimData.dealerName,
|
|
tatHours: currentLevel ? (currentLevel as any).tatHours : undefined
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Send standard approval email (single approver template)
|
|
* For dealer claim workflows, enrich with dealer claim-specific details
|
|
*/
|
|
private async sendStandardApprovalEmail(
|
|
requestData: any,
|
|
approverUser: User,
|
|
initiatorData: any,
|
|
currentLevel: ApprovalLevel | null
|
|
): Promise<void> {
|
|
logger.info(`[DealerClaimEmail] Sending enhanced approval email to ${approverUser.email}`);
|
|
|
|
// Get dealer claim details to enrich the email
|
|
const { DealerClaimDetails } = await import('@models/DealerClaimDetails');
|
|
const { DealerProposalDetails } = await import('@models/DealerProposalDetails');
|
|
|
|
const claimDetails = await DealerClaimDetails.findOne({
|
|
where: { requestId: requestData.requestId }
|
|
});
|
|
|
|
const proposalDetails = await DealerProposalDetails.findOne({
|
|
where: { requestId: requestData.requestId }
|
|
});
|
|
|
|
// Determine stage-specific instructions
|
|
let stageInstructions = '';
|
|
const name = (currentLevel?.levelName || '').toLowerCase();
|
|
|
|
if (name.includes('evaluation') || name.includes('requestor evaluation')) {
|
|
stageInstructions = 'Please evaluate the proposal submitted by the dealer. Verify the activity details and estimated budget.';
|
|
} else if (name.includes('lead') || name.includes('department lead')) {
|
|
stageInstructions = 'The requestor has evaluated this proposal and recommended it for your approval. Please review and provide your authorization.';
|
|
} else if (name.includes('claim approval') && name.includes('requestor')) {
|
|
stageInstructions = 'The dealer has submitted completion documents. Please verify the expenses and documents before providing final claim approval.';
|
|
}
|
|
|
|
// Enrich requestData with dealer claim-specific information
|
|
const enrichedRequestData = {
|
|
...requestData,
|
|
// Add dealer claim details to description if not already present
|
|
description: this.enrichDescriptionWithClaimDetails(
|
|
requestData.description || '',
|
|
claimDetails,
|
|
proposalDetails
|
|
),
|
|
// Add activity information
|
|
activityName: claimDetails ? (claimDetails as any).activityName : undefined,
|
|
activityType: claimDetails ? (claimDetails as any).activityType : undefined,
|
|
dealerName: claimDetails ? (claimDetails as any).dealerName : undefined,
|
|
dealerCode: claimDetails ? (claimDetails as any).dealerCode : undefined,
|
|
location: claimDetails ? (claimDetails as any).location : undefined,
|
|
proposalBudget: proposalDetails ? (proposalDetails as any).totalEstimatedBudget : undefined
|
|
};
|
|
|
|
const approverData = approverUser.toJSON();
|
|
|
|
// Add level number if available
|
|
if (currentLevel) {
|
|
(approverData as any).levelNumber = (currentLevel as any).levelNumber;
|
|
}
|
|
|
|
// Always use single approver template for dealer claim workflows
|
|
// (not multi-level, even if there are multiple steps)
|
|
await emailNotificationService.sendApprovalRequest(
|
|
enrichedRequestData,
|
|
approverData,
|
|
initiatorData,
|
|
false, // isMultiLevel = false for dealer claim workflows
|
|
undefined, // No approval chain needed
|
|
stageInstructions // Pass as customMessage (contextual instruction)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Enrich request description with dealer claim-specific details
|
|
*/
|
|
private enrichDescriptionWithClaimDetails(
|
|
existingDescription: string,
|
|
claimDetails: any,
|
|
proposalDetails: any
|
|
): string {
|
|
if (!claimDetails) {
|
|
return existingDescription;
|
|
}
|
|
|
|
const claimData = (claimDetails as any).toJSON();
|
|
let enrichedDescription = existingDescription || '';
|
|
|
|
// Add dealer claim details section if not already present
|
|
const detailsSection = `
|
|
<div style="margin-top: 20px; padding: 15px; background-color: #f8f9fa; border-left: 4px solid #667eea; border-radius: 4px;">
|
|
<h3 style="margin: 0 0 15px; color: #333333; font-size: 16px; font-weight: 600;">Claim Details:</h3>
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
${claimData.activityName ? `
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #666666; font-size: 14px; width: 140px;"><strong>Activity Name:</strong></td>
|
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">${claimData.activityName}</td>
|
|
</tr>
|
|
` : ''}
|
|
${claimData.activityType ? `
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;"><strong>Activity Type:</strong></td>
|
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">${claimData.activityType}</td>
|
|
</tr>
|
|
` : ''}
|
|
${claimData.dealerName ? `
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;"><strong>Dealer:</strong></td>
|
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">${claimData.dealerName}${claimData.dealerCode ? ` (${claimData.dealerCode})` : ''}</td>
|
|
</tr>
|
|
` : ''}
|
|
${claimData.location ? `
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;"><strong>Location:</strong></td>
|
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">${claimData.location}</td>
|
|
</tr>
|
|
` : ''}
|
|
${claimData.activityDate ? `
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;"><strong>Activity Date:</strong></td>
|
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">${new Date(claimData.activityDate).toLocaleDateString('en-IN', { year: 'numeric', month: 'long', day: 'numeric' })}</td>
|
|
</tr>
|
|
` : ''}
|
|
${proposalDetails && (proposalDetails as any).totalEstimatedBudget ? `
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;"><strong>Proposed Budget:</strong></td>
|
|
<td style="padding: 8px 0; color: #333333; font-size: 14px; font-weight: 600;">₹${Number((proposalDetails as any).totalEstimatedBudget).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
|
</tr>
|
|
` : ''}
|
|
</table>
|
|
</div>
|
|
`;
|
|
|
|
// Append details section if not already in description
|
|
if (!enrichedDescription.includes('Claim Details:') && !enrichedDescription.includes('Activity Name:')) {
|
|
enrichedDescription += detailsSection;
|
|
}
|
|
|
|
return enrichedDescription;
|
|
}
|
|
}
|
|
|
|
export const dealerClaimEmailService = new DealerClaimEmailService();
|
|
|