feat: add configuration UI for license behavior and multi-stage examination fees

This commit is contained in:
estifanos
2026-08-29 06:34:33 +00:00
parent af3fad018a
commit 2e9083211f
8 changed files with 764 additions and 1 deletions

View File

@@ -21,6 +21,8 @@ import type {
LicenseTypeRequirements,
OperatorType,
AssignableOfficer,
CertificateCategory,
CompletionEffect,
DocumentDecision,
DocumentReview,
EligibleExam,
@@ -37,12 +39,14 @@ import type {
RemarkTargetType,
SavedQueueView,
SchemaIssue,
ServiceKind,
TemplateFieldPlacement,
TemplateLogoPlacement,
TemplatePageOptions,
TemplateVariable,
PersonalDocumentFilter,
PersonalDocumentGroup,
WorkflowProfile,
} from './licensing.types';
/**
@@ -182,6 +186,12 @@ export const licensingApi = baseApi
feeNewApplication?: number | null;
feeRenewal?: number | null;
feeCurrency?: string;
// The examined-certificate stages. Unlike the two above, these are
// read live off the licence type rather than snapshotted, so the
// server refuses to clear one a candidate is currently waiting on.
feeEligibility?: number | null;
feeExamination?: number | null;
feeCertificate?: number | null;
}
>({
query: ({ id, ...body }) => ({
@@ -193,6 +203,47 @@ export const licensingApi = baseApi
error ? [] : [listTag('LicenseType'), itemTag('LicenseType', id)],
}),
/**
* How a licence type behaves: its workflow, eligibility gates, renewal
* policy and applicant rules — everything that used to be settable only
* by editing a seed file.
*
* Three of these (`workflowProfile`, `completionEffect`,
* `requiresExamination`) decide the course an application runs, and are
* read live rather than snapshotted. The server answers 409
* `license_type_in_use` when changing one would strand applications that
* have not yet had their approval decision.
*/
updateLicenseBehavior: builder.mutation<
LicenseType,
{
id: string;
workflowProfile?: WorkflowProfile;
serviceKind?: ServiceKind;
completionEffect?: CompletionEffect | null;
certificateCategory?: CertificateCategory | null;
requiresExamination?: boolean;
requiresSeafarerRegistration?: boolean;
requiresValidMedical?: boolean;
minSeaTimeDays?: number | null;
renewalWindowDays?: number;
expiryReminderDays?: number[];
requiresOperatorMode?: boolean;
allowMultipleOpenDrafts?: boolean;
requiresIssuanceScheduling?: boolean;
uniqueFormKeyPath?: string | null;
slaHours?: number | null;
}
>({
query: ({ id, ...body }) => ({
url: `/license-types/${id}/behavior`,
method: 'PATCH',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [listTag('LicenseType'), itemTag('LicenseType', id)],
}),
/** Validity is edited beside the certificate design, not with the fees. */
updateLicenseValidity: builder.mutation<
LicenseType,
@@ -1240,6 +1291,7 @@ export const {
useGetLicenseTypesQuery,
useGetLicenseCategoriesQuery,
useUpdateLicenseFeesMutation,
useUpdateLicenseBehaviorMutation,
useUpdateFormSchemaMutation,
useValidateFormSchemaMutation,
useGetFormSchemaPaletteQuery,

View File

@@ -400,6 +400,15 @@ const ERROR_MESSAGES: Record<string, string> = {
inspection_not_passed:
'Approval requires a passed inspection. Schedule a re-inspection or request corrections.',
license_type_inactive: 'This licence type is not currently accepting applications.',
// Licence-type configuration guards. Each of these refuses a change that
// would leave applications already in progress unable to move.
license_type_in_use:
'Applications of this type are already in progress, and this setting decides the course they run. Wait until those have been decided, or change something else.',
stage_fee_in_use:
'Candidates are currently waiting to pay this fee. Removing it would leave them unable to pay and unable to continue — set a different amount instead.',
form_schema_missing_protected_paths:
'The form for this licence type does not contain the field this setting depends on. Add the field to the form first.',
};
export function extractErrorMessage(error: unknown, fallback = 'Something went wrong'): string {

View File

@@ -216,11 +216,39 @@ export interface LicenseType {
// --------------------------------------------------- examined certificates
/** Approval establishes eligibility; the certificate is earned by exam. */
requiresExamination?: boolean;
/** Assessment fee, due on submission before any officer review. */
feeEligibility?: string | number | null;
/** Fee per sitting. Falls back to `feeNewApplication` when null. */
feeExamination?: string | number | null;
/** Fee to issue after a pass. Falls back to `feeNewApplication` when null. */
feeCertificate?: string | number | null;
// ------------------------------------------------------- behaviour config
// Editable from the backoffice Behavior tab. Optional because the API has
// only recently begun returning them and older mocks/fixtures omit them.
/** Licence or registration. Catalogue metadata; no behaviour hangs off it. */
serviceKind?: ServiceKind;
/** Platform side effect fired when an application of this type completes. */
completionEffect?: CompletionEffect | null;
/** Only ACTIVE registered seafarers may apply. */
requiresSeafarerRegistration?: boolean;
/** Submission requires a current, unexpired medical certificate. */
requiresValidMedical?: boolean;
/** Minimum VERIFIED sea time in days at submission. Null means no floor. */
minSeaTimeDays?: number | null;
/** Whether several open drafts may exist at once, for per-asset registrations. */
allowMultipleOpenDrafts?: boolean;
/** Days before expiry that renewal opens. */
renewalWindowDays?: number;
/** Days before expiry to remind the holder, most distant first. */
expiryReminderDays?: number[];
/**
* Dotted `formData` path whose answer may appear on only one live
* application of this type. Null means no such rule.
*/
uniqueFormKeyPath?: string | null;
// ---------------------------------------------------------- STCW mapping
certificateCategory?: CertificateCategory | null;
stcwControlled?: boolean;
@@ -579,6 +607,15 @@ export type QueueSortField =
/** Review → evaluation → (inspection) → approval, or the short registration course. */
export type WorkflowProfile = "STANDARD" | "REGISTRATION";
/** A permission to operate, or a registration granting a status and a number. */
export type ServiceKind = "LICENSE" | "REGISTRATION";
/** Platform side effect fired when an application reaches COMPLETED. */
export type CompletionEffect =
| "REGISTER_SEAFARER"
| "REGISTER_VESSEL"
| "OPEN_SEAFARER_DOCUMENTS";
/** Row counts behind the queue's saved-view tabs. */
export interface QueueCounts {
unassigned: number;