Adding license generation functionality

This commit is contained in:
Muluhabt
2026-07-29 16:53:50 +03:00
parent 26cc1f4fd2
commit 5161e278ce
102 changed files with 5995 additions and 21326 deletions

View File

@@ -0,0 +1,434 @@
import { baseApi } from '../../base-api';
import type {
AppNotification,
ApplicationDetail,
ApplicationKind,
ApplicationPayment,
ApplicationStaff,
Attachment,
InitiatePaymentResult,
IssuedLicense,
Inspection,
LicenseApplication,
LicenseCategoryDefinition,
LicenseStatus,
LicenseType,
LicenseTypeRequirements,
Paginated,
RemarkTargetType,
} from './licensing.types';
const TAGS = [
'LicenseType',
'LicenseApplication',
'ApplicationQueue',
'Attachment',
'Notification',
'Inspection',
'License',
] 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')],
}),
/**
* 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)],
}),
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
getMyLicenses: builder.query<Paginated<IssuedLicense>, void>({
query: () => ({ url: '/licenses/mine' }),
providesTags: () => [listTag('License')],
}),
getCertificateUrl: builder.mutation<{ url: string }, string>({
query: (id) => ({ url: `/licenses/${id}/certificate` }),
}),
// ------------------------------------------------------------- review
getQueue: builder.query<
Paginated<LicenseApplication>,
{ licenseTypeId?: string; search?: string } | void
>({
query: (params) => ({ url: '/license-application-review/queue', params: params ?? {} }),
providesTags: () => [listTag('ApplicationQueue')],
}),
getAssignedToMe: builder.query<
Paginated<LicenseApplication>,
{ licenseTypeId?: string; search?: string } | void
>({
query: (params) => ({
url: '/license-application-review/assigned-to-me',
params: params ?? {},
}),
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 }[];
}
>({
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 }>({
query: ({ id, reason }) => ({
url: `/license-application-review/${id}/reject`,
method: 'POST',
body: { reason },
}),
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 }
>({
query: ({ inspectionId, result, findings }) => ({
url: `/inspections/${inspectionId}/result`,
method: 'PATCH',
body: { result, findings },
}),
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,
useGetLicenseTypeRequirementsQuery,
useCreateApplicationMutation,
useGetMyApplicationsQuery,
useGetApplicationQuery,
useInitiatePaymentMutation,
useBypassPaymentMutation,
useGetPaymentCapabilitiesQuery,
useGetMyLicensesQuery,
useGetCertificateUrlMutation,
useGetApplicationPaymentQuery,
usePatchSectionMutation,
useAddStaffMutation,
useRemoveStaffMutation,
useSubmitApplicationMutation,
useResolveRemarkMutation,
useResubmitApplicationMutation,
useGetAttachmentsQuery,
useDeleteAttachmentMutation,
useGetQueueQuery,
useGetAssignedToMeQuery,
useGetApplicationForReviewQuery,
useClaimApplicationMutation,
useCompleteReviewMutation,
useRequestAdjustmentMutation,
useApproveDocumentsMutation,
useFinalApproveMutation,
useRejectApplicationMutation,
useConfirmPaymentMutation,
useScheduleInspectionMutation,
useGetInspectionsQuery,
useRecordInspectionResultMutation,
useGetNotificationsQuery,
useGetUnseenNotificationsQuery,
useMarkNotificationReadMutation,
} = licensingApi;