Re_Backend/src/services/dealerClaimEmail.service.ts

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();