693 lines
26 KiB
TypeScript
693 lines
26 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import type { AuthenticatedRequest } from '../types/express';
|
|
import { DealerClaimService } from '../services/dealerClaim.service';
|
|
import { ResponseHandler } from '../utils/responseHandler';
|
|
import logger from '../utils/logger';
|
|
import { gcsStorageService } from '../services/gcsStorage.service';
|
|
import { Document } from '../models/Document';
|
|
import { constants } from '../config/constants';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import crypto from 'crypto';
|
|
|
|
export class DealerClaimController {
|
|
private dealerClaimService = new DealerClaimService();
|
|
|
|
/**
|
|
* Create a new dealer claim request
|
|
* POST /api/v1/dealer-claims
|
|
*/
|
|
async createClaimRequest(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const userId = req.user?.userId;
|
|
if (!userId) {
|
|
return ResponseHandler.error(res, 'Unauthorized', 401);
|
|
}
|
|
|
|
const {
|
|
activityName,
|
|
activityType,
|
|
dealerCode,
|
|
dealerName,
|
|
dealerEmail,
|
|
dealerPhone,
|
|
dealerAddress,
|
|
activityDate,
|
|
location,
|
|
requestDescription,
|
|
periodStartDate,
|
|
periodEndDate,
|
|
estimatedBudget,
|
|
} = req.body;
|
|
|
|
// Validation
|
|
if (!activityName || !activityType || !dealerCode || !dealerName || !location || !requestDescription) {
|
|
return ResponseHandler.error(res, 'Missing required fields', 400);
|
|
}
|
|
|
|
const claimRequest = await this.dealerClaimService.createClaimRequest(userId, {
|
|
activityName,
|
|
activityType,
|
|
dealerCode,
|
|
dealerName,
|
|
dealerEmail,
|
|
dealerPhone,
|
|
dealerAddress,
|
|
activityDate: activityDate ? new Date(activityDate) : undefined,
|
|
location,
|
|
requestDescription,
|
|
periodStartDate: periodStartDate ? new Date(periodStartDate) : undefined,
|
|
periodEndDate: periodEndDate ? new Date(periodEndDate) : undefined,
|
|
estimatedBudget: estimatedBudget ? parseFloat(estimatedBudget) : undefined,
|
|
});
|
|
|
|
return ResponseHandler.success(res, {
|
|
request: claimRequest,
|
|
message: 'Claim request created successfully'
|
|
}, 'Claim request created');
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
logger.error('[DealerClaimController] Error creating claim request:', error);
|
|
return ResponseHandler.error(res, 'Failed to create claim request', 500, errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get claim details
|
|
* GET /api/v1/dealer-claims/:requestId
|
|
* Accepts either UUID or requestNumber
|
|
*/
|
|
async getClaimDetails(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const identifier = req.params.requestId; // Can be UUID or requestNumber
|
|
|
|
// Find workflow to get actual UUID
|
|
const workflow = await this.findWorkflowByIdentifier(identifier);
|
|
if (!workflow) {
|
|
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
|
}
|
|
|
|
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
|
if (!requestId) {
|
|
return ResponseHandler.error(res, 'Invalid workflow request', 400);
|
|
}
|
|
|
|
const claimDetails = await this.dealerClaimService.getClaimDetails(requestId);
|
|
|
|
return ResponseHandler.success(res, claimDetails, 'Claim details fetched');
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
logger.error('[DealerClaimController] Error getting claim details:', error);
|
|
return ResponseHandler.error(res, 'Failed to fetch claim details', 500, errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to find workflow by either requestId (UUID) or requestNumber
|
|
*/
|
|
private async findWorkflowByIdentifier(identifier: string): Promise<any> {
|
|
const isUuid = (id: string): boolean => {
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
return uuidRegex.test(id);
|
|
};
|
|
|
|
const { WorkflowRequest } = await import('../models/WorkflowRequest');
|
|
if (isUuid(identifier)) {
|
|
return await WorkflowRequest.findByPk(identifier);
|
|
} else {
|
|
return await WorkflowRequest.findOne({ where: { requestNumber: identifier } });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Submit dealer proposal (Step 1)
|
|
* POST /api/v1/dealer-claims/:requestId/proposal
|
|
* Accepts either UUID or requestNumber
|
|
*/
|
|
async submitProposal(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const identifier = req.params.requestId; // Can be UUID or requestNumber
|
|
const userId = req.user?.userId;
|
|
const {
|
|
costBreakup,
|
|
totalEstimatedBudget,
|
|
timelineMode,
|
|
expectedCompletionDate,
|
|
expectedCompletionDays,
|
|
dealerComments,
|
|
} = req.body;
|
|
|
|
// Find workflow by identifier (UUID or requestNumber)
|
|
const workflow = await this.findWorkflowByIdentifier(identifier);
|
|
if (!workflow) {
|
|
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
|
}
|
|
|
|
// Get actual UUID and requestNumber
|
|
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
|
const requestNumber = (workflow as any).requestNumber || (workflow as any).request_number;
|
|
|
|
if (!requestId) {
|
|
return ResponseHandler.error(res, 'Invalid workflow request', 400);
|
|
}
|
|
|
|
// Parse costBreakup - it comes as JSON string from FormData
|
|
let parsedCostBreakup: any[] = [];
|
|
if (costBreakup) {
|
|
if (typeof costBreakup === 'string') {
|
|
try {
|
|
parsedCostBreakup = JSON.parse(costBreakup);
|
|
} catch (parseError) {
|
|
logger.error('[DealerClaimController] Failed to parse costBreakup JSON:', parseError);
|
|
return ResponseHandler.error(res, 'Invalid costBreakup format. Expected JSON array.', 400);
|
|
}
|
|
} else if (Array.isArray(costBreakup)) {
|
|
parsedCostBreakup = costBreakup;
|
|
} else {
|
|
logger.warn('[DealerClaimController] costBreakup is not a string or array:', typeof costBreakup);
|
|
parsedCostBreakup = [];
|
|
}
|
|
}
|
|
|
|
// Validate costBreakup is an array
|
|
if (!Array.isArray(parsedCostBreakup)) {
|
|
logger.error('[DealerClaimController] costBreakup is not an array after parsing:', parsedCostBreakup);
|
|
return ResponseHandler.error(res, 'costBreakup must be an array of cost items', 400);
|
|
}
|
|
|
|
// Validate each cost item has required fields
|
|
for (const item of parsedCostBreakup) {
|
|
if (!item.description || item.amount === undefined || item.amount === null) {
|
|
return ResponseHandler.error(res, 'Each cost item must have description and amount', 400);
|
|
}
|
|
}
|
|
|
|
// Handle file upload if present
|
|
let proposalDocumentPath: string | undefined;
|
|
let proposalDocumentUrl: string | undefined;
|
|
|
|
if (req.file) {
|
|
const file = req.file;
|
|
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
|
|
|
|
const uploadResult = await gcsStorageService.uploadFileWithFallback({
|
|
buffer: fileBuffer,
|
|
originalName: file.originalname,
|
|
mimeType: file.mimetype,
|
|
requestNumber: requestNumber || 'UNKNOWN',
|
|
fileType: 'documents'
|
|
});
|
|
|
|
proposalDocumentPath = uploadResult.filePath;
|
|
proposalDocumentUrl = uploadResult.storageUrl;
|
|
|
|
// Cleanup local file if exists
|
|
if (file.path && fs.existsSync(file.path)) {
|
|
fs.unlinkSync(file.path);
|
|
}
|
|
}
|
|
|
|
// Use actual UUID for service call with parsed costBreakup array
|
|
await this.dealerClaimService.submitDealerProposal(requestId, {
|
|
proposalDocumentPath,
|
|
proposalDocumentUrl,
|
|
costBreakup: parsedCostBreakup, // Use parsed array
|
|
totalEstimatedBudget: totalEstimatedBudget ? parseFloat(totalEstimatedBudget) : 0,
|
|
timelineMode: timelineMode || 'date',
|
|
expectedCompletionDate: expectedCompletionDate ? new Date(expectedCompletionDate) : undefined,
|
|
expectedCompletionDays: expectedCompletionDays ? parseInt(expectedCompletionDays) : undefined,
|
|
dealerComments: dealerComments || '',
|
|
});
|
|
|
|
return ResponseHandler.success(res, { message: 'Proposal submitted successfully' }, 'Proposal submitted');
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
logger.error('[DealerClaimController] Error submitting proposal:', error);
|
|
return ResponseHandler.error(res, 'Failed to submit proposal', 500, errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Submit completion documents (Step 5)
|
|
* POST /api/v1/dealer-claims/:requestId/completion
|
|
* Accepts either UUID or requestNumber
|
|
*/
|
|
async submitCompletion(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const identifier = req.params.requestId; // Can be UUID or requestNumber
|
|
const {
|
|
activityCompletionDate,
|
|
numberOfParticipants,
|
|
closedExpenses,
|
|
totalClosedExpenses,
|
|
} = req.body;
|
|
|
|
// Parse closedExpenses if it's a JSON string
|
|
let parsedClosedExpenses: any[] = [];
|
|
if (closedExpenses) {
|
|
try {
|
|
parsedClosedExpenses = typeof closedExpenses === 'string' ? JSON.parse(closedExpenses) : closedExpenses;
|
|
} catch (e) {
|
|
logger.warn('[DealerClaimController] Failed to parse closedExpenses JSON:', e);
|
|
parsedClosedExpenses = [];
|
|
}
|
|
}
|
|
|
|
// Get files from multer
|
|
const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined;
|
|
const completionDocumentsFiles = files?.completionDocuments || [];
|
|
const activityPhotosFiles = files?.activityPhotos || [];
|
|
const invoicesReceiptsFiles = files?.invoicesReceipts || [];
|
|
const attendanceSheetFile = files?.attendanceSheet?.[0];
|
|
|
|
// Find workflow to get actual UUID
|
|
const workflow = await this.findWorkflowByIdentifier(identifier);
|
|
if (!workflow) {
|
|
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
|
}
|
|
|
|
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
|
const requestNumber = (workflow as any).requestNumber || (workflow as any).request_number || 'UNKNOWN';
|
|
if (!requestId) {
|
|
return ResponseHandler.error(res, 'Invalid workflow request', 400);
|
|
}
|
|
|
|
const userId = (req as any).user?.userId || (req as any).user?.user_id;
|
|
if (!userId) {
|
|
return ResponseHandler.error(res, 'User not authenticated', 401);
|
|
}
|
|
|
|
if (!activityCompletionDate) {
|
|
return ResponseHandler.error(res, 'Activity completion date is required', 400);
|
|
}
|
|
|
|
// Upload files to GCS and save to documents table
|
|
const completionDocuments: any[] = [];
|
|
const activityPhotos: any[] = [];
|
|
|
|
// Upload and save completion documents to documents table with COMPLETION_DOC category
|
|
for (const file of completionDocumentsFiles) {
|
|
try {
|
|
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
|
|
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
|
|
const uploadResult = await gcsStorageService.uploadFileWithFallback({
|
|
buffer: fileBuffer,
|
|
originalName: file.originalname,
|
|
mimeType: file.mimetype,
|
|
requestNumber: requestNumber,
|
|
fileType: 'documents'
|
|
});
|
|
|
|
const extension = path.extname(file.originalname).replace('.', '').toLowerCase();
|
|
|
|
// Save to documents table
|
|
const doc = await Document.create({
|
|
requestId,
|
|
uploadedBy: userId,
|
|
fileName: path.basename(file.filename || file.originalname),
|
|
originalFileName: file.originalname,
|
|
fileType: extension,
|
|
fileExtension: extension,
|
|
fileSize: file.size,
|
|
filePath: uploadResult.filePath,
|
|
storageUrl: uploadResult.storageUrl,
|
|
mimeType: file.mimetype,
|
|
checksum,
|
|
isGoogleDoc: false,
|
|
googleDocUrl: null as any,
|
|
category: constants.DOCUMENT_CATEGORIES.COMPLETION_DOC,
|
|
version: 1,
|
|
parentDocumentId: null as any,
|
|
isDeleted: false,
|
|
downloadCount: 0,
|
|
} as any);
|
|
|
|
completionDocuments.push({
|
|
documentId: doc.documentId,
|
|
name: file.originalname,
|
|
url: uploadResult.storageUrl,
|
|
size: file.size,
|
|
type: file.mimetype,
|
|
});
|
|
|
|
// Cleanup local file if exists
|
|
if (file.path && fs.existsSync(file.path)) {
|
|
try {
|
|
fs.unlinkSync(file.path);
|
|
} catch (unlinkError) {
|
|
logger.warn(`[DealerClaimController] Failed to delete local file ${file.path}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error(`[DealerClaimController] Error uploading completion document ${file.originalname}:`, error);
|
|
}
|
|
}
|
|
|
|
// Upload and save activity photos to documents table with ACTIVITY_PHOTO category
|
|
for (const file of activityPhotosFiles) {
|
|
try {
|
|
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
|
|
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
|
|
const uploadResult = await gcsStorageService.uploadFileWithFallback({
|
|
buffer: fileBuffer,
|
|
originalName: file.originalname,
|
|
mimeType: file.mimetype,
|
|
requestNumber: requestNumber,
|
|
fileType: 'attachments'
|
|
});
|
|
|
|
const extension = path.extname(file.originalname).replace('.', '').toLowerCase();
|
|
|
|
// Save to documents table
|
|
const doc = await Document.create({
|
|
requestId,
|
|
uploadedBy: userId,
|
|
fileName: path.basename(file.filename || file.originalname),
|
|
originalFileName: file.originalname,
|
|
fileType: extension,
|
|
fileExtension: extension,
|
|
fileSize: file.size,
|
|
filePath: uploadResult.filePath,
|
|
storageUrl: uploadResult.storageUrl,
|
|
mimeType: file.mimetype,
|
|
checksum,
|
|
isGoogleDoc: false,
|
|
googleDocUrl: null as any,
|
|
category: constants.DOCUMENT_CATEGORIES.ACTIVITY_PHOTO,
|
|
version: 1,
|
|
parentDocumentId: null as any,
|
|
isDeleted: false,
|
|
downloadCount: 0,
|
|
} as any);
|
|
|
|
activityPhotos.push({
|
|
documentId: doc.documentId,
|
|
name: file.originalname,
|
|
url: uploadResult.storageUrl,
|
|
size: file.size,
|
|
type: file.mimetype,
|
|
});
|
|
|
|
// Cleanup local file if exists
|
|
if (file.path && fs.existsSync(file.path)) {
|
|
try {
|
|
fs.unlinkSync(file.path);
|
|
} catch (unlinkError) {
|
|
logger.warn(`[DealerClaimController] Failed to delete local file ${file.path}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error(`[DealerClaimController] Error uploading activity photo ${file.originalname}:`, error);
|
|
}
|
|
}
|
|
|
|
// Upload and save invoices/receipts to documents table with SUPPORTING category
|
|
const invoicesReceipts: any[] = [];
|
|
for (const file of invoicesReceiptsFiles) {
|
|
try {
|
|
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
|
|
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
|
|
const uploadResult = await gcsStorageService.uploadFileWithFallback({
|
|
buffer: fileBuffer,
|
|
originalName: file.originalname,
|
|
mimeType: file.mimetype,
|
|
requestNumber: requestNumber,
|
|
fileType: 'attachments'
|
|
});
|
|
|
|
const extension = path.extname(file.originalname).replace('.', '').toLowerCase();
|
|
|
|
// Save to documents table
|
|
const doc = await Document.create({
|
|
requestId,
|
|
uploadedBy: userId,
|
|
fileName: path.basename(file.filename || file.originalname),
|
|
originalFileName: file.originalname,
|
|
fileType: extension,
|
|
fileExtension: extension,
|
|
fileSize: file.size,
|
|
filePath: uploadResult.filePath,
|
|
storageUrl: uploadResult.storageUrl,
|
|
mimeType: file.mimetype,
|
|
checksum,
|
|
isGoogleDoc: false,
|
|
googleDocUrl: null as any,
|
|
category: constants.DOCUMENT_CATEGORIES.SUPPORTING,
|
|
version: 1,
|
|
parentDocumentId: null as any,
|
|
isDeleted: false,
|
|
downloadCount: 0,
|
|
} as any);
|
|
|
|
invoicesReceipts.push({
|
|
documentId: doc.documentId,
|
|
name: file.originalname,
|
|
url: uploadResult.storageUrl,
|
|
size: file.size,
|
|
type: file.mimetype,
|
|
});
|
|
|
|
// Cleanup local file if exists
|
|
if (file.path && fs.existsSync(file.path)) {
|
|
try {
|
|
fs.unlinkSync(file.path);
|
|
} catch (unlinkError) {
|
|
logger.warn(`[DealerClaimController] Failed to delete local file ${file.path}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error(`[DealerClaimController] Error uploading invoice/receipt ${file.originalname}:`, error);
|
|
}
|
|
}
|
|
|
|
// Upload and save attendance sheet to documents table with SUPPORTING category
|
|
let attendanceSheet: any = null;
|
|
if (attendanceSheetFile) {
|
|
try {
|
|
const fileBuffer = attendanceSheetFile.buffer || (attendanceSheetFile.path ? fs.readFileSync(attendanceSheetFile.path) : Buffer.from(''));
|
|
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
|
|
const uploadResult = await gcsStorageService.uploadFileWithFallback({
|
|
buffer: fileBuffer,
|
|
originalName: attendanceSheetFile.originalname,
|
|
mimeType: attendanceSheetFile.mimetype,
|
|
requestNumber: requestNumber,
|
|
fileType: 'attachments'
|
|
});
|
|
|
|
const extension = path.extname(attendanceSheetFile.originalname).replace('.', '').toLowerCase();
|
|
|
|
// Save to documents table
|
|
const doc = await Document.create({
|
|
requestId,
|
|
uploadedBy: userId,
|
|
fileName: path.basename(attendanceSheetFile.filename || attendanceSheetFile.originalname),
|
|
originalFileName: attendanceSheetFile.originalname,
|
|
fileType: extension,
|
|
fileExtension: extension,
|
|
fileSize: attendanceSheetFile.size,
|
|
filePath: uploadResult.filePath,
|
|
storageUrl: uploadResult.storageUrl,
|
|
mimeType: attendanceSheetFile.mimetype,
|
|
checksum,
|
|
isGoogleDoc: false,
|
|
googleDocUrl: null as any,
|
|
category: constants.DOCUMENT_CATEGORIES.SUPPORTING,
|
|
version: 1,
|
|
parentDocumentId: null as any,
|
|
isDeleted: false,
|
|
downloadCount: 0,
|
|
} as any);
|
|
|
|
attendanceSheet = {
|
|
documentId: doc.documentId,
|
|
name: attendanceSheetFile.originalname,
|
|
url: uploadResult.storageUrl,
|
|
size: attendanceSheetFile.size,
|
|
type: attendanceSheetFile.mimetype,
|
|
};
|
|
|
|
// Cleanup local file if exists
|
|
if (attendanceSheetFile.path && fs.existsSync(attendanceSheetFile.path)) {
|
|
try {
|
|
fs.unlinkSync(attendanceSheetFile.path);
|
|
} catch (unlinkError) {
|
|
logger.warn(`[DealerClaimController] Failed to delete local file ${attendanceSheetFile.path}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error(`[DealerClaimController] Error uploading attendance sheet:`, error);
|
|
}
|
|
}
|
|
|
|
await this.dealerClaimService.submitCompletionDocuments(requestId, {
|
|
activityCompletionDate: new Date(activityCompletionDate),
|
|
numberOfParticipants: numberOfParticipants ? parseInt(numberOfParticipants) : undefined,
|
|
closedExpenses: parsedClosedExpenses,
|
|
totalClosedExpenses: totalClosedExpenses ? parseFloat(totalClosedExpenses) : 0,
|
|
invoicesReceipts: invoicesReceipts.length > 0 ? invoicesReceipts : undefined,
|
|
attendanceSheet: attendanceSheet || undefined,
|
|
});
|
|
|
|
return ResponseHandler.success(res, { message: 'Completion documents submitted successfully' }, 'Completion submitted');
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
logger.error('[DealerClaimController] Error submitting completion:', error);
|
|
return ResponseHandler.error(res, 'Failed to submit completion documents', 500, errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update IO details (Step 3 - Department Lead)
|
|
* PUT /api/v1/dealer-claims/:requestId/io
|
|
* Accepts either UUID or requestNumber
|
|
*/
|
|
async updateIODetails(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
const userId = (req as any).user?.userId || (req as any).user?.user_id;
|
|
try {
|
|
const identifier = req.params.requestId; // Can be UUID or requestNumber
|
|
const {
|
|
ioNumber,
|
|
ioRemark,
|
|
availableBalance,
|
|
blockedAmount,
|
|
remainingBalance,
|
|
} = req.body;
|
|
|
|
// Find workflow to get actual UUID
|
|
const workflow = await this.findWorkflowByIdentifier(identifier);
|
|
if (!workflow) {
|
|
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
|
}
|
|
|
|
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
|
if (!requestId) {
|
|
return ResponseHandler.error(res, 'Invalid workflow request', 400);
|
|
}
|
|
|
|
if (!ioNumber || availableBalance === undefined || blockedAmount === undefined) {
|
|
return ResponseHandler.error(res, 'Missing required IO fields', 400);
|
|
}
|
|
|
|
await this.dealerClaimService.updateIODetails(
|
|
requestId,
|
|
{
|
|
ioNumber,
|
|
ioRemark: ioRemark || '',
|
|
availableBalance: parseFloat(availableBalance),
|
|
blockedAmount: parseFloat(blockedAmount),
|
|
remainingBalance: remainingBalance ? parseFloat(remainingBalance) : parseFloat(availableBalance) - parseFloat(blockedAmount),
|
|
},
|
|
userId
|
|
);
|
|
|
|
return ResponseHandler.success(res, { message: 'IO details updated successfully' }, 'IO details updated');
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
logger.error('[DealerClaimController] Error updating IO details:', error);
|
|
return ResponseHandler.error(res, 'Failed to update IO details', 500, errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update e-invoice details (Step 7)
|
|
* PUT /api/v1/dealer-claims/:requestId/e-invoice
|
|
* If eInvoiceNumber is not provided, will auto-generate via DMS
|
|
* Accepts either UUID or requestNumber
|
|
*/
|
|
async updateEInvoice(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const identifier = req.params.requestId; // Can be UUID or requestNumber
|
|
const {
|
|
eInvoiceNumber,
|
|
eInvoiceDate,
|
|
dmsNumber,
|
|
amount,
|
|
description,
|
|
} = req.body;
|
|
|
|
// Find workflow to get actual UUID
|
|
const workflow = await this.findWorkflowByIdentifier(identifier);
|
|
if (!workflow) {
|
|
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
|
}
|
|
|
|
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
|
if (!requestId) {
|
|
return ResponseHandler.error(res, 'Invalid workflow request', 400);
|
|
}
|
|
|
|
// If eInvoiceNumber provided, use manual entry; otherwise auto-generate
|
|
const invoiceData = eInvoiceNumber ? {
|
|
eInvoiceNumber,
|
|
eInvoiceDate: eInvoiceDate ? new Date(eInvoiceDate) : new Date(),
|
|
dmsNumber,
|
|
} : {
|
|
amount: amount ? parseFloat(amount) : undefined,
|
|
description,
|
|
};
|
|
|
|
await this.dealerClaimService.updateEInvoiceDetails(requestId, invoiceData);
|
|
|
|
return ResponseHandler.success(res, { message: 'E-Invoice details updated successfully' }, 'E-Invoice updated');
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
logger.error('[DealerClaimController] Error updating e-invoice:', error);
|
|
return ResponseHandler.error(res, 'Failed to update e-invoice details', 500, errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update credit note details (Step 8)
|
|
* PUT /api/v1/dealer-claims/:requestId/credit-note
|
|
* If creditNoteNumber is not provided, will auto-generate via DMS
|
|
* Accepts either UUID or requestNumber
|
|
*/
|
|
async updateCreditNote(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const identifier = req.params.requestId; // Can be UUID or requestNumber
|
|
const {
|
|
creditNoteNumber,
|
|
creditNoteDate,
|
|
creditNoteAmount,
|
|
reason,
|
|
description,
|
|
} = req.body;
|
|
|
|
// Find workflow to get actual UUID
|
|
const workflow = await this.findWorkflowByIdentifier(identifier);
|
|
if (!workflow) {
|
|
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
|
}
|
|
|
|
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
|
if (!requestId) {
|
|
return ResponseHandler.error(res, 'Invalid workflow request', 400);
|
|
}
|
|
|
|
// If creditNoteNumber provided, use manual entry; otherwise auto-generate
|
|
const creditNoteData = creditNoteNumber ? {
|
|
creditNoteNumber,
|
|
creditNoteDate: creditNoteDate ? new Date(creditNoteDate) : new Date(),
|
|
creditNoteAmount: creditNoteAmount ? parseFloat(creditNoteAmount) : undefined,
|
|
} : {
|
|
creditNoteAmount: creditNoteAmount ? parseFloat(creditNoteAmount) : undefined,
|
|
reason,
|
|
description,
|
|
};
|
|
|
|
await this.dealerClaimService.updateCreditNoteDetails(requestId, creditNoteData);
|
|
|
|
return ResponseHandler.success(res, { message: 'Credit note details updated successfully' }, 'Credit note updated');
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
logger.error('[DealerClaimController] Error updating credit note:', error);
|
|
return ResponseHandler.error(res, 'Failed to update credit note details', 500, errorMessage);
|
|
}
|
|
}
|
|
}
|
|
|