/** * Mock API Service * * Purpose: Simulates backend API for development and testing * Provides realistic API responses with proper structure, error handling, and validation * * This replaces localStorage approach with a more realistic API simulation */ // API Response Types interface ApiResponse { success: boolean; data?: T; message?: string; error?: { code: string; message: string; details?: any; }; meta?: { timestamp: string; requestId?: string; version?: string; }; } interface PaginatedResponse extends ApiResponse { pagination?: { page: number; limit: number; total: number; totalPages: number; }; } // In-memory data store const mockDatabase: { requests: Map; approvalFlows: Map; documents: Map; activities: Map; ioBlocks: Map; } = { requests: new Map(), approvalFlows: new Map(), documents: new Map(), activities: new Map(), ioBlocks: new Map(), }; // Helper to simulate realistic API delay const delay = (min: number = 300, max: number = 800): Promise => { const delayTime = Math.floor(Math.random() * (max - min + 1)) + min; return new Promise(resolve => setTimeout(resolve, delayTime)); }; // Helper to create success response function successResponse(data: T, message?: string): ApiResponse { return { success: true, data, message: message || 'Operation completed successfully', meta: { timestamp: new Date().toISOString(), version: '1.0', }, }; } // Helper to create error response function errorResponse(code: string, message: string, details?: any): ApiResponse { return { success: false, error: { code, message, details, }, meta: { timestamp: new Date().toISOString(), version: '1.0', }, }; } // Helper to validate request ID function validateRequestId(requestId: string | undefined | null): string { if (!requestId) { throw new Error('REQUEST_ID_REQUIRED'); } return requestId; } /** * Generate unique ID */ function generateId(prefix: string = 'req'): string { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 9); return `${prefix}-${timestamp}-${random}`; } /** * Mock API Service Class */ class MockApiService { /** * Create a new request */ async createRequest(requestData: any): Promise> { try { await delay(600, 1000); // Validation if (!requestData) { return errorResponse('VALIDATION_ERROR', 'Request data is required'); } const requestId = requestData.requestId || `RE-REQ-${new Date().getFullYear()}-CM-${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`; const now = new Date().toISOString(); // Check if request ID already exists if (mockDatabase.requests.has(requestId)) { return errorResponse('DUPLICATE_REQUEST', `Request with ID ${requestId} already exists`); } const request = { ...requestData, id: requestId, requestId: requestId, requestNumber: requestId, createdAt: now, updatedAt: now, version: 1, }; mockDatabase.requests.set(requestId, request); mockDatabase.approvalFlows.set(requestId, []); mockDatabase.documents.set(requestId, []); mockDatabase.activities.set(requestId, []); console.log('[MockAPI] ✅ Request created:', requestId); return successResponse(request, 'Request created successfully'); } catch (error: any) { console.error('[MockAPI] ❌ Error creating request:', error); return errorResponse('INTERNAL_ERROR', 'Failed to create request', error.message); } } /** * Get request by ID */ async getRequest(requestId: string): Promise> { try { await delay(200, 500); const id = validateRequestId(requestId); const request = mockDatabase.requests.get(id); if (!request) { return errorResponse('NOT_FOUND', `Request with ID ${id} not found`); } // Attach related data const approvalFlow = (mockDatabase.approvalFlows.get(id) || []).sort((a: any, b: any) => a.step - b.step); const documents = (mockDatabase.documents.get(id) || []).sort((a: any, b: any) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime() ); const activities = (mockDatabase.activities.get(id) || []).sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); const ioBlock = mockDatabase.ioBlocks.get(id) || null; const enrichedRequest = { ...request, approvalFlow, documents, activities, auditTrail: activities, ioBlock, _meta: { approvalFlowCount: approvalFlow.length, documentCount: documents.length, activityCount: activities.length, hasIOBlock: !!ioBlock, }, }; return successResponse(enrichedRequest); } catch (error: any) { console.error('[MockAPI] ❌ Error fetching request:', error); return errorResponse('INTERNAL_ERROR', 'Failed to fetch request', error.message); } } /** * Update request */ async updateRequest(requestId: string, updates: any): Promise> { try { await delay(300, 600); const id = validateRequestId(requestId); const request = mockDatabase.requests.get(id); if (!request) { return errorResponse('NOT_FOUND', `Request with ID ${id} not found`); } // Validation: prevent invalid status transitions if (updates.status && request.status === 'cancelled' && updates.status !== 'cancelled') { return errorResponse('INVALID_TRANSITION', 'Cannot change status of a cancelled request'); } const updated = { ...request, ...updates, updatedAt: new Date().toISOString(), version: (request.version || 1) + 1, }; mockDatabase.requests.set(id, updated); console.log('[MockAPI] ✅ Request updated:', id, Object.keys(updates)); return successResponse(updated, 'Request updated successfully'); } catch (error: any) { console.error('[MockAPI] ❌ Error updating request:', error); return errorResponse('INTERNAL_ERROR', 'Failed to update request', error.message); } } /** * Create approval flow step */ async createApprovalFlow(requestId: string, flowData: any): Promise> { try { await delay(200, 400); const id = validateRequestId(requestId); // Validate request exists if (!mockDatabase.requests.has(id)) { return errorResponse('NOT_FOUND', `Request with ID ${id} not found`); } // Validation if (!flowData.step || !flowData.approver || !flowData.role) { return errorResponse('VALIDATION_ERROR', 'Step, approver, and role are required'); } const flows = mockDatabase.approvalFlows.get(id) || []; // Check for duplicate step if (flows.some((f: any) => f.step === flowData.step)) { return errorResponse('DUPLICATE_STEP', `Step ${flowData.step} already exists for this request`); } const flow = { ...flowData, id: flowData.id || generateId('flow'), requestId: id, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; flows.push(flow); flows.sort((a: any, b: any) => a.step - b.step); mockDatabase.approvalFlows.set(id, flows); console.log('[MockAPI] ✅ Approval flow created:', flow.id, `Step ${flow.step}`); return successResponse(flow, 'Approval flow step created successfully'); } catch (error: any) { console.error('[MockAPI] ❌ Error creating approval flow:', error); return errorResponse('INTERNAL_ERROR', 'Failed to create approval flow', error.message); } } /** * Update approval flow step */ async updateApprovalFlow(requestId: string, flowId: string, updates: any): Promise> { try { await delay(250, 500); const id = validateRequestId(requestId); const flows = mockDatabase.approvalFlows.get(id) || []; const index = flows.findIndex((f: any) => f.id === flowId); if (index === -1) { return errorResponse('NOT_FOUND', `Approval flow with ID ${flowId} not found`); } const currentFlow = flows[index]; // Validation: prevent invalid status transitions if (updates.status) { const validTransitions: Record = { 'waiting': ['pending', 'cancelled'], 'pending': ['approved', 'rejected', 'cancelled'], 'approved': [], // Final state 'rejected': [], // Final state 'cancelled': [], // Final state }; const allowed = validTransitions[currentFlow.status] || []; if (!allowed.includes(updates.status)) { return errorResponse('INVALID_TRANSITION', `Cannot transition from ${currentFlow.status} to ${updates.status}`); } } flows[index] = { ...currentFlow, ...updates, updatedAt: new Date().toISOString(), }; mockDatabase.approvalFlows.set(id, flows); console.log('[MockAPI] ✅ Approval flow updated:', flowId, updates.status || 'fields updated'); return successResponse(flows[index], 'Approval flow updated successfully'); } catch (error: any) { console.error('[MockAPI] ❌ Error updating approval flow:', error); return errorResponse('INTERNAL_ERROR', 'Failed to update approval flow', error.message); } } /** * Get approval flows for request */ async getApprovalFlows(requestId: string): Promise> { try { await delay(150, 300); const id = validateRequestId(requestId); const flows = (mockDatabase.approvalFlows.get(id) || []).sort((a: any, b: any) => a.step - b.step); return successResponse(flows); } catch (error: any) { console.error('[MockAPI] ❌ Error fetching approval flows:', error); return errorResponse('INTERNAL_ERROR', 'Failed to fetch approval flows', error.message); } } /** * Create document */ async createDocument(requestId: string, documentData: any): Promise> { try { await delay(400, 800); const id = validateRequestId(requestId); // Validate request exists if (!mockDatabase.requests.has(id)) { return errorResponse('NOT_FOUND', `Request with ID ${id} not found`); } // Validation if (!documentData.name || !documentData.type) { return errorResponse('VALIDATION_ERROR', 'Document name and type are required'); } const documents = mockDatabase.documents.get(id) || []; const document = { ...documentData, id: documentData.id || generateId('doc'), requestId: id, uploadedAt: documentData.uploadedAt || new Date().toISOString(), size: documentData.size || 0, mimeType: documentData.mimeType || 'application/octet-stream', }; documents.push(document); mockDatabase.documents.set(id, documents); console.log('[MockAPI] ✅ Document created:', document.id, document.name); return successResponse(document, 'Document uploaded successfully'); } catch (error: any) { console.error('[MockAPI] ❌ Error creating document:', error); return errorResponse('INTERNAL_ERROR', 'Failed to upload document', error.message); } } /** * Get documents for request */ async getDocuments(requestId: string): Promise> { try { await delay(150, 300); const id = validateRequestId(requestId); const documents = (mockDatabase.documents.get(id) || []).sort((a: any, b: any) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime() ); return successResponse(documents); } catch (error: any) { console.error('[MockAPI] ❌ Error fetching documents:', error); return errorResponse('INTERNAL_ERROR', 'Failed to fetch documents', error.message); } } /** * Create activity */ async createActivity(requestId: string, activityData: any): Promise> { try { await delay(150, 300); const id = validateRequestId(requestId); // Validate request exists if (!mockDatabase.requests.has(id)) { return errorResponse('NOT_FOUND', `Request with ID ${id} not found`); } // Validation if (!activityData.type || !activityData.action) { return errorResponse('VALIDATION_ERROR', 'Activity type and action are required'); } const activities = mockDatabase.activities.get(id) || []; const activity = { ...activityData, id: activityData.id || generateId('act'), requestId: id, timestamp: activityData.timestamp || new Date().toISOString(), }; activities.push(activity); activities.sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); mockDatabase.activities.set(id, activities); console.log('[MockAPI] ✅ Activity created:', activity.id, activity.action); return successResponse(activity, 'Activity logged successfully'); } catch (error: any) { console.error('[MockAPI] ❌ Error creating activity:', error); return errorResponse('INTERNAL_ERROR', 'Failed to create activity', error.message); } } /** * Get activities for request */ async getActivities(requestId: string): Promise> { try { await delay(150, 300); const id = validateRequestId(requestId); const activities = (mockDatabase.activities.get(id) || []).sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); return successResponse(activities); } catch (error: any) { console.error('[MockAPI] ❌ Error fetching activities:', error); return errorResponse('INTERNAL_ERROR', 'Failed to fetch activities', error.message); } } /** * Create IO block */ async createIOBlock(requestId: string, ioBlockData: any): Promise> { try { await delay(500, 1000); // Simulate SAP integration delay const id = validateRequestId(requestId); // Validate request exists if (!mockDatabase.requests.has(id)) { return errorResponse('NOT_FOUND', `Request with ID ${id} not found`); } // Validation if (!ioBlockData.ioNumber || !ioBlockData.blockedAmount) { return errorResponse('VALIDATION_ERROR', 'IO number and blocked amount are required'); } // Check if IO block already exists const existing = mockDatabase.ioBlocks.get(id); if (existing) { return errorResponse('DUPLICATE_IO_BLOCK', 'IO block already exists for this request'); } const ioBlock = { ...ioBlockData, id: ioBlockData.id || generateId('ioblock'), requestId: id, blockedDate: ioBlockData.blockedDate || new Date().toISOString(), status: ioBlockData.status || 'blocked', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; mockDatabase.ioBlocks.set(id, ioBlock); console.log('[MockAPI] ✅ IO block created:', ioBlock.id, ioBlock.ioNumber); return successResponse(ioBlock, 'IO budget blocked successfully in SAP'); } catch (error: any) { console.error('[MockAPI] ❌ Error creating IO block:', error); return errorResponse('INTERNAL_ERROR', 'Failed to block IO budget', error.message); } } /** * Update IO block */ async updateIOBlock(requestId: string, updates: any): Promise> { try { await delay(300, 600); const id = validateRequestId(requestId); const ioBlock = mockDatabase.ioBlocks.get(id); if (!ioBlock) { return errorResponse('NOT_FOUND', 'IO block not found for this request'); } const updated = { ...ioBlock, ...updates, updatedAt: new Date().toISOString(), }; mockDatabase.ioBlocks.set(id, updated); console.log('[MockAPI] ✅ IO block updated:', id); return successResponse(updated, 'IO block updated successfully'); } catch (error: any) { console.error('[MockAPI] ❌ Error updating IO block:', error); return errorResponse('INTERNAL_ERROR', 'Failed to update IO block', error.message); } } /** * Get IO block for request */ async getIOBlock(requestId: string): Promise> { try { await delay(150, 300); const id = validateRequestId(requestId); const ioBlock = mockDatabase.ioBlocks.get(id) || null; if (!ioBlock) { return errorResponse('NOT_FOUND', 'IO block not found for this request'); } return successResponse(ioBlock); } catch (error: any) { console.error('[MockAPI] ❌ Error fetching IO block:', error); return errorResponse('INTERNAL_ERROR', 'Failed to fetch IO block', error.message); } } /** * Get all requests (for listing) */ async getAllRequests(filters?: any): Promise> { try { await delay(300, 600); let requests = Array.from(mockDatabase.requests.values()); // Apply filters if provided if (filters) { if (filters.status) { requests = requests.filter((r: any) => r.status === filters.status); } if (filters.category) { requests = requests.filter((r: any) => r.category === filters.category); } if (filters.initiatorId) { requests = requests.filter((r: any) => r.initiator?.userId === filters.initiatorId); } } // Sort by updated date (newest first) requests.sort((a: any, b: any) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ); const page = filters?.page || 1; const limit = filters?.limit || 50; const total = requests.length; const totalPages = Math.ceil(total / limit); const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedRequests = requests.slice(startIndex, endIndex); return { success: true, data: paginatedRequests, pagination: { page, limit, total, totalPages, }, meta: { timestamp: new Date().toISOString(), version: '1.0', }, }; } catch (error: any) { console.error('[MockAPI] ❌ Error fetching requests:', error); return errorResponse('INTERNAL_ERROR', 'Failed to fetch requests', error.message) as any; } } /** * Clear all data (for testing) */ clearAll(): void { mockDatabase.requests.clear(); mockDatabase.approvalFlows.clear(); mockDatabase.documents.clear(); mockDatabase.activities.clear(); mockDatabase.ioBlocks.clear(); console.log('[MockAPI] 🗑️ All data cleared'); } /** * Initialize with dummy data */ initializeDummyData(): void { // Create a sample request for testing const sampleRequestId = 'RE-REQ-2024-CM-001'; const now = new Date().toISOString(); if (mockDatabase.requests.has(sampleRequestId)) { return; // Already initialized } const sampleRequest = { id: sampleRequestId, requestId: sampleRequestId, requestNumber: sampleRequestId, title: 'Diwali Festival Campaign - Claim Request', description: 'Claim request for dealer-led Diwali festival marketing campaign', category: 'claim-management', subcategory: 'Claim Management', type: 'dealer-claim', status: 'pending', priority: 'standard', amount: 245000, claimAmount: 245000, slaProgress: 35, slaRemaining: '4 days 12 hours', slaEndDate: new Date(Date.now() + 4 * 24 * 60 * 60 * 1000).toISOString(), currentStep: 1, totalSteps: 8, template: 'claim-management', templateName: 'Claim Management', initiator: { userId: 'user-123', name: 'Sneha Patil', email: 'sneha.patil@royalenfield.com', role: 'Regional Marketing Coordinator', department: 'Marketing - West Zone', phone: '+91 98765 43250', avatar: 'SP' }, department: 'Marketing - West Zone', createdAt: now, updatedAt: now, dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), conclusionRemark: '', claimDetails: { activityName: 'Diwali Festival Campaign 2024', activityType: 'Marketing Activity', activityDate: now, location: 'Mumbai, Maharashtra', dealerCode: 'RE-MH-001', dealerName: 'Royal Motors Mumbai', dealerEmail: 'dealer@royalmotorsmumbai.com', dealerPhone: '+91 98765 12345', dealerAddress: '123 Main Street, Andheri West, Mumbai, Maharashtra 400053', requestDescription: 'Marketing campaign for Diwali festival', estimatedBudget: '₹2,45,000', periodStart: now, periodEnd: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toISOString() }, dealerInfo: { name: 'Royal Motors Mumbai', code: 'RE-MH-001', email: 'dealer@royalmotorsmumbai.com', phone: '+91 98765 12345', address: '123 Main Street, Andheri West, Mumbai, Maharashtra 400053', }, activityInfo: { activityName: 'Diwali Festival Campaign 2024', activityType: 'Marketing Activity', activityDate: now, location: 'Mumbai, Maharashtra', }, tags: ['claim-management', 'marketing-activity'], version: 1, }; mockDatabase.requests.set(sampleRequestId, sampleRequest); // Create approval flows const approvalFlows = [ { id: 'flow-1', requestId: sampleRequestId, step: 1, levelId: 'level-1', approver: 'Royal Motors Mumbai (Dealer)', role: 'Dealer - Proposal Submission', status: 'pending', tatHours: 72, assignedAt: now, description: 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests', createdAt: now, updatedAt: now, }, { id: 'flow-2', requestId: sampleRequestId, step: 2, levelId: 'level-2', approver: 'Sneha Patil (Requestor)', role: 'Requestor Evaluation & Confirmation', status: 'waiting', tatHours: 48, description: 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)', createdAt: now, updatedAt: now, }, { id: 'flow-3', requestId: sampleRequestId, step: 3, levelId: 'level-3', approver: 'Department Lead', role: 'Dept Lead Approval', status: 'waiting', tatHours: 72, description: 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)', createdAt: now, updatedAt: now, }, { id: 'flow-4', requestId: sampleRequestId, step: 4, levelId: 'level-4', approver: 'System Auto-Process', role: 'Activity Creation', status: 'waiting', tatHours: 1, description: 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.', createdAt: now, updatedAt: now, }, { id: 'flow-5', requestId: sampleRequestId, step: 5, levelId: 'level-5', approver: 'Royal Motors Mumbai (Dealer)', role: 'Dealer - Completion Documents', status: 'waiting', tatHours: 120, description: 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description', createdAt: now, updatedAt: now, }, { id: 'flow-6', requestId: sampleRequestId, step: 6, levelId: 'level-6', approver: 'Sneha Patil (Requestor)', role: 'Requestor - Claim Approval', status: 'waiting', tatHours: 48, description: 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.', createdAt: now, updatedAt: now, }, { id: 'flow-7', requestId: sampleRequestId, step: 7, levelId: 'level-7', approver: 'System Auto-Process', role: 'E-Invoice Generation', status: 'waiting', tatHours: 1, description: 'E-invoice will be generated through DMS.', createdAt: now, updatedAt: now, }, { id: 'flow-8', requestId: sampleRequestId, step: 8, levelId: 'level-8', approver: 'Finance Team', role: 'Credit Note from SAP', status: 'waiting', tatHours: 48, description: 'Got credit note from SAP. Review and send to dealer to complete the claim management process.', createdAt: now, updatedAt: now, }, ]; mockDatabase.approvalFlows.set(sampleRequestId, approvalFlows); // Create initial activity mockDatabase.activities.set(sampleRequestId, [{ id: 'act-1', requestId: sampleRequestId, type: 'created', action: 'Request Created', details: 'Claim request for Diwali Festival Campaign 2024 created', user: 'Sneha Patil', timestamp: now, message: 'Request created', }]); console.log('[MockAPI] 📦 Dummy data initialized'); } } // Export singleton instance export const mockApi = new MockApiService(); // Initialize dummy data on import if (typeof window !== 'undefined') { mockApi.initializeDummyData(); }