mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 08:48:12 +00:00
Remove obsolete test artifacts and error context files for seafarer registration tests
This commit is contained in:
@@ -6,7 +6,11 @@ import {
|
||||
verifyOtpIfPrompted,
|
||||
} from './support/applicant';
|
||||
import { deleteApplicant, sql, sqlValue } from './support/db';
|
||||
import { approveRegistration, runWorkflow } from './support/workflow';
|
||||
import {
|
||||
approveRegistration,
|
||||
resolveOpenRemarks,
|
||||
runWorkflow,
|
||||
} from './support/workflow';
|
||||
|
||||
/**
|
||||
* Seafarer registration, applicant through to approval.
|
||||
@@ -35,13 +39,20 @@ import { approveRegistration, runWorkflow } from './support/workflow';
|
||||
* be filled, and `PROFILE_FIELD_SECTION` in the auth lib is the map of which
|
||||
* field lives where.
|
||||
*/
|
||||
async function completeProfile(page: Page): Promise<void> {
|
||||
async function completeProfile(
|
||||
page: Page,
|
||||
applicant: Applicant,
|
||||
): Promise<void> {
|
||||
await page.goto('/profile');
|
||||
|
||||
await openTab(page, 'Profile');
|
||||
await page.getByLabel('First Name').fill('Dawit');
|
||||
await page.getByLabel('Middle Name').fill('Bekele');
|
||||
await page.getByLabel('Last Name').fill('Tesfaye');
|
||||
// The account's own name parts, not invented ones: the Maritime tab refuses
|
||||
// to save when they do not join to the name on the Personal tab, and it
|
||||
// refuses by returning early — no request, no field error, so the failure
|
||||
// surfaced only as "save produced no request".
|
||||
await page.getByLabel('First Name').fill(applicant.firstName);
|
||||
await page.getByLabel('Middle Name').fill(applicant.middleName);
|
||||
await page.getByLabel('Last Name').fill(applicant.lastName);
|
||||
await pick(page, 'Gender', /male/i);
|
||||
await pickDate(page, 'Date of Birth', '1995-04-12');
|
||||
await pick(page, 'Marital Status', /single/i);
|
||||
@@ -49,15 +60,16 @@ async function completeProfile(page: Page): Promise<void> {
|
||||
await save(page);
|
||||
|
||||
await openTab(page, 'Address');
|
||||
await pick(page, 'ID Type', /^NID$/i);
|
||||
// Matched on the option's label, not its stored value: the select shows
|
||||
// "National Id" and submits `NID`, so `/^NID$/` matched no option at all.
|
||||
await pick(page, 'ID Type', /^national id$/i);
|
||||
await page.getByLabel('ID Number').fill('FYD1234567890');
|
||||
// A country select, not a free-text field.
|
||||
await pick(page, 'Nationality', /ethiopia/i);
|
||||
// `addressSchema` requires this in Ethiopian format; without it the form
|
||||
// never submits and no request is made for `save` to wait on.
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Primary Phone' })
|
||||
.fill('+251911234567');
|
||||
// Primary Phone is deliberately not filled: it is `readOnly` here and already
|
||||
// carries the account's number ("From your account, edit it in the Personal
|
||||
// tab"), so `addressSchema`'s Ethiopian-format rule is already satisfied and a
|
||||
// fill would only fail against a read-only input.
|
||||
await save(page);
|
||||
}
|
||||
|
||||
@@ -145,19 +157,35 @@ async function save(page: Page): Promise<void> {
|
||||
// A zod-blocked submit fires no request at all, so the bare timeout says
|
||||
// only "no response" — which reads as a backend fault rather than a form
|
||||
// that refused to submit. Surface the field errors instead.
|
||||
// Field errors only. `[role="alert"]` also matches Mantine's `<Alert>`, and
|
||||
// the profile page renders an informational seafarer banner as one — which
|
||||
// got reported as "validation errors: Seafarer registration asks for these
|
||||
// details…", pointing at a form that was in fact filled in correctly.
|
||||
const messages = await page
|
||||
.locator('.mantine-InputWrapper-error, [role="alert"]')
|
||||
.locator('.mantine-InputWrapper-error')
|
||||
.allTextContents();
|
||||
throw new Error(
|
||||
messages.length
|
||||
? `Save did not submit — validation errors: ${messages.join('; ')}`
|
||||
: 'Save produced no request and reported no validation error.',
|
||||
: // No field error either, so the form was valid and something else
|
||||
// refused: `onSaveProfile` early-returns when the profile name does
|
||||
// not match the account name, and notifies rather than marking a
|
||||
// field.
|
||||
'Save produced no request and reported no field error — check for a rejected notification (e.g. the profile/account name match).',
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Signs up, declares seafarer operations, and fills the gating profile. */
|
||||
/**
|
||||
* Signs up, declares seafarer operations, and fills the profile.
|
||||
*
|
||||
* Declaring seafarer now lands on the registration wizard, not `/profile` — the
|
||||
* wizard collects the identity itself. The profile is still filled here because
|
||||
* these tests are about the registration workflow, and a profile with a name and
|
||||
* an address is what the approval's completion effect writes onto; `/profile` is
|
||||
* navigated to directly rather than waited for as a redirect.
|
||||
*/
|
||||
async function readyApplicant(page: Page, applicant: Applicant): Promise<void> {
|
||||
const offset = await signUp(page, applicant);
|
||||
await verifyOtpIfPrompted(page, offset);
|
||||
@@ -167,10 +195,12 @@ async function readyApplicant(page: Page, applicant: Applicant): Promise<void> {
|
||||
.first()
|
||||
.check();
|
||||
await page.getByRole('button', { name: /save operations/i }).click();
|
||||
// A seafarer is taken to `/profile`, not the dashboard: registration is
|
||||
// built from the profile, and a fresh signup holds none of it yet.
|
||||
await expect(page).toHaveURL(/\/licensing\/SEAFARER_REGISTRATION\/apply/, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await page.goto('/profile');
|
||||
await expect(page).toHaveURL(/\/profile/, { timeout: 30_000 });
|
||||
await completeProfile(page);
|
||||
await completeProfile(page, applicant);
|
||||
}
|
||||
|
||||
test.describe('seafarer registration', () => {
|
||||
@@ -229,7 +259,7 @@ test.describe('seafarer registration', () => {
|
||||
const number = await waitForApplication(applicant.email);
|
||||
const id = idOf(number);
|
||||
|
||||
await submit(id);
|
||||
await submit(id, applicant);
|
||||
await runWorkflow(id, [{ path: 'claim' }]);
|
||||
expect(statusOf(number)).toBe('UNDER_REVIEW');
|
||||
|
||||
@@ -253,19 +283,36 @@ test.describe('seafarer registration', () => {
|
||||
const number = await waitForApplication(applicant.email);
|
||||
const id = idOf(number);
|
||||
|
||||
await submit(id);
|
||||
await submit(id, applicant);
|
||||
await runWorkflow(id, [
|
||||
{ path: 'claim' },
|
||||
{
|
||||
path: 'request-adjustment',
|
||||
data: { remarks: [{ message: 'Medical certificate is illegible.' }] },
|
||||
// `RequestAdjustmentDto` takes `items`, each naming what to fix and
|
||||
// where — a bare `remarks: [{ message }]` is refused with "items should
|
||||
// not be empty", which reads as an empty request rather than a wrongly
|
||||
// shaped one.
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
targetType: 'FORM_SECTION',
|
||||
targetKey: 'medicalCertificate',
|
||||
remark: 'Medical certificate is illegible.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(statusOf(number)).toBe('RESUBMIT_REQUIRED');
|
||||
|
||||
// Every flagged item has to be ticked off first: `resubmit` refuses while
|
||||
// any remark is open (`unresolved_remarks`), which is what stops an
|
||||
// applicant returning the same form untouched.
|
||||
await resolveOpenRemarks(id, openRemarkIds(number), applicant);
|
||||
|
||||
// A resubmission returns to review directly — a registration has no
|
||||
// earlier stage to fall back to.
|
||||
await runWorkflow(id, [{ path: 'resubmit' }]);
|
||||
await runWorkflow(id, [{ path: 'resubmit' }], applicant);
|
||||
expect(statusOf(number)).toBe('UNDER_REVIEW');
|
||||
});
|
||||
|
||||
@@ -275,7 +322,7 @@ test.describe('seafarer registration', () => {
|
||||
const number = await waitForApplication(applicant.email);
|
||||
const id = idOf(number);
|
||||
|
||||
await submit(id);
|
||||
await submit(id, applicant);
|
||||
await runWorkflow(id, [
|
||||
{ path: 'claim' },
|
||||
{ path: 'hold', data: { reason: 'Awaiting confirmation from the clinic.' } },
|
||||
@@ -293,7 +340,7 @@ test.describe('seafarer registration', () => {
|
||||
const number = await waitForApplication(applicant.email);
|
||||
const id = idOf(number);
|
||||
|
||||
await submit(id);
|
||||
await submit(id, applicant);
|
||||
await runWorkflow(id, [
|
||||
{ path: 'claim' },
|
||||
{ path: 'reject', data: { reason: 'Basic training evidence incomplete.' } },
|
||||
@@ -314,7 +361,7 @@ test.describe('seafarer registration', () => {
|
||||
const number = await waitForApplication(applicant.email);
|
||||
const id = idOf(number);
|
||||
|
||||
await submit(id);
|
||||
await submit(id, applicant);
|
||||
await approveRegistration(id);
|
||||
|
||||
expect(statusOf(number)).toBe('COMPLETED');
|
||||
@@ -349,7 +396,7 @@ test.describe('seafarer registration', () => {
|
||||
const number = await waitForApplication(applicant.email);
|
||||
const id = idOf(number);
|
||||
|
||||
await submit(id);
|
||||
await submit(id, applicant);
|
||||
await approveRegistration(id);
|
||||
const first = seafarerNumberOf(applicant.email);
|
||||
|
||||
@@ -367,7 +414,7 @@ test.describe('seafarer registration', () => {
|
||||
await page.goto('/licensing/SEAFARER_REGISTRATION/apply');
|
||||
const number = await waitForApplication(applicant.email);
|
||||
|
||||
await submit(idOf(number));
|
||||
await submit(idOf(number), applicant);
|
||||
await approveRegistration(idOf(number));
|
||||
|
||||
// The number is permanent and the service is not renewable, so the portal
|
||||
@@ -438,6 +485,17 @@ function seafarerNumberOf(email: string): string | null {
|
||||
`);
|
||||
}
|
||||
|
||||
/** Ids of the remarks still open on the current adjustment round. */
|
||||
function openRemarkIds(applicationNumber: string): string[] {
|
||||
return sql(`
|
||||
SELECT r.id FROM application_remarks r
|
||||
JOIN license_applications a ON a.id = r.application_id
|
||||
WHERE a.application_number = '${applicationNumber}'
|
||||
AND r.is_resolved = false
|
||||
AND r.round_number = a.adjustment_round
|
||||
`).map((row) => row[0]);
|
||||
}
|
||||
|
||||
function childrenOf(applicationNumber: string): string[][] {
|
||||
return sql(`
|
||||
SELECT lt.key, a.status, a.origin
|
||||
@@ -452,12 +510,98 @@ function childrenOf(applicationNumber: string): string[][] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the draft.
|
||||
* Fills the draft's answers and evidence directly, so it can be submitted.
|
||||
*
|
||||
* The wizard's own sections are not filled in: what these tests are about is
|
||||
* the workflow and its approval effects, and a form-validation failure would
|
||||
* fail them for the wrong reason. Field-level rules belong in their own spec.
|
||||
* These tests are about the workflow and its approval effects, not the wizard's
|
||||
* fields — but `submit` validates the whole form and every required document, so
|
||||
* an unfilled draft cannot reach the workflow at all. Driving six wizard steps
|
||||
* and four uploads in each test would make them slow tests of the form instead.
|
||||
*
|
||||
* So the answers go in as one `form_data` write and the evidence as attachment
|
||||
* rows. Deliberately not through MinIO: `getSuppliedDocumentKeys` joins
|
||||
* attachments to their files and counts document keys, and nothing at submission
|
||||
* reads a file's bytes — a row with a storage key is exactly as complete as an
|
||||
* upload, without requiring object storage to be reachable.
|
||||
*
|
||||
* Values mirror the seeded schema (`seafarer-registration.seed-data.ts`); a
|
||||
* required field added there fails these with `application_incomplete`, naming
|
||||
* the field.
|
||||
*/
|
||||
async function submit(applicationId: string): Promise<void> {
|
||||
await runWorkflow(applicationId, [{ path: 'submit' }]);
|
||||
function fillForSubmission(applicationId: string): void {
|
||||
const locationId = sqlValue(`
|
||||
SELECT l.id FROM iam.locations l
|
||||
JOIN iam.location_types lt ON lt.id = l.location_type_id
|
||||
WHERE lt.code = 'SUBCITY' LIMIT 1
|
||||
`);
|
||||
if (!locationId) {
|
||||
throw new Error('No SUBCITY location seeded — run the location seed.');
|
||||
}
|
||||
|
||||
const formData = JSON.stringify({
|
||||
profileSummary: {
|
||||
firstName: 'Dawit',
|
||||
middleName: 'Bekele',
|
||||
lastName: 'Tesfaye',
|
||||
gender: 'MALE',
|
||||
dateOfBirth: '1995-04-12',
|
||||
maritalStatus: 'SINGLE',
|
||||
nationality: 'Ethiopian',
|
||||
nationalIdNumber: 'FYD1234567890',
|
||||
},
|
||||
identity: { placeOfBirth: 'Addis Ababa', department: 'DECK' },
|
||||
address: { locationId, permanentAddress: 'Bole, Addis Ababa' },
|
||||
emergencyContact: {
|
||||
name: 'Almaz Tesfaye',
|
||||
relationship: 'Sister',
|
||||
phoneNumber: '+251911222333',
|
||||
},
|
||||
physicalCharacteristics: {
|
||||
hairColor: 'BLACK',
|
||||
eyeColor: 'BROWN',
|
||||
heightCm: 172,
|
||||
weightKg: 68,
|
||||
bloodType: 'O_POSITIVE',
|
||||
},
|
||||
medicalCertificate: {
|
||||
certificateNumber: 'MED-2026-001',
|
||||
issuerName: 'Addis Marine Clinic',
|
||||
issueDate: '2026-01-15',
|
||||
},
|
||||
declaration: { accepted: true },
|
||||
}).replace(/'/g, "''");
|
||||
|
||||
const documentKeys = [
|
||||
'photo',
|
||||
'nationalId',
|
||||
'medical_certificate',
|
||||
'basic_training_evidence',
|
||||
];
|
||||
|
||||
sql(`
|
||||
UPDATE license_applications
|
||||
SET form_data = '${formData}'::jsonb
|
||||
WHERE id = '${applicationId}';
|
||||
|
||||
WITH inserted AS (
|
||||
INSERT INTO attachments (owner_type, owner_id, document_key, valid_from, valid_to)
|
||||
SELECT 'APPLICATION', '${applicationId}', key, CURRENT_DATE, CURRENT_DATE + 365
|
||||
FROM unnest(ARRAY[${documentKeys.map((d) => `'${d}'`).join(',')}]) AS key
|
||||
RETURNING id
|
||||
)
|
||||
INSERT INTO attachment_files
|
||||
(attachment_id, original_name, mime_type, size_bytes, storage_key)
|
||||
SELECT id, 'evidence.pdf', 'application/pdf', 1024, 'e2e/' || id || '.pdf'
|
||||
FROM inserted;
|
||||
`);
|
||||
}
|
||||
|
||||
/** Fills what submission requires, then submits as the applicant. */
|
||||
async function submit(
|
||||
applicationId: string,
|
||||
applicant: Applicant,
|
||||
): Promise<void> {
|
||||
fillForSubmission(applicationId);
|
||||
// As the applicant: `submit` is ownership-guarded, so the officer's token —
|
||||
// which every other step here uses — is refused with `not_application_owner`.
|
||||
await runWorkflow(applicationId, [{ path: 'submit' }], applicant);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ import { E2E } from '../../playwright.config';
|
||||
|
||||
const OTP_PATTERN = /is (\d{4,8})\./g;
|
||||
|
||||
/** Byte offset to read from later. Zero when the log does not exist yet. */
|
||||
/**
|
||||
* Byte offset to read from later. Zero when the log does not exist yet.
|
||||
*
|
||||
* Bytes, and read back as bytes — see `otpSince`.
|
||||
*/
|
||||
export function logOffset(): number {
|
||||
try {
|
||||
return statSync(E2E.apiLog).size;
|
||||
@@ -48,7 +52,14 @@ export async function waitForOtp(
|
||||
function otpSince(offset: number): string | null {
|
||||
let text: string;
|
||||
try {
|
||||
text = readFileSync(E2E.apiLog, 'utf8').slice(offset);
|
||||
// Sliced as a Buffer, then decoded — not `readFileSync(…, 'utf8').slice()`.
|
||||
// `logOffset()` is a byte count from `statSync`, while slicing a string
|
||||
// counts UTF-16 code units, and the API logs Amharic notification bodies:
|
||||
// every multi-byte character made the offset overshoot, so a code written
|
||||
// just after it was skipped and the wait timed out. The drift grows with
|
||||
// the log, which is why this failed intermittently and more often later in
|
||||
// a run.
|
||||
text = readFileSync(E2E.apiLog).subarray(offset).toString('utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -14,18 +14,35 @@ export interface Applicant {
|
||||
username: string;
|
||||
phoneNumber: string;
|
||||
password: string;
|
||||
/** The account name, as typed at signup. Always `${firstName} ${middleName} ${lastName}`. */
|
||||
name: string;
|
||||
firstName: string;
|
||||
middleName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
export function newApplicant(label: string): Applicant {
|
||||
const stamp = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
||||
// The profile's Maritime tab refuses to save unless first/middle/last join to
|
||||
// exactly the account name (`ProfilePage.onSaveProfile`) — and that refusal is
|
||||
// a silent early return, no request. So the parts are the source of truth here
|
||||
// and the account name is composed from them, rather than the two being
|
||||
// written independently and hoped to agree.
|
||||
//
|
||||
// Each part is at least three characters, which `profileSchema` requires.
|
||||
const firstName = 'Dawit';
|
||||
const middleName = 'Bekele';
|
||||
const lastName = `Tesfaye${stamp.slice(-4)}`;
|
||||
return {
|
||||
email: `e2e.${label}.${stamp}@example.test`,
|
||||
username: `e2e${label}${stamp}`.slice(0, 28),
|
||||
// Ethiopian mobile format; the last digits vary so two runs never collide.
|
||||
phoneNumber: `+2519${stamp.slice(-8)}`,
|
||||
password: 'E2ePassw0rd!',
|
||||
name: `E2E ${label} ${stamp.slice(-4)}`,
|
||||
name: `${firstName} ${middleName} ${lastName}`,
|
||||
firstName,
|
||||
middleName,
|
||||
lastName,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,47 @@ import { OFFICER } from './officer';
|
||||
/** Routes served by the applicant-facing controller rather than the review one. */
|
||||
const APPLICANT_STEPS = new Set(['submit', 'resubmit']);
|
||||
|
||||
async function officerContext(): Promise<APIRequestContext> {
|
||||
const context = await request.newContext({ baseURL: E2E.apiUrl });
|
||||
const response = await context.post('/auth/login', {
|
||||
data: { email: OFFICER.email, password: OFFICER.password },
|
||||
/**
|
||||
* Resolves every open remark on an application, as the applicant.
|
||||
*
|
||||
* `resubmit` refuses while any remain (`unresolved_remarks`) — the applicant is
|
||||
* expected to tick off each correction as they make it, which the portal does
|
||||
* per section. A test that only wants the round-trip still has to do it.
|
||||
*/
|
||||
export async function resolveOpenRemarks(
|
||||
applicationId: string,
|
||||
remarkIds: string[],
|
||||
applicant: { email: string; password: string },
|
||||
): Promise<void> {
|
||||
await runWorkflow(
|
||||
applicationId,
|
||||
remarkIds.map((remarkId) => ({
|
||||
path: `remarks/${remarkId}/resolve`,
|
||||
method: 'patch' as const,
|
||||
})),
|
||||
applicant,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* An authenticated API context for one account.
|
||||
*
|
||||
* Paths built against it are relative on purpose. `E2E.apiUrl` carries the
|
||||
* `/api` prefix, and a leading slash resolves against the *origin* —
|
||||
* `/auth/login` against `http://host/api` requests `http://host/auth/login`,
|
||||
* which 404s. Every path in this file is therefore written without one.
|
||||
*/
|
||||
async function contextFor(
|
||||
who: string,
|
||||
credentials: { email: string; password: string },
|
||||
): Promise<APIRequestContext> {
|
||||
const context = await request.newContext({ baseURL: `${E2E.apiUrl}/` });
|
||||
const response = await context.post('auth/login', {
|
||||
data: { email: credentials.email, password: credentials.password },
|
||||
});
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`Officer login failed (${response.status()}): ${await response.text()}`,
|
||||
`${who} login failed (${response.status()}): ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
const body = await response.json();
|
||||
@@ -36,17 +69,23 @@ async function officerContext(): Promise<APIRequestContext> {
|
||||
|
||||
await context.dispose();
|
||||
return request.newContext({
|
||||
baseURL: E2E.apiUrl,
|
||||
baseURL: `${E2E.apiUrl}/`,
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
function officerContext(): Promise<APIRequestContext> {
|
||||
return contextFor('Officer', OFFICER);
|
||||
}
|
||||
|
||||
export interface WorkflowStep {
|
||||
/** Route under the review controller, e.g. `claim`, `final-approve`. */
|
||||
path: string;
|
||||
data?: Record<string, unknown>;
|
||||
/** Set when a step is expected to be refused — the refusal is the assertion. */
|
||||
expectFailure?: boolean;
|
||||
/** POST unless stated; the applicant's remark-resolve route is a PATCH. */
|
||||
method?: 'post' | 'patch';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,8 +98,29 @@ export interface WorkflowStep {
|
||||
export async function runWorkflow(
|
||||
applicationId: string,
|
||||
steps: WorkflowStep[],
|
||||
/**
|
||||
* The owner, required only when a step is applicant-side. `submit` and
|
||||
* `resubmit` are guarded by ownership, not permission — the officer holds
|
||||
* every permission but is not the applicant, so running them on the officer's
|
||||
* token is refused with `not_application_owner`.
|
||||
*/
|
||||
applicant?: { email: string; password: string },
|
||||
): Promise<number[]> {
|
||||
const api = await officerContext();
|
||||
const officer = await officerContext();
|
||||
const needsApplicant = steps.some(
|
||||
(step) => APPLICANT_STEPS.has(step.path) || step.path.startsWith('remarks/'),
|
||||
);
|
||||
if (needsApplicant && !applicant) {
|
||||
throw new Error(
|
||||
`Steps [${steps
|
||||
.filter((s) => APPLICANT_STEPS.has(s.path) || s.path.startsWith('remarks/'))
|
||||
.map((s) => s.path)
|
||||
.join(', ')}] act as the applicant — pass their credentials to runWorkflow.`,
|
||||
);
|
||||
}
|
||||
const owner = needsApplicant && applicant
|
||||
? await contextFor('Applicant', applicant)
|
||||
: null;
|
||||
const codes: number[] = [];
|
||||
|
||||
try {
|
||||
@@ -68,13 +128,17 @@ export async function runWorkflow(
|
||||
// Applicant-side actions (`submit`, `resubmit`) live on the
|
||||
// applications controller; everything an officer does is on the review
|
||||
// controller. Routing by step keeps callers from having to know.
|
||||
const base = APPLICANT_STEPS.has(step.path)
|
||||
const isApplicantStep =
|
||||
APPLICANT_STEPS.has(step.path) || step.path.startsWith('remarks/');
|
||||
const base = isApplicantStep
|
||||
? 'license-applications'
|
||||
: 'license-application-review';
|
||||
const response = await api.post(
|
||||
`/${base}/${applicationId}/${step.path}`,
|
||||
{ data: step.data ?? {} },
|
||||
);
|
||||
const api = isApplicantStep && owner ? owner : officer;
|
||||
const url = `${base}/${applicationId}/${step.path}`;
|
||||
const response =
|
||||
step.method === 'patch'
|
||||
? await api.patch(url, { data: step.data ?? {} })
|
||||
: await api.post(url, { data: step.data ?? {} });
|
||||
codes.push(response.status());
|
||||
|
||||
if (!step.expectFailure && !response.ok()) {
|
||||
@@ -84,7 +148,8 @@ export async function runWorkflow(
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await api.dispose();
|
||||
await officer.dispose();
|
||||
await owner?.dispose();
|
||||
}
|
||||
|
||||
return codes;
|
||||
|
||||
Reference in New Issue
Block a user