Files
emaui/libs/api/src/lib/base-api/mock-base-query.ts

395 lines
12 KiB
TypeScript

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<string, unknown>;
body?: unknown;
};
type MockHandler = {
method: string;
/** Matches the URL path (query string stripped). */
pattern: RegExp;
respond: (req: MockRequest, match: RegExpMatchArray) => unknown;
};
function clone<T>(value: T): T {
return JSON.parse(JSON.stringify(value));
}
function paginated<T>(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<string, unknown> };
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<string, unknown>; 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<string, unknown> | undefined,
body: args.body,
};
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const mockBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = 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);
};