Files
emaui/libs/api/src/lib/features/licensing/licensing-api.ts
fitse-yotor f294f67ced fix(portal,backoffice): six reported issues in registration and the designer
Seafarer and vessel registration were filtered out of the operations
step by `requiresOperatorMode !== false`, so someone registering as a
seafarer or vessel owner landed on an onboarding screen that did not
describe them. Both now appear, grouped apart from the company modes:
declaring "I am a seafarer" is a different kind of statement from "my
company forwards freight".

The designer's preview was gated on the Handlebars source alone, which a
canvas layout does not have until the server compiles it on save -- so
the button was dead for exactly the designs the canvas exists for.

Editing looked broken rather than deliberately read-only: every seeded
template is PUBLISHED, and a published design is immutable because
certificates were issued from it. Says so, and offers the new-version
action that is the way forward.

Reviewing officers saw company, capital and staff tabs on seafarer
certificate applications, because PRESENTATION is keyed by the generic
licence keys and the fifty-odd rank-specific CoC/CoP keys fell through
to the company default. Matched by prefix instead, so a certificate
configured tomorrow gets the right presentation without a code change.

Seaman book and BTC are wired to the newly seeded licence types and no
longer marked "soon".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:21:00 +03:00

853 lines
30 KiB
TypeScript

import { baseApi } from '../../base-api';
import type {
AppNotification,
ApplicationDetail,
ApplicationKind,
ApplicationPayment,
ApplicationStaff,
Attachment,
InitiatePaymentResult,
IssuedLicense,
Inspection,
LicenseApplication,
LicenseCategoryDefinition,
LicenseStatus,
LicenseType,
LicenseTypeRequirements,
OperatorType,
AssignableOfficer,
DocumentDecision,
DocumentReview,
ExportResult,
LicenseTemplate,
Paginated,
QueueCounts,
QueueFilter,
RemarkTargetType,
SavedQueueView,
TemplateFieldPlacement,
TemplateLogoPlacement,
TemplatePageOptions,
TemplateVariable,
} from './licensing.types';
/**
* Drops empty filters and flattens the status array.
*
* The grid keeps every facet in one object and serialises it to the URL, so
* cleared facets arrive as undefined/[]; sending those verbatim would put
* `status=` and `assignee=` on the wire and fail DTO validation.
*/
function serialiseQueueFilter(
filter: QueueFilter | void,
): Record<string, unknown> {
if (!filter) return {};
const params: Record<string, unknown> = {};
for (const [key, value] of Object.entries(filter)) {
if (value === undefined || value === null || value === '') continue;
if (Array.isArray(value)) {
if (value.length === 0) continue;
params[key] = value.join(',');
continue;
}
params[key] = value;
}
return params;
}
const TAGS = [
'LicenseType',
'OperatorType',
'LicenseApplication',
'ApplicationQueue',
'Attachment',
'Notification',
'Inspection',
'License',
'SavedView',
'LicenseTemplate',
] as const;
const listTag = (type: (typeof TAGS)[number]) => ({ type, id: 'LIST' }) as const;
const itemTag = (type: (typeof TAGS)[number], id: string) => ({ type, id }) as const;
/**
* Typed licensing endpoints, shared by the portal and the backoffice.
*
* Each resource gets its own cache tag so a transition invalidates the one
* application and its queue, rather than the whole store.
*/
export const licensingApi = baseApi
.enhanceEndpoints({ addTagTypes: TAGS })
.injectEndpoints({
endpoints: (builder) => ({
// ------------------------------------------------------------- config
/** Active licence types an applicant can choose from. */
getLicenseTypes: builder.query<Paginated<LicenseType>, void>({
query: () => ({ url: '/license-types' }),
providesTags: () => [listTag('LicenseType')],
}),
/**
* The signed-in applicant's declared modes of operation.
*
* Separate from the licence-type catalogue on purpose: the catalogue is
* the same for everyone and heavily cached, while this is per-user and
* changes the moment they edit their profile.
*/
getMyOperatorTypes: builder.query<{ items: OperatorType[] }, void>({
query: () => ({ url: '/profiles/me/operations' }),
providesTags: () => [listTag('OperatorType')],
}),
/** Replaces the set — see the Operations tab in the portal profile. */
updateMyOperatorTypes: builder.mutation<
{ items: OperatorType[] },
{ licenseTypeIds: string[] }
>({
query: (body) => ({
url: '/profiles/me/operations',
method: 'PUT',
body,
}),
// The catalogue is filtered by this, so it has to refetch too.
invalidatesTags: (_r, error) =>
error ? [] : [listTag('OperatorType'), listTag('LicenseType')],
}),
/**
* Category catalogue used to group the licence cards. Static on the
* server, so it is cached under the licence-type list tag.
*/
getLicenseCategories: builder.query<
Paginated<LicenseCategoryDefinition>,
void
>({
query: () => ({ url: '/license-categories' }),
providesTags: () => [listTag('LicenseType')],
}),
/**
* Fee configuration, for licensing administrators.
*
* `null` is a meaningful value for both amounts and must survive the
* round trip: null on the new-application fee means the licence is free,
* null on the renewal fee means it is charged at the new-application
* rate. Sending `undefined` instead would leave the column untouched.
*/
updateLicenseFees: builder.mutation<
LicenseType,
{
id: string;
feeNewApplication?: number | null;
feeRenewal?: number | null;
feeCurrency?: string;
}
>({
query: ({ id, ...body }) => ({
url: `/license-types/${id}/fees`,
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,
{ id: string; validityMonths: number }
>({
query: ({ id, validityMonths }) => ({
url: `/license-types/${id}/validity`,
method: 'PATCH',
body: { validityMonths },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseType', id), listTag('LicenseType')],
}),
getLicenseTypeRequirements: builder.query<
LicenseTypeRequirements,
{ idOrKey: string; kind?: ApplicationKind }
>({
query: ({ idOrKey, kind = 'NEW' }) => ({
url: `/license-types/requirements/${idOrKey}`,
params: { kind },
}),
providesTags: (_r, _e, arg) => [itemTag('LicenseType', arg.idOrKey)],
}),
// -------------------------------------------------------- application
createApplication: builder.mutation<
LicenseApplication,
{ licenseType: string; kind?: ApplicationKind; previousLicenseId?: string }
>({
query: (body) => ({ url: '/license-applications', method: 'POST', body }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseApplication')]),
}),
getMyApplications: builder.query<Paginated<LicenseApplication>, void>({
query: () => ({ url: '/license-applications/mine' }),
providesTags: () => [listTag('LicenseApplication')],
}),
getApplication: builder.query<ApplicationDetail, string>({
query: (id) => ({ url: `/license-applications/${id}` }),
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
}),
patchSection: builder.mutation<
LicenseApplication,
{ id: string; sectionKey: string; values: Record<string, unknown> }
>({
query: ({ id, sectionKey, values }) => ({
url: `/license-applications/${id}/sections/${sectionKey}`,
method: 'PATCH',
body: { values },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id)],
}),
addStaff: builder.mutation<
ApplicationStaff,
{ id: string; roleKey: string; fullName: string; position?: string; yearsOfExperience?: number }
>({
query: ({ id, ...body }) => ({
url: `/license-applications/${id}/staff`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id)],
}),
removeStaff: builder.mutation<unknown, { id: string; staffId: string }>({
query: ({ id, staffId }) => ({
url: `/license-applications/${id}/staff/${staffId}`,
method: 'DELETE',
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id)],
}),
submitApplication: builder.mutation<LicenseApplication, string>({
query: (id) => ({ url: `/license-applications/${id}/submit`, method: 'POST' }),
invalidatesTags: (_r, error, id) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('LicenseApplication'), listTag('ApplicationQueue')],
}),
resolveRemark: builder.mutation<unknown, { id: string; remarkId: string }>({
query: ({ id, remarkId }) => ({
url: `/license-applications/${id}/remarks/${remarkId}/resolve`,
method: 'PATCH',
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id)],
}),
resubmitApplication: builder.mutation<LicenseApplication, string>({
query: (id) => ({ url: `/license-applications/${id}/resubmit`, method: 'POST' }),
invalidatesTags: (_r, error, id) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('LicenseApplication')],
}),
// -------------------------------------------------------- attachments
getAttachments: builder.query<Attachment[], { ownerType: string; ownerId: string }>({
query: ({ ownerType, ownerId }) => ({
url: '/attachments',
params: { ownerType, ownerId, withUrls: true },
}),
providesTags: (_r, _e, arg) => [itemTag('Attachment', arg.ownerId)],
}),
deleteAttachment: builder.mutation<unknown, { attachmentId: string; ownerId: string }>({
query: ({ attachmentId }) => ({
url: `/attachments/${attachmentId}`,
method: 'DELETE',
}),
invalidatesTags: (_r, error, { ownerId }) =>
error ? [] : [itemTag('Attachment', ownerId)],
}),
// ----------------------------------------------------------- payments
initiatePayment: builder.mutation<
InitiatePaymentResult,
{ id: string; provider?: string; platform?: 'web' | 'mobile'; payerAccount?: string }
>({
query: ({ id, ...body }) => ({
url: `/license-applications/${id}/payments/initiate`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id)],
}),
getApplicationPayment: builder.query<ApplicationPayment, string>({
query: (id) => ({ url: `/license-applications/${id}/payments` }),
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
}),
/** Testing shortcut — only offered when the API reports it enabled. */
bypassPayment: builder.mutation<
{ status: LicenseStatus; certificateIssued: boolean },
string
>({
query: (id) => ({
url: `/license-applications/${id}/payments/bypass`,
method: 'POST',
}),
invalidatesTags: (_r, error, id) =>
error
? []
: [itemTag('LicenseApplication', id), listTag('LicenseApplication'), listTag('License')],
}),
getPaymentCapabilities: builder.query<{ bypassEnabled: boolean }, void>({
query: () => ({ url: '/license-applications/payments/capabilities' }),
}),
// ------------------------------------------------------------ licences
/** The licence register for enforcement officers. */
getLicenses: builder.query<
Paginated<IssuedLicense>,
{ search?: string } | void
>({
query: (args) => ({
url: '/licenses',
params: args?.search ? { search: args.search } : undefined,
}),
providesTags: () => [listTag('License')],
}),
getMyLicenses: builder.query<Paginated<IssuedLicense>, void>({
query: () => ({ url: '/licenses/mine' }),
providesTags: () => [listTag('License')],
}),
suspendLicense: builder.mutation<IssuedLicense, { id: string; reason: string }>({
query: ({ id, reason }) => ({
url: `/licenses/${id}/suspend`,
method: 'POST',
body: { reason },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('License', id), listTag('License')],
}),
revokeLicense: builder.mutation<IssuedLicense, { id: string; reason: string }>({
query: ({ id, reason }) => ({
url: `/licenses/${id}/revoke`,
method: 'POST',
body: { reason },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('License', id), listTag('License')],
}),
reinstateLicense: builder.mutation<IssuedLicense, { id: string; reason: string }>({
query: ({ id, reason }) => ({
url: `/licenses/${id}/reinstate`,
method: 'POST',
body: { reason },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('License', id), listTag('License')],
}),
getCertificateUrl: builder.mutation<{ url: string }, string>({
query: (id) => ({ url: `/licenses/${id}/certificate` }),
}),
// ------------------------------------------------------------- review
getQueue: builder.query<Paginated<LicenseApplication>, QueueFilter | void>({
query: (params) => ({
url: '/license-application-review/queue',
params: serialiseQueueFilter(params),
}),
providesTags: () => [listTag('ApplicationQueue')],
}),
getAssignedToMe: builder.query<
Paginated<LicenseApplication>,
QueueFilter | void
>({
query: (params) => ({
url: '/license-application-review/assigned-to-me',
params: serialiseQueueFilter(params),
}),
providesTags: () => [listTag('ApplicationQueue')],
}),
/** Every non-draft application — backs the grid's "All" view. */
getAllApplications: builder.query<
Paginated<LicenseApplication>,
QueueFilter | void
>({
query: (params) => ({
url: '/license-application-review/all',
params: serialiseQueueFilter(params),
}),
providesTags: () => [listTag('ApplicationQueue')],
}),
/**
* Counts for the saved-view tabs. Shares the ApplicationQueue tag, so a
* claim or a decision refreshes the badges along with the list and the
* numbers can never drift from what the grid is showing.
*/
getQueueCounts: builder.query<QueueCounts, void>({
query: () => ({ url: '/license-application-review/counts' }),
providesTags: () => [listTag('ApplicationQueue')],
}),
getApplicationForReview: builder.query<ApplicationDetail, string>({
query: (id) => ({ url: `/license-application-review/${id}` }),
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
}),
claimApplication: builder.mutation<LicenseApplication, string>({
query: (id) => ({ url: `/license-application-review/${id}/claim`, method: 'POST' }),
invalidatesTags: (_r, error, id) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
completeReview: builder.mutation<
LicenseApplication,
{ id: string; capitalAmountVerified?: number; remark?: string }
>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/complete-review`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
requestAdjustment: builder.mutation<
LicenseApplication,
{
id: string;
generalRemark?: string;
items: { targetType: RemarkTargetType; targetKey: string; remark: string }[];
/** Officer-edited wording for the applicant notification. */
notificationBody?: string;
}
>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/request-adjustment`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
approveDocuments: builder.mutation<LicenseApplication, { id: string; remark?: string }>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/approve-documents`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
/**
* `capitalAmountVerified` is what the officer confirms against the bank
* letter. Licence types with a capital threshold reject the approval
* without it, so it is part of the contract, not an optional extra.
*/
finalApprove: builder.mutation<
LicenseApplication,
{ id: string; remark?: string; capitalAmountVerified?: number }
>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/final-approve`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
rejectApplication: builder.mutation<
LicenseApplication,
{ id: string; reason: string; notificationBody?: string }
>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/reject`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
/** Places a candidate who has paid the examination fee into a sitting. */
scheduleExam: builder.mutation<
LicenseApplication,
{ id: string; examId: string; admissionNumber?: string; examDate?: string }
>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/schedule-exam`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
/**
* Raises the examination fee — after eligibility approval, or again when
* a failed candidate elects to resit.
*/
requestExamPayment: builder.mutation<LicenseApplication, string>({
query: (id) => ({
url: `/license-applications/${id}/request-exam-payment`,
method: 'POST',
}),
invalidatesTags: (_r, error, id) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('LicenseApplication')],
}),
// ------------------------------------------------- certificate designs
getLicenseTemplates: builder.query<LicenseTemplate[], string | void>({
query: (licenseTypeId) => ({
url: '/license-templates',
params: licenseTypeId ? { licenseTypeId } : {},
}),
providesTags: () => [listTag('LicenseTemplate')],
}),
getTemplateVariables: builder.query<TemplateVariable[], void>({
query: () => ({ url: '/license-templates/variables' }),
}),
getBuiltInTemplate: builder.query<{ hbsSource: string }, void>({
query: () => ({ url: '/license-templates/built-in' }),
}),
createLicenseTemplate: builder.mutation<
LicenseTemplate,
{
licenseTypeId: string;
name: string;
hbsSource?: string;
pageOptions?: TemplatePageOptions;
}
>({
query: (body) => ({ url: '/license-templates', method: 'POST', body }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
}),
updateLicenseTemplate: builder.mutation<
LicenseTemplate,
{
id: string;
name?: string;
hbsSource?: string;
pageOptions?: TemplatePageOptions;
backgroundUrl?: string;
logoUrl?: string;
logoPlacement?: TemplateLogoPlacement;
fieldPlacements?: TemplateFieldPlacement[];
}
>({
query: ({ id, ...body }) => ({
url: `/license-templates/${id}`,
method: 'PATCH',
body,
}),
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
}),
publishLicenseTemplate: builder.mutation<LicenseTemplate, string>({
query: (id) => ({ url: `/license-templates/${id}/publish`, method: 'POST' }),
// The previously published design is archived by the same call, so the
// whole list is refetched rather than patching two rows by hand.
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
}),
archiveLicenseTemplate: builder.mutation<LicenseTemplate, string>({
query: (id) => ({ url: `/license-templates/${id}/archive`, method: 'POST' }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
}),
deleteLicenseTemplate: builder.mutation<unknown, string>({
query: (id) => ({ url: `/license-templates/${id}`, method: 'DELETE' }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
}),
// ------------------------------------------------------ document review
getDocumentReviews: builder.query<DocumentReview[], string>({
query: (id) => ({ url: `/license-application-review/${id}/document-reviews` }),
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
}),
reviewDocument: builder.mutation<
DocumentReview,
{
id: string;
documentKey: string;
decision: DocumentDecision;
reason?: string;
attachmentId?: string;
}
>({
query: ({ id, documentKey, ...body }) => ({
url: `/license-application-review/${id}/documents/${encodeURIComponent(documentKey)}/review`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id)],
}),
clearDocumentReview: builder.mutation<unknown, { id: string; documentKey: string }>({
query: ({ id, documentKey }) => ({
url: `/license-application-review/${id}/documents/${encodeURIComponent(documentKey)}/review`,
method: 'DELETE',
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id)],
}),
// ----------------------------------------------------------- saved views
getSavedViews: builder.query<SavedQueueView[], void>({
query: () => ({ url: '/license-application-review/saved-views' }),
providesTags: () => [listTag('SavedView')],
}),
createSavedView: builder.mutation<
SavedQueueView,
{ name: string; queryString: string; isShared?: boolean; isDefault?: boolean }
>({
query: (body) => ({
url: '/license-application-review/saved-views',
method: 'POST',
body,
}),
invalidatesTags: (_r, error) => (error ? [] : [listTag('SavedView')]),
}),
deleteSavedView: builder.mutation<unknown, string>({
query: (viewId) => ({
url: `/license-application-review/saved-views/${viewId}`,
method: 'DELETE',
}),
invalidatesTags: (_r, error) => (error ? [] : [listTag('SavedView')]),
}),
// ------------------------------------------------------------- export
/**
* The whole filtered result set, not the current page. Lazy so it only
* runs when the officer actually asks for a file.
*/
exportApplications: builder.query<ExportResult, QueueFilter | void>({
query: (params) => ({
url: '/license-application-review/export',
params: serialiseQueueFilter(params),
}),
}),
/** Officers the Assign and Escalate dialogs can offer. */
getAssignableOfficers: builder.query<AssignableOfficer[], void>({
query: () => ({ url: '/license-application-review/officers' }),
}),
// ----------------------------------------------------- workflow controls
assignApplication: builder.mutation<
LicenseApplication,
{ id: string; officerId: string; remark?: string }
>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/assign`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
holdApplication: builder.mutation<
LicenseApplication,
{ id: string; reason: string }
>({
query: ({ id, reason }) => ({
url: `/license-application-review/${id}/hold`,
method: 'POST',
body: { reason },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
resumeApplication: builder.mutation<
LicenseApplication,
{ id: string; remark?: string }
>({
query: ({ id, remark }) => ({
url: `/license-application-review/${id}/resume`,
method: 'POST',
body: { remark },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
escalateApplication: builder.mutation<
LicenseApplication,
{ id: string; supervisorId: string; reason: string }
>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/escalate`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
confirmPayment: builder.mutation<LicenseApplication, string>({
query: (id) => ({
url: `/license-application-review/${id}/confirm-payment`,
method: 'POST',
}),
invalidatesTags: (_r, error, id) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
// --------------------------------------------------------- inspection
scheduleInspection: builder.mutation<
Inspection,
{ applicationId: string; scheduledDate: string; location?: string }
>({
query: (body) => ({ url: '/inspections', method: 'POST', body }),
invalidatesTags: (_r, error, { applicationId }) =>
error ? [] : [itemTag('LicenseApplication', applicationId), listTag('Inspection')],
}),
getInspections: builder.query<Inspection[], string>({
query: (applicationId) => ({ url: `/inspections/application/${applicationId}` }),
providesTags: () => [listTag('Inspection')],
}),
recordInspectionResult: builder.mutation<
Inspection,
{
inspectionId: string;
applicationId: string;
result: 'PASSED' | 'FAILED';
findings: string;
/** Structured per-item outcomes (office, warehouse, vehicles, …). */
checklist?: {
key: string;
label: string;
outcome: 'PASS' | 'FAIL' | 'NEEDS_CORRECTION';
note?: string;
}[];
}
>({
query: ({ inspectionId, result, findings, checklist }) => ({
url: `/inspections/${inspectionId}/result`,
method: 'PATCH',
body: { result, findings, ...(checklist?.length ? { checklist } : {}) },
}),
invalidatesTags: (_r, error, { applicationId }) =>
error ? [] : [itemTag('LicenseApplication', applicationId), listTag('Inspection')],
}),
// ------------------------------------------------------ notifications
getNotifications: builder.query<{ count: number; items: AppNotification[] }, void>({
query: () => ({ url: '/notifications' }),
providesTags: () => [listTag('Notification')],
}),
getUnseenNotifications: builder.query<{ count: number; items: AppNotification[] }, void>({
query: () => ({ url: '/notifications/unseen' }),
providesTags: () => [listTag('Notification')],
}),
markNotificationRead: builder.mutation<unknown, string>({
query: (id) => ({ url: `/notifications/${id}/read`, method: 'PATCH' }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('Notification')]),
}),
}),
overrideExisting: false,
});
export const {
useGetLicenseTypesQuery,
useGetLicenseCategoriesQuery,
useUpdateLicenseFeesMutation,
useUpdateLicenseValidityMutation,
useGetLicenseTypeRequirementsQuery,
useCreateApplicationMutation,
useGetMyApplicationsQuery,
useGetApplicationQuery,
useInitiatePaymentMutation,
useBypassPaymentMutation,
useGetPaymentCapabilitiesQuery,
useGetMyLicensesQuery,
useGetLicensesQuery,
useGetCertificateUrlMutation,
useGetApplicationPaymentQuery,
usePatchSectionMutation,
useAddStaffMutation,
useRemoveStaffMutation,
useSubmitApplicationMutation,
useResolveRemarkMutation,
useResubmitApplicationMutation,
useGetAttachmentsQuery,
useDeleteAttachmentMutation,
useGetQueueQuery,
useGetAssignedToMeQuery,
useGetAllApplicationsQuery,
useGetQueueCountsQuery,
useGetLicenseTemplatesQuery,
useGetTemplateVariablesQuery,
useGetBuiltInTemplateQuery,
useCreateLicenseTemplateMutation,
useUpdateLicenseTemplateMutation,
usePublishLicenseTemplateMutation,
useArchiveLicenseTemplateMutation,
useDeleteLicenseTemplateMutation,
useGetDocumentReviewsQuery,
useReviewDocumentMutation,
useClearDocumentReviewMutation,
useGetSavedViewsQuery,
useCreateSavedViewMutation,
useDeleteSavedViewMutation,
useLazyExportApplicationsQuery,
useGetAssignableOfficersQuery,
useSuspendLicenseMutation,
useRevokeLicenseMutation,
useReinstateLicenseMutation,
useAssignApplicationMutation,
useHoldApplicationMutation,
useResumeApplicationMutation,
useEscalateApplicationMutation,
useGetApplicationForReviewQuery,
useClaimApplicationMutation,
useCompleteReviewMutation,
useRequestAdjustmentMutation,
useApproveDocumentsMutation,
useFinalApproveMutation,
useRejectApplicationMutation,
useScheduleExamMutation,
useRequestExamPaymentMutation,
useConfirmPaymentMutation,
useScheduleInspectionMutation,
useGetInspectionsQuery,
useRecordInspectionResultMutation,
useGetNotificationsQuery,
useGetUnseenNotificationsQuery,
useMarkNotificationReadMutation,
useGetMyOperatorTypesQuery,
useUpdateMyOperatorTypesMutation,
} = licensingApi;