import type { BaseQueryFn } from '@reduxjs/toolkit/query/react'; import type { FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query'; import { baseQueryWithReauth } from './base-query-with-reauth'; import { mockApplicationDetails, mockApplications, mockLicenses, mockLicenseTypeRequirements, mockLicenseTypes, mockMyExamAppeals, mockMyExamRegistrations, mockMyExamResults, mockNotifications, mockOpenExams, mockProfileMeResponse, mockVessels, } from './mock-data'; /** * Dev-only mock `BaseQueryFn`, opt-in via `VITE_USE_MOCKS=true` (see * `base-api/index.ts`). * * Design choice: this is a partial mock, not a full fake backend. Each * handler below matches a specific method + URL pattern for the four preview * feature areas (vessel registration, seafarer registration, CoC * certificates, exams) plus the shared profile/permissions/licensing-config * endpoints they depend on. Anything not matched here falls through to the * real `baseQueryWithReauth`, so login, signup, and any endpoint outside the * four areas keep talking to a real backend if one is reachable. This was * chosen over a full-mock/404 approach so the rest of the app (profile * editing, payments, other license types, backoffice) is unaffected and a * developer can extend coverage incrementally by adding more handlers below. * * Extend by adding another entry to `handlers`. Each handler receives the * parsed method/path/params/body and returns the response payload (or * `undefined` to fall through to the next handler / the real backend). */ type MockRequest = { method: string; path: string; params?: Record; body?: unknown; }; type MockHandler = { method: string; /** Matches the URL path (query string stripped). */ pattern: RegExp; respond: (req: MockRequest, match: RegExpMatchArray) => unknown; }; function clone(value: T): T { return JSON.parse(JSON.stringify(value)); } function paginated(items: T[]) { return { total: items.length, items }; } let mockAppSeq = 100; const handlers: MockHandler[] = [ // --------------------------------------------------------------- profile { method: 'GET', pattern: /^\/profiles\/me$/, respond: () => clone(mockProfileMeResponse), }, // ----------------------------------------------------------- license types { method: 'GET', pattern: /^\/license-types$/, respond: () => paginated(clone(mockLicenseTypes)), }, { method: 'GET', pattern: /^\/license-types\/requirements\/([\w-]+)$/, respond: (_req, match) => { const key = match[1]; const found = mockLicenseTypeRequirements[key]; return found ? clone(found) : undefined; }, }, { method: 'GET', pattern: /^\/license-categories$/, respond: () => paginated([ { key: 'MARITIME_PERSONNEL', name: { en: 'Maritime Personnel' }, description: { en: 'Seafarer and vessel services' }, sortOrder: 1 }, ]), }, { method: 'GET', pattern: /^\/profiles\/me\/operations$/, respond: () => ({ items: [] }), }, // ----------------------------------------------------------- applications { method: 'GET', pattern: /^\/license-applications\/mine$/, respond: () => paginated(clone(Object.values(mockApplications))), }, { method: 'POST', pattern: /^\/license-applications$/, respond: (req) => { const body = (req.body ?? {}) as { licenseType?: string; kind?: string; previousLicenseId?: string }; const typeKey = body.licenseType ?? 'VESSEL_REGISTRATION'; const requirements = mockLicenseTypeRequirements[typeKey]; const id = `app-draft-${++mockAppSeq}`; const now = new Date().toISOString(); const application = { id, applicationNumber: `${requirements?.licenseType.certificatePrefix ?? 'APP'}-2026-${String(mockAppSeq).padStart(6, '0')}`, licenseTypeId: requirements?.licenseType.id ?? 'license-type-unknown', licenseType: requirements?.licenseType ?? { id: 'license-type-unknown', key: typeKey, name: { en: typeKey } }, applicantUserId: 'user-mock-001', kind: body.kind ?? 'NEW', status: 'DRAFT', assignedOfficerId: null, claimedAt: null, formData: {}, companyName: null, tradeName: null, tinNumber: null, businessAddress: null, capitalAmountDeclared: null, capitalAmountVerified: null, adjustmentRound: 0, submittedAt: null, decidedAt: null, rejectionReason: null, feeAmount: String(requirements?.fee ?? ''), feeCurrency: requirements?.feeCurrency ?? 'ETB', issuedLicenseId: null, createdAt: now, }; mockApplications[id] = application; mockApplicationDetails[id] = { application, staff: [], attachments: [], history: [ { id: `hist-${id}-1`, fromStatus: null, toStatus: 'DRAFT', event: 'created', actorUserId: 'user-mock-001', actorName: 'Abebe Tesfaye', actorRole: 'APPLICANT', remark: null, metadata: null, createdAt: now, }, ], remarks: [], openRemarks: [], availableEvents: [], }; return clone(application); }, }, { method: 'GET', pattern: /^\/license-applications\/([\w-]+)$/, respond: (_req, match) => { const detail = mockApplicationDetails[match[1]]; return detail ? clone(detail) : undefined; }, }, { method: 'DELETE', pattern: /^\/license-applications\/([\w-]+)$/, respond: (_req, match) => { delete mockApplications[match[1]]; delete mockApplicationDetails[match[1]]; return { deleted: true }; }, }, { method: 'PATCH', pattern: /^\/license-applications\/([\w-]+)\/sections\/([\w-]+)$/, respond: (req, match) => { const [, id, sectionKey] = match; const application = mockApplications[id]; if (!application) return undefined; const body = (req.body ?? {}) as { values?: Record }; application.formData = { ...application.formData, [sectionKey]: body.values ?? {} }; return clone(application); }, }, { method: 'POST', pattern: /^\/license-applications\/([\w-]+)\/submit$/, respond: (_req, match) => { const application = mockApplications[match[1]]; if (!application) return undefined; application.status = 'UNDER_REVIEW'; application.submittedAt = new Date().toISOString(); return clone(application); }, }, { method: 'POST', pattern: /^\/license-applications\/([\w-]+)\/resubmit$/, respond: (_req, match) => { const application = mockApplications[match[1]]; if (!application) return undefined; application.status = 'UNDER_REVIEW'; return clone(application); }, }, { method: 'PATCH', pattern: /^\/license-applications\/([\w-]+)\/remarks\/([\w-]+)\/resolve$/, respond: () => ({ ok: true }), }, { method: 'POST', pattern: /^\/license-applications\/([\w-]+)\/staff$/, respond: (req) => { const body = (req.body ?? {}) as { roleKey?: string; fullName?: string; position?: string; yearsOfExperience?: number }; return { id: `staff-mock-${Date.now()}`, roleKey: body.roleKey ?? '', fullName: body.fullName ?? '', position: body.position ?? null, roleCategory: null, yearsOfExperience: body.yearsOfExperience ?? null, documents: [], }; }, }, { method: 'DELETE', pattern: /^\/license-applications\/([\w-]+)\/staff\/([\w-]+)$/, respond: () => ({ ok: true }), }, // -------------------------------------------------------------- licenses { method: 'GET', pattern: /^\/licenses\/mine$/, respond: () => paginated(clone(mockLicenses)), }, { method: 'GET', pattern: /^\/licenses\/([\w-]+)\/certificate$/, respond: (_req, match) => ({ url: `https://example-cdn.ema.gov.et/certificates/${match[1]}.pdf`, }), }, // ------------------------------------------------------------ attachments { method: 'GET', pattern: /^\/attachments$/, respond: () => [], }, // ---------------------------------------------------------- notifications { method: 'GET', pattern: /^\/notifications\/unseen$/, respond: () => clone(mockNotifications), }, { method: 'GET', pattern: /^\/notifications$/, respond: () => clone(mockNotifications), }, { method: 'PATCH', pattern: /^\/notifications\/([\w-]+)\/read$/, respond: () => ({ ok: true }), }, // ---------------------------------------------------------------- vessels { method: 'GET', pattern: /^\/vessels\/mine$/, respond: () => clone(mockVessels), }, { method: 'POST', pattern: /^\/vessels\/([\w-]+)\/incidents$/, respond: (req, match) => ({ id: `incident-mock-${Date.now()}`, vesselId: match[1], occurredAt: (req.body as { occurredAt?: string })?.occurredAt ?? new Date().toISOString(), location: (req.body as { location?: string })?.location ?? null, description: (req.body as { description?: string })?.description ?? '', severity: null, reportedById: 'user-mock-001', reportedByOfficer: false, createdAt: new Date().toISOString(), }), }, // ------------------------------------------------------------------ exams { method: 'GET', pattern: /^\/exams\/open$/, respond: () => clone(mockOpenExams), }, { method: 'GET', pattern: /^\/exams\/registrations\/mine$/, respond: () => clone(mockMyExamRegistrations), }, { method: 'POST', pattern: /^\/exams\/([\w-]+)\/register$/, respond: () => ({ admissionNumber: `ADM-2026-${Math.floor(Math.random() * 900000 + 100000)}` }), }, { method: 'GET', pattern: /^\/results\/mine$/, respond: () => clone(mockMyExamResults), }, { method: 'GET', pattern: /^\/results\/appeals\/mine$/, respond: () => clone(mockMyExamAppeals), }, { method: 'POST', pattern: /^\/results\/([\w-]+)\/appeal$/, respond: () => ({ appealNumber: `APL-2026-${Math.floor(Math.random() * 900000 + 100000)}` }), }, // -------------------------------------------------------- seafarer records { method: 'GET', pattern: /^\/sea-service-records\/mine$/, respond: () => [], }, { method: 'GET', pattern: /^\/medical-certificates\/mine$/, respond: () => [], }, { method: 'GET', pattern: /^\/sea-service-records\/mine\/sea-time$/, respond: () => ({ totalDays: 620, verifiedRecords: 4 }), }, ]; function parseArgs(args: string | FetchArgs): { method: string; path: string; params?: Record; body?: unknown } { if (typeof args === 'string') { const [path] = args.split('?'); return { method: 'GET', path }; } const [path] = (args.url ?? '').split('?'); return { method: (args.method ?? 'GET').toUpperCase(), path, params: args.params as Record | undefined, body: args.body, }; } function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } export const mockBaseQuery: BaseQueryFn = async ( args, api, extraOptions, ) => { const req = parseArgs(args); for (const handler of handlers) { if (handler.method !== req.method) continue; const match = req.path.match(handler.pattern); if (!match) continue; const data = handler.respond(req, match); if (data === undefined) continue; await delay(150 + Math.floor(Math.random() * 150)); return { data }; } // Unmatched — fall through to the real backend (auth/login, and any // endpoint outside the four mocked preview areas). return baseQueryWithReauth(args, api, extraOptions); };