mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-02 08:03:44 +00:00
395 lines
12 KiB
TypeScript
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);
|
|
};
|