/** * 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 { 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 { 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 { 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 { 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 = `

Claim Details:

${claimData.activityName ? ` ` : ''} ${claimData.activityType ? ` ` : ''} ${claimData.dealerName ? ` ` : ''} ${claimData.location ? ` ` : ''} ${claimData.activityDate ? ` ` : ''} ${proposalDetails && (proposalDetails as any).totalEstimatedBudget ? ` ` : ''}
Activity Name: ${claimData.activityName}
Activity Type: ${claimData.activityType}
Dealer: ${claimData.dealerName}${claimData.dealerCode ? ` (${claimData.dealerCode})` : ''}
Location: ${claimData.location}
Activity Date: ${new Date(claimData.activityDate).toLocaleDateString('en-IN', { year: 'numeric', month: 'long', day: 'numeric' })}
Proposed Budget: ₹${Number((proposalDetails as any).totalEstimatedBudget).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
`; // 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();