mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-01 18:13:28 +00:00
Merge pull request #53 from Tria-plc/WorkflowChange
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -36,3 +36,6 @@ local-packages/iamui-extracted/
|
||||
# Playwright visual-regression artifacts (baselines under apps/e2e/visual are tracked)
|
||||
test-results/
|
||||
dist/visual-report/
|
||||
branch_structure.json
|
||||
temp_auto_push.bat
|
||||
temp_interactive_push.bat
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ApplicationDetail } from "@ema-platform/api";
|
||||
import { resolveActions } from "./actions";
|
||||
|
||||
const REASONS = {
|
||||
wrongStatus: "wrong status",
|
||||
notAssigned: "not assigned",
|
||||
noPermission: "no permission",
|
||||
needsFlags: "needs flags",
|
||||
needsCapital: "needs capital",
|
||||
needsInspection: "needs inspection",
|
||||
needsDocumentReviews: "needs document reviews",
|
||||
inspectionNotYetDue: "not yet due",
|
||||
};
|
||||
|
||||
/** Held by someone else, so ownership gating is what the assertions turn on. */
|
||||
function detailFor(licenseTypeKey: string): ApplicationDetail {
|
||||
return {
|
||||
application: {
|
||||
id: "app-1",
|
||||
status: "SUBMITTED",
|
||||
assignedOfficerId: "another-officer",
|
||||
licenseType: { key: licenseTypeKey },
|
||||
},
|
||||
availableEvents: ["claim", "assign", "request-adjustment"],
|
||||
} as unknown as ApplicationDetail;
|
||||
}
|
||||
|
||||
function resolve(licenseTypeKey: string) {
|
||||
return resolveActions({
|
||||
detail: detailFor(licenseTypeKey),
|
||||
currentUserId: "me",
|
||||
can: () => true,
|
||||
reasons: REASONS,
|
||||
flaggedCount: 1,
|
||||
hasPendingInspection: false,
|
||||
inspectionNotYetDue: false,
|
||||
allDocumentsAccepted: true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("resolveActions — seafarer certificates skip the queue", () => {
|
||||
it("drops claim and assign for a CoC", () => {
|
||||
const ids = resolve("CERTIFICATE_OF_COMPETENCY").map((a) => a.id);
|
||||
expect(ids).not.toContain("claim");
|
||||
expect(ids).not.toContain("assign");
|
||||
expect(ids).not.toContain("assign-reviewer");
|
||||
});
|
||||
|
||||
it("lets an officer act on a CoP another officer holds", () => {
|
||||
const adjust = resolve("CERTIFICATE_OF_PROFICIENCY").find(
|
||||
(a) => a.id === "request-adjustment",
|
||||
);
|
||||
expect(adjust?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves an ordinary licence claimed and owner-gated", () => {
|
||||
const actions = resolve("FREIGHT_FORWARDER");
|
||||
expect(actions.map((a) => a.id)).toContain("claim");
|
||||
const adjust = actions.find((a) => a.id === "request-adjustment");
|
||||
expect(adjust?.enabled).toBe(false);
|
||||
expect(adjust?.disabledReason).toBe(REASONS.notAssigned);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ApplicationDetail, LicenseStatus } from '@ema-platform/api';
|
||||
import { isSeafarerCertificate } from './license-types';
|
||||
|
||||
/**
|
||||
* Where an action is rendered. One tier per action, decided here rather than
|
||||
@@ -412,6 +413,10 @@ export function resolveActions(ctx: ResolveContext): ResolvedAction[] {
|
||||
const { detail, currentUserId, can, reasons } = ctx;
|
||||
const app = detail.application;
|
||||
const serverEvents = detail.availableEvents;
|
||||
// A seafarer's certificate is reviewed by whoever opens it. There is no
|
||||
// queue to claim it from and no reviewer to assign, so the two actions that
|
||||
// move it through one are dropped and ownership stops gating the decisions.
|
||||
const skipsAssignment = isSeafarerCertificate(app.licenseType?.key);
|
||||
|
||||
return ACTIONS.filter((action) => can(action.permissions)).flatMap<ResolvedAction>(
|
||||
(action) => {
|
||||
@@ -428,6 +433,15 @@ export function resolveActions(ctx: ResolveContext): ResolvedAction[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
skipsAssignment &&
|
||||
(action.id === 'claim' ||
|
||||
action.id === 'assign' ||
|
||||
action.id === 'assign-reviewer')
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Scheduling and recording are the same slot at the same status; which
|
||||
// one applies depends on whether an inspection is already booked.
|
||||
if (action.id === 'schedule-inspection' && ctx.hasPendingInspection) return [];
|
||||
@@ -465,7 +479,9 @@ export function resolveActions(ctx: ResolveContext): ResolvedAction[] {
|
||||
|
||||
// Decisions belong to whoever holds the application.
|
||||
const needsOwnership =
|
||||
action.tier === 'primary' && action.id !== 'confirm-payment';
|
||||
!skipsAssignment &&
|
||||
action.tier === 'primary' &&
|
||||
action.id !== 'confirm-payment';
|
||||
if (
|
||||
needsOwnership &&
|
||||
app.assignedOfficerId &&
|
||||
|
||||
@@ -152,7 +152,13 @@ const PRESENTATION: Record<string, LicenseTypePresentation> = {
|
||||
const CERTIFICATE_KEY_PREFIXES = ['COC_', 'COP_', 'GOC_'];
|
||||
const CERTIFICATE_SECTIONS: DetailSection[] = ['overview', 'documents'];
|
||||
|
||||
function isSeafarerCertificate(key: string): boolean {
|
||||
/**
|
||||
* CoC, CoP and the endorsement/GOC family: a seafarer's own certificate rather
|
||||
* than an organisation's licence. They skip the claim/assign queue — see
|
||||
* `resolveActions`.
|
||||
*/
|
||||
export function isSeafarerCertificate(key: string | undefined): boolean {
|
||||
if (!key) return false;
|
||||
return (
|
||||
CERTIFICATE_KEY_PREFIXES.some((prefix) => key.startsWith(prefix)) ||
|
||||
key === 'CERTIFICATE_OF_COMPETENCY' ||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1154,8 +1154,17 @@ export const licensingApi = baseApi
|
||||
url: `/license-application-review/${id}/issue-certificate`,
|
||||
method: 'POST',
|
||||
}),
|
||||
// The certificate this writes is what `License` lists read, so the
|
||||
// issued register and the holder's own licences refresh with it —
|
||||
// without them an officer issues a certificate that nobody's list shows.
|
||||
invalidatesTags: (_r, error, id) =>
|
||||
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
||||
error
|
||||
? []
|
||||
: [
|
||||
itemTag('LicenseApplication', id),
|
||||
listTag('ApplicationQueue'),
|
||||
listTag('License'),
|
||||
],
|
||||
}),
|
||||
|
||||
// ------------------------------------------------------------- pickup
|
||||
|
||||
Reference in New Issue
Block a user