Re_Figma_Code/src/services/mockApi.ts

841 lines
26 KiB
TypeScript

/**
* 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<T = any> {
success: boolean;
data?: T;
message?: string;
error?: {
code: string;
message: string;
details?: any;
};
meta?: {
timestamp: string;
requestId?: string;
version?: string;
};
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// In-memory data store
const mockDatabase: {
requests: Map<string, any>;
approvalFlows: Map<string, any[]>;
documents: Map<string, any[]>;
activities: Map<string, any[]>;
ioBlocks: Map<string, any>;
} = {
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<void> => {
const delayTime = Math.floor(Math.random() * (max - min + 1)) + min;
return new Promise(resolve => setTimeout(resolve, delayTime));
};
// Helper to create success response
function successResponse<T>(data: T, message?: string): ApiResponse<T> {
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<ApiResponse<any>> {
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<ApiResponse<any>> {
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<ApiResponse<any>> {
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<ApiResponse<any>> {
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<ApiResponse<any>> {
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<string, string[]> = {
'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<ApiResponse<any[]>> {
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<ApiResponse<any>> {
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<ApiResponse<any[]>> {
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<ApiResponse<any>> {
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<ApiResponse<any[]>> {
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<ApiResponse<any>> {
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<ApiResponse<any>> {
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<ApiResponse<any>> {
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<PaginatedResponse<any>> {
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();
}