mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 11:33:41 +00:00
Implement intercity document handling and rejection notes for contracts
This commit is contained in:
@@ -168,6 +168,17 @@ export class BookingLifecycleNotifierService {
|
||||
});
|
||||
}
|
||||
|
||||
/** Intercity documents approved → booking waits in the ride-along pool. */
|
||||
intercityDocumentsApproved(b: Booking): void {
|
||||
const msg =
|
||||
`Documents for intercity booking ${b.reference} are approved. ` +
|
||||
`Operations will assign your shipment to a passing train; payment opens once it is accepted.`;
|
||||
void this.notifyContact(b, msg, 'DOCUMENTS APPROVED');
|
||||
this.inApp(b, 'Documents approved', msg, {
|
||||
type: NotificationType.CLEARANCE_DECISION,
|
||||
});
|
||||
}
|
||||
|
||||
/** Operations returned the operation request for changes. */
|
||||
operationChangesRequested(b: Booking, note: string): void {
|
||||
const msg =
|
||||
|
||||
@@ -840,6 +840,22 @@ export class BookingTransitionService {
|
||||
}
|
||||
}
|
||||
|
||||
// Intercity: there is no shipment-day request step — an approved booking
|
||||
// goes straight to FULLY_EXECUTED, which is what the intercity ride-along
|
||||
// pool keys on. Staff then accept it onto a passing train (that accept
|
||||
// opens the pay window).
|
||||
if (booking.tradeDirection === "DOMESTIC") {
|
||||
const now = new Date();
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
status: "FULLY_EXECUTED",
|
||||
fullyExecutedAt: now,
|
||||
lockedAt: booking.lockedAt ?? now,
|
||||
} as never);
|
||||
const fresh = await this.bookingsService.findById(bookingId);
|
||||
this.notifier.intercityDocumentsApproved(fresh);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
status: "CLEARANCE_READY",
|
||||
} as never);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
clearanceSettingCode,
|
||||
clearanceOutputSettingCode,
|
||||
clearanceCodesForBooking,
|
||||
INTERCITY_DOCUMENTS_SETTING_CODE,
|
||||
} from './clearance.util';
|
||||
import type { Booking } from './entities/booking.entity';
|
||||
|
||||
describe('clearance.util — clearanceSettingCode', () => {
|
||||
it('resolves import container with/without customs', () => {
|
||||
@@ -24,9 +27,49 @@ describe('clearance.util — clearanceSettingCode', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null for DOMESTIC (no clearance gate)', () => {
|
||||
expect(clearanceSettingCode('DOMESTIC', 'CONTAINER', true)).toBeNull();
|
||||
expect(clearanceSettingCode('DOMESTIC', 'BULK', false)).toBeNull();
|
||||
it('resolves the intercity document set for DOMESTIC regardless of customs/freight', () => {
|
||||
expect(clearanceSettingCode('DOMESTIC', 'CONTAINER', true)).toBe(
|
||||
INTERCITY_DOCUMENTS_SETTING_CODE,
|
||||
);
|
||||
expect(clearanceSettingCode('DOMESTIC', 'BULK', false)).toBe(
|
||||
INTERCITY_DOCUMENTS_SETTING_CODE,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearance.util — clearanceCodesForBooking (intercity)', () => {
|
||||
const base = {
|
||||
tradeDirection: 'DOMESTIC',
|
||||
freightType: 'CONTAINER',
|
||||
serviceType: null,
|
||||
customsClearingEnabled: false,
|
||||
};
|
||||
|
||||
it('GENERAL drawdowns and direct bookings carry the per-booking intercity set', () => {
|
||||
const general = clearanceCodesForBooking({
|
||||
...base,
|
||||
contractId: 'c1',
|
||||
contractKind: 'GENERAL',
|
||||
} as unknown as Booking);
|
||||
expect(general.inputCode).toBe(INTERCITY_DOCUMENTS_SETTING_CODE);
|
||||
expect(general.outputCode).toBeNull();
|
||||
|
||||
const direct = clearanceCodesForBooking({
|
||||
...base,
|
||||
contractId: null,
|
||||
contractKind: null,
|
||||
} as unknown as Booking);
|
||||
expect(direct.inputCode).toBe(INTERCITY_DOCUMENTS_SETTING_CODE);
|
||||
});
|
||||
|
||||
it('ONE_TIME contract drawdowns skip the per-booking set (contract collected it)', () => {
|
||||
const drawdown = clearanceCodesForBooking({
|
||||
...base,
|
||||
contractId: 'c1',
|
||||
contractKind: 'ONE_TIME',
|
||||
} as unknown as Booking);
|
||||
expect(drawdown.inputCode).toBeNull();
|
||||
expect(drawdown.outputCode).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,11 +9,19 @@ import { Booking } from './entities/booking.entity';
|
||||
type Op = 'import' | 'export';
|
||||
type Freight = 'container' | 'bulk';
|
||||
|
||||
/**
|
||||
* The single (admin-configured) document set intercity shipments upload.
|
||||
* DOMESTIC has no customs, so one shared set serves contracts and bookings:
|
||||
* ONE_TIME collects it at contract level, GENERAL per booking — Operations
|
||||
* reviews either way.
|
||||
*/
|
||||
export const INTERCITY_DOCUMENTS_SETTING_CODE = 'intercity_documents';
|
||||
|
||||
/** Trade direction → clearance operation. DOMESTIC has no customs clearance. */
|
||||
function operationFor(tradeDirection: string): Op | null {
|
||||
if (tradeDirection === 'IMPORT') return 'import';
|
||||
if (tradeDirection === 'EXPORT') return 'export';
|
||||
return null; // DOMESTIC / intercity — no clearance gate
|
||||
return null; // DOMESTIC / intercity — no customs operation
|
||||
}
|
||||
|
||||
function freightFor(freightType: string): Freight {
|
||||
@@ -26,6 +34,9 @@ export function clearanceSettingCode(
|
||||
freightType: string,
|
||||
includesCustoms: boolean,
|
||||
): string | null {
|
||||
// Intercity: no customs, but the admin-configured intercity document set is
|
||||
// still collected and ops-reviewed before the shipment may board a train.
|
||||
if (tradeDirection === 'DOMESTIC') return INTERCITY_DOCUMENTS_SETTING_CODE;
|
||||
const op = operationFor(tradeDirection);
|
||||
if (!op) return null;
|
||||
const freight = freightFor(freightType);
|
||||
@@ -66,6 +77,16 @@ export function clearanceCodesForBooking(booking: Booking): {
|
||||
const includesCustoms =
|
||||
Boolean(booking.serviceType?.includesCustoms) ||
|
||||
Boolean(booking.customsClearingEnabled);
|
||||
// Intercity drawdowns under a ONE_TIME contract already cleared the intercity
|
||||
// document set on the CONTRACT (post-signature); only GENERAL drawdowns and
|
||||
// direct (contract-less) bookings carry the per-booking set.
|
||||
if (
|
||||
booking.tradeDirection === 'DOMESTIC' &&
|
||||
booking.contractId &&
|
||||
booking.contractKind === 'ONE_TIME'
|
||||
) {
|
||||
return { inputCode: null, outputCode: null, includesCustoms: false };
|
||||
}
|
||||
return {
|
||||
inputCode: clearanceSettingCode(
|
||||
booking.tradeDirection,
|
||||
|
||||
Reference in New Issue
Block a user