mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 17:45:42 +00:00
add dispute functionality for contract duty and implement collection dates
This commit is contained in:
@@ -30,18 +30,35 @@ export class BookingRequestService {
|
||||
private readonly notifier: ContractNotifierService,
|
||||
) {}
|
||||
|
||||
/** Only GENERAL contracts that bundle customs use the request → GL → clearance flow. */
|
||||
private assertGeneralCustoms(contract: Contract): void {
|
||||
if (
|
||||
contract.contractKind !== 'GENERAL' ||
|
||||
!contract.customsClearingEnabled
|
||||
) {
|
||||
/**
|
||||
* Shipment requests exist because on a CUSTOMS contract the customer never
|
||||
* books directly — GL Ethiopia does it for them. The request is how the
|
||||
* customer states what to ship and, now, which currency to be invoiced in.
|
||||
*
|
||||
* GENERAL: each request opens its own per-booking clearance instance.
|
||||
* ONE_TIME: clearance already ran at the contract level, so the request only
|
||||
* records the customer's intent; GL creates the single booking from it.
|
||||
*/
|
||||
private assertCustomsContract(contract: Contract): void {
|
||||
if (!contract.customsClearingEnabled) {
|
||||
throw new BadRequestException(
|
||||
'Shipment requests apply only to general customs-clearance contracts.',
|
||||
'Shipment requests apply only to customs-clearance contracts.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statuses in which a ONE_TIME customs contract may take a shipment request:
|
||||
* both signatures are in and the contract is at (or past) its clearance
|
||||
* phase, but GL has not booked yet.
|
||||
*/
|
||||
private static readonly ONE_TIME_REQUESTABLE_STATUSES = [
|
||||
'FULLY_EXECUTED',
|
||||
'AWAITING_CLEARANCE_DOCUMENTS',
|
||||
'CLEARANCE_UNDER_REVIEW',
|
||||
'CLEARANCE_READY_FOR_BOOKING',
|
||||
];
|
||||
|
||||
/** Customer submits a shipment request. */
|
||||
async submit(
|
||||
contractId: string,
|
||||
@@ -50,13 +67,35 @@ export class BookingRequestService {
|
||||
): Promise<BookingRequest> {
|
||||
const contract = await this.contractsService.findById(contractId);
|
||||
await this.contractsService.assertCustomerCanAccessContract(userId, contract);
|
||||
this.assertGeneralCustoms(contract);
|
||||
this.assertCustomsContract(contract);
|
||||
const isOneTime = contract.contractKind === 'ONE_TIME';
|
||||
|
||||
if (contract.status === 'CONTRACT_CLOSED') {
|
||||
throw new ConflictException(
|
||||
'This contract is completed — the full contracted quantity has been booked.',
|
||||
);
|
||||
}
|
||||
if (contract.status !== 'CONTRACT_ACTIVE') {
|
||||
if (isOneTime) {
|
||||
if (
|
||||
!BookingRequestService.ONE_TIME_REQUESTABLE_STATUSES.includes(
|
||||
contract.status,
|
||||
)
|
||||
) {
|
||||
throw new ConflictException(
|
||||
'The contract must be fully executed before requesting its shipment.',
|
||||
);
|
||||
}
|
||||
// A one-time contract carries exactly one shipment, so it carries at most
|
||||
// one open request — otherwise GL sees two conflicting currencies.
|
||||
const open = (await this.repo.findForContract(contractId)).find(
|
||||
(r) => r.status === 'PENDING',
|
||||
);
|
||||
if (open) {
|
||||
throw new ConflictException(
|
||||
`Shipment request ${open.reference} is already open on this contract.`,
|
||||
);
|
||||
}
|
||||
} else if (contract.status !== 'CONTRACT_ACTIVE') {
|
||||
throw new ConflictException(
|
||||
'The contract must be active before requesting a shipment.',
|
||||
);
|
||||
@@ -90,10 +129,14 @@ export class BookingRequestService {
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.contractBookingService.assertRequestWithinCapacity(contract, {
|
||||
containers: dto.containers,
|
||||
bulk: dto.bulk,
|
||||
});
|
||||
// Draw-down capacity is a GENERAL concept — a ONE_TIME contract's single
|
||||
// shipment is bounded by the contract scope itself, checked when GL books.
|
||||
if (!isOneTime) {
|
||||
await this.contractBookingService.assertRequestWithinCapacity(contract, {
|
||||
containers: dto.containers,
|
||||
bulk: dto.bulk,
|
||||
});
|
||||
}
|
||||
|
||||
const requestedLines: Freight.RequestedShipmentLines = isContainer
|
||||
? {
|
||||
@@ -119,10 +162,17 @@ export class BookingRequestService {
|
||||
// reviews the documents in the clearance queue and completes the booking
|
||||
// (container numbers, VGM, shipment day) once clearance is ready. The
|
||||
// instance is created first so a failure leaves no half-linked request.
|
||||
const booking = await this.contractBookingService.initiateForShipmentRequest(
|
||||
contract,
|
||||
{ contractRouteId: dto.contractRouteId, userId },
|
||||
);
|
||||
// GENERAL: the request immediately opens a BARE booking instance that runs
|
||||
// per-booking phased customs clearance. ONE_TIME: clearance already ran on
|
||||
// the contract, so there is nothing to open — the request stays PENDING
|
||||
// until GL creates the contract's single booking from it.
|
||||
const booking = isOneTime
|
||||
? null
|
||||
: await this.contractBookingService.initiateForShipmentRequest(contract, {
|
||||
contractRouteId: dto.contractRouteId,
|
||||
userId,
|
||||
paymentCurrency: dto.paymentCurrency,
|
||||
});
|
||||
|
||||
const reference = await this.generateReference();
|
||||
const request = await this.repo.create({
|
||||
@@ -131,9 +181,14 @@ export class BookingRequestService {
|
||||
requestedByUserId: userId ?? null,
|
||||
contractRouteId: dto.contractRouteId ?? null,
|
||||
scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : null,
|
||||
status: 'ACCEPTED',
|
||||
createdBookingId: booking.id,
|
||||
status: booking ? 'ACCEPTED' : 'PENDING',
|
||||
createdBookingId: booking?.id ?? null,
|
||||
requestedLines,
|
||||
// Intercity is invoiced in birr whatever the customer picked.
|
||||
paymentCurrency:
|
||||
contract.tradeDirection === 'DOMESTIC'
|
||||
? 'ETB'
|
||||
: (dto.paymentCurrency ?? contract.paymentCurrency ?? 'USD'),
|
||||
notes: dto.notes ?? null,
|
||||
} as never);
|
||||
this.notifier.shipmentRequestedToStaff(contract, request.id, request.reference);
|
||||
|
||||
Reference in New Issue
Block a user