mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 04:20:55 +00:00
Merge pull request #989 from Tria-plc/freight_feature/usermanagement
update body-parser implementation to use Nest's API and improve depe…
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
|
||||
import { ContractBookingService } from './contract-booking.service';
|
||||
import { Contract } from './entities/contract.entity';
|
||||
|
||||
/**
|
||||
* Who may open a shipment instance on a customs (Path B) contract. The customer
|
||||
* initiates his own ONE_TIME customs booking and uploads the GL-input documents
|
||||
* on it; GL still clears it and completes it with cargo and price. GENERAL
|
||||
* customs instances come from a shipment request, and completing/creating a
|
||||
* customs booking outright stays GL-only.
|
||||
*/
|
||||
describe('ContractBookingService — customs booking gate', () => {
|
||||
function makeService() {
|
||||
return new ContractBookingService(
|
||||
{} as never, // contractsRepository
|
||||
{} as never, // bookingsRepository
|
||||
{} as never, // bookingPricingService
|
||||
{} as never, // consolidationService
|
||||
{} as never, // containerTypesService
|
||||
{} as never, // ruleEngineService
|
||||
{} as never, // milestoneService
|
||||
{} as never, // invoiceService
|
||||
{} as never, // bookingNotifier
|
||||
{} as never, // dataSource
|
||||
{} as never, // trainSchedulingService
|
||||
{} as never, // bookingBatchService
|
||||
{} as never, // bookingTransitionService
|
||||
);
|
||||
}
|
||||
|
||||
type WithPrivate = {
|
||||
assertGate: (
|
||||
c: Contract,
|
||||
isGlActor: boolean,
|
||||
isInitiate?: boolean,
|
||||
) => Promise<string>;
|
||||
};
|
||||
|
||||
const customsContract = (contractKind: 'ONE_TIME' | 'GENERAL'): Contract =>
|
||||
({
|
||||
id: 'c-1',
|
||||
contractKind,
|
||||
status: 'FULLY_EXECUTED',
|
||||
customsClearingEnabled: true,
|
||||
}) as Contract;
|
||||
|
||||
const gate = (c: Contract, isGl: boolean, isInitiate?: boolean) =>
|
||||
(makeService() as never as WithPrivate).assertGate(c, isGl, isInitiate);
|
||||
|
||||
it('lets the customer initiate a ONE_TIME customs shipment', async () => {
|
||||
await expect(gate(customsContract('ONE_TIME'), false, true)).resolves.toBe(
|
||||
'CUSTOMER',
|
||||
);
|
||||
});
|
||||
|
||||
it('still lets GL initiate on the customer behalf', async () => {
|
||||
await expect(gate(customsContract('ONE_TIME'), true, true)).resolves.toBe(
|
||||
'GL_ET',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a customer creating a customs booking outright (cargo + day)', async () => {
|
||||
await expect(gate(customsContract('ONE_TIME'), false)).rejects.toBeInstanceOf(
|
||||
ForbiddenException,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a customer initiating a GENERAL customs shipment (request only)', async () => {
|
||||
await expect(
|
||||
gate(customsContract('GENERAL'), false, true),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
});
|
||||
@@ -397,9 +397,11 @@ export class ContractBookingService {
|
||||
*
|
||||
* - Path A (self-clearance): the customer initiates, uploads his clearance
|
||||
* proof, Operations reviews and finalizes.
|
||||
* - Path B (customs): GL initiates on the customer's behalf, the customer
|
||||
* uploads the GL-input documents on the instance, GL approves them and runs
|
||||
* the phased ET/DJ workflow (pre-booking milestones are seeded here).
|
||||
* - Path B (customs, ONE_TIME): the customer initiates too, then uploads the
|
||||
* GL-input documents on the instance; GL approves them and runs the phased
|
||||
* ET/DJ workflow (pre-booking milestones are seeded here). GL may still
|
||||
* initiate on his behalf. GENERAL customs instances come from a shipment
|
||||
* request ({@link initiateForShipmentRequest}), not from here.
|
||||
*
|
||||
* Only after the clearance is finalized is the booking completed (cargo +
|
||||
* binding day + window check) via {@link completeUnderContract} — by the
|
||||
@@ -432,9 +434,10 @@ export class ContractBookingService {
|
||||
const isGlActor =
|
||||
actorPermissions != null &&
|
||||
hasFreightPermission(actorPermissions, FREIGHT_PERMS.contracts.createBooking);
|
||||
// Customs (Path B): GL initiates on the customer's behalf — assertGate
|
||||
// rejects anyone else. Self-clearance (Path A): the customer initiates.
|
||||
const createdByRole = await this.assertGate(contract, isGlActor);
|
||||
// The customer initiates his own shipment instance on ONE_TIME contracts
|
||||
// (customs or self-clearance); GL may also initiate on a customs contract.
|
||||
// GENERAL customs instances come from a shipment request, not from here.
|
||||
const createdByRole = await this.assertGate(contract, isGlActor, true);
|
||||
|
||||
if (contract.contractValidUntil && contract.contractValidUntil.getTime() < Date.now()) {
|
||||
throw new BadRequestException('Contract validity has expired — no new bookings.');
|
||||
@@ -991,7 +994,11 @@ export class ContractBookingService {
|
||||
* Returns the role to stamp on the booking, or throws if the caller is not
|
||||
* allowed to create one for this contract's execution path.
|
||||
*/
|
||||
private async assertGate(contract: Contract, isGlActor: boolean): Promise<string> {
|
||||
private async assertGate(
|
||||
contract: Contract,
|
||||
isGlActor: boolean,
|
||||
isInitiate = false,
|
||||
): Promise<string> {
|
||||
// Suspended contracts are frozen for everyone, GL included — say so instead
|
||||
// of letting the executed-status check below give a misleading reason.
|
||||
if (contract.status === 'SUSPENDED') {
|
||||
@@ -1000,10 +1007,13 @@ export class ContractBookingService {
|
||||
);
|
||||
}
|
||||
if (contract.customsClearingEnabled) {
|
||||
// Path B — Global Logistics initiates and completes the booking ON BEHALF
|
||||
// OF the customer. The customer never books a customs contract himself;
|
||||
// he only uploads documents on the instance GL opened for him.
|
||||
if (!isGlActor) {
|
||||
// Path B — the customer OPENS the shipment instance on a ONE_TIME customs
|
||||
// contract (one click, no cargo) and uploads the GL-input documents on it;
|
||||
// GL still runs the phased ET/DJ clearance and completes the booking with
|
||||
// cargo, day and price. A GENERAL customs instance is opened by a shipment
|
||||
// request instead, and completing any customs booking stays GL-only.
|
||||
const customerMayInitiate = isInitiate && contract.contractKind === 'ONE_TIME';
|
||||
if (!isGlActor && !customerMayInitiate) {
|
||||
throw new ForbiddenException(
|
||||
'Customs-clearance contracts are booked by Global Logistics on behalf of the customer.',
|
||||
);
|
||||
@@ -1015,7 +1025,7 @@ export class ContractBookingService {
|
||||
'Contract must be fully executed before booking a shipment.',
|
||||
);
|
||||
}
|
||||
return 'GL_ET';
|
||||
return isGlActor ? 'GL_ET' : 'CUSTOMER';
|
||||
}
|
||||
|
||||
// Path A — customer (or staff) once the contract is executed.
|
||||
@@ -1028,11 +1038,11 @@ export class ContractBookingService {
|
||||
}
|
||||
|
||||
/**
|
||||
* GL worklist: executed ONE_TIME customs contracts with no live shipment
|
||||
* instance yet. Customs contracts are initiated by GL on the customer's
|
||||
* behalf, so without this list a signed contract would sit with nothing on any
|
||||
* queue (clearance lives on the booking, and the booking does not exist yet).
|
||||
* GENERAL customs is excluded — its instances are opened by shipment requests.
|
||||
* GL fallback worklist: executed ONE_TIME customs contracts with no live
|
||||
* shipment instance yet. The customer normally opens it himself from the
|
||||
* portal; this list lets GL do it on his behalf, and shows the contracts that
|
||||
* are on no other queue (clearance lives on the booking, which does not exist
|
||||
* yet). GENERAL customs is excluded — opened by shipment requests.
|
||||
*/
|
||||
async awaitingShipmentContracts(): Promise<Contract[]> {
|
||||
const { items } = await this.contractsRepository.findAllPaginated({
|
||||
|
||||
@@ -1278,9 +1278,10 @@ export class ContractTransitionService {
|
||||
// Clearance ALWAYS runs per booking — both contract kinds, both paths, and
|
||||
// intercity. A signed contract carries no clearance cycle and collects no
|
||||
// documents: the shipment instance created after signature does. Customs
|
||||
// (Path B): GL initiates the booking, the customer uploads on it, GL
|
||||
// reviews and completes it. Self-clearance (Path A) and intercity: the
|
||||
// customer initiates/books and Operations reviews the booking documents.
|
||||
// (Path B): the customer initiates the booking (GENERAL: via a shipment
|
||||
// request) and uploads on it, GL reviews and completes it. Self-clearance
|
||||
// (Path A) and intercity: the customer initiates/books and Operations
|
||||
// reviews the booking documents.
|
||||
updates.status =
|
||||
contract.contractKind === 'GENERAL' ? 'CONTRACT_ACTIVE' : 'FULLY_EXECUTED';
|
||||
updates.clearanceStatus = 'NOT_APPLICABLE';
|
||||
|
||||
@@ -1068,7 +1068,7 @@ export class ContractsController {
|
||||
@Post(':id/bookings/initiate')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Initiate a bare booking instance under an import/export contract (ONE_TIME or GENERAL) — no cargo, no date; enters per-booking clearance (AWAITING_DOCUMENTS). Customs contracts are initiated by GL Ethiopia.',
|
||||
'Initiate a bare booking instance under an import/export contract (ONE_TIME or GENERAL) — no cargo, no date; enters per-booking clearance (AWAITING_DOCUMENTS). ONE_TIME customs instances are opened by the customer (or GL); GENERAL customs comes from a shipment request.',
|
||||
})
|
||||
initiateBooking(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
|
||||
Reference in New Issue
Block a user