feat(last-mile): customer-signed LM contract gates the advance invoice

Approval now snapshots the rate estimate and generates a last-mile
contract instead of invoicing immediately. The customer picks a delivery
date on the confirm form, then reviews and signs the contract in the
portal (saved signature or drawn); the signed PDF is stored as
LM_<CustomerName>.pdf and only then is the advance invoice issued.
Backoffice shows signature status and the contract download.
This commit is contained in:
Hagernesh
2026-08-06 07:10:53 +00:00
parent 4716aa14c3
commit 4786c896b5
14 changed files with 937 additions and 24 deletions

View File

@@ -10,11 +10,35 @@ export interface LastMileRequest {
trainScheduleId: string;
status: "AWAITING_CONFIRMATION" | "SUBMITTED" | "APPROVED" | "REJECTED";
requestedContainerNumbers?: string[] | null;
requestedDeliveryDate?: string | null;
customerSignedAt?: string | null;
rejectionReason?: string | null;
createdAt: string;
updatedAt: string;
}
export interface LastMileContractView {
requestId: string;
bookingId: string;
bookingReference?: string | null;
status: LastMileRequest["status"];
html: string;
customerSignedAt: string | null;
signerDisplayName: string | null;
canSign: boolean;
savedSignature?: {
signerDisplayName: string;
signatureImageUrl: string | null;
stampImageUrl: string | null;
};
}
export interface SignLastMileContractPayload {
signatureImageBase64?: string;
signerDisplayName: string;
consentText?: string;
}
export const lastMileRequestsService = {
/** One last-mile confirmation request, by id. */
get: async (id: string): Promise<LastMileRequest> => {
@@ -22,12 +46,36 @@ export const lastMileRequestsService = {
return data.data ?? data;
},
/** Confirm which containers on the booking go via EDR last-mile. */
/** Confirm which containers go via EDR last-mile and the requested delivery date. */
submit: async (
id: string,
containerNumbers: string[],
deliveryDate: string,
): Promise<LastMileRequest> => {
const { data } = await client.post(L.SUBMIT(id), { containerNumbers });
const { data } = await client.post(L.SUBMIT(id), { containerNumbers, deliveryDate });
return data.data ?? data;
},
/** LM contract view model + rendered HTML + saved signature. */
getContractView: async (id: string): Promise<LastMileContractView> => {
const { data } = await client.get(L.CONTRACT_VIEW(id));
return data.data ?? data;
},
/** Agree & sign the LM contract — the advance invoice is issued right after. */
signContract: async (
id: string,
payload: SignLastMileContractPayload,
): Promise<LastMileRequest> => {
const { data } = await client.post(L.CONTRACT_SIGN(id), payload);
return data.data ?? data;
},
/** The LM contract PDF (LM_<CustomerName>.pdf). */
downloadContractDocument: async (id: string): Promise<Blob> => {
const { data } = await client.get(L.CONTRACT_DOCUMENT(id), {
responseType: "blob",
});
return data;
},
};