mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +00:00
feat(train-scheduling): mid-route consist changes, audit history, safer workspace
- planned couples: loose wagons join the train at a route stop, added from the schedule yards tab; capacity credits them per corridor edge and coupling validates locomotive weight/length caps per leg - real-cut toggle: a cut wagon permanently leaves the train build at its cut yard (soft cut still sits out one trip only) - fix heaviest-leg display counting a shared slot's full cargo on every spanned edge (phantom pull-weight overload on S-2026-00045) - confirmation dialogs for workspace add/load/unload/remove actions - train-builder History and Detached-wagons tabs, backed by paginated endpoints; builder detaches now always write adjustment-log rows Migrations 3660 (planned_wagon_couples, planned_wagon_real_cuts) and 3670 (adjustment log train_schedule_id nullable) — both applied to the dev DB by hand; watch mode does not run migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,6 +140,14 @@ export class ContractBookingService {
|
||||
dto: CreateBookingUnderContractDto,
|
||||
user?: { id?: string } | null,
|
||||
actorPermissions?: unknown,
|
||||
opts?: {
|
||||
/**
|
||||
* Wagon-cancellation credit rebook only: the freight was paid while the
|
||||
* contract was live, so redeeming the credit is allowed even after the
|
||||
* contract's validity lapsed. Never set for a genuinely new booking.
|
||||
*/
|
||||
allowExpiredContract?: boolean;
|
||||
},
|
||||
): Promise<CreateBookingUnderContractResult> {
|
||||
const contract = await this.contractsRepository.findByIdWithRelations(contractId);
|
||||
if (!contract) throw new NotFoundException(`Contract ${contractId} not found`);
|
||||
@@ -180,8 +188,13 @@ export class ContractBookingService {
|
||||
actorPermissions != null &&
|
||||
hasFreightPermission(actorPermissions, FREIGHT_PERMS.contracts.createBooking);
|
||||
|
||||
await this.assertNotExpired(contract);
|
||||
const createdByRole = await this.assertGate(contract, isGlActor);
|
||||
if (!opts?.allowExpiredContract) await this.assertNotExpired(contract);
|
||||
const createdByRole = await this.assertGate(
|
||||
contract,
|
||||
isGlActor,
|
||||
false,
|
||||
opts?.allowExpiredContract,
|
||||
);
|
||||
|
||||
// ONE_TIME: a single shipment at a time. The slot frees only if the prior
|
||||
// booking reached a terminal state (e.g. payment expired without shipping),
|
||||
@@ -1203,6 +1216,7 @@ export class ContractBookingService {
|
||||
contract: Contract,
|
||||
isGlActor: boolean,
|
||||
isInitiate = false,
|
||||
allowExpired = 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.
|
||||
@@ -1225,7 +1239,10 @@ export class ContractBookingService {
|
||||
}
|
||||
// No contract clearance cycle exists on either kind now — clearance runs
|
||||
// on the booking, so an executed/active contract is the only gate here.
|
||||
if (!['FULLY_EXECUTED', 'CONTRACT_ACTIVE'].includes(contract.status)) {
|
||||
if (
|
||||
!['FULLY_EXECUTED', 'CONTRACT_ACTIVE'].includes(contract.status) &&
|
||||
!(allowExpired && contract.status === 'EXPIRED')
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Contract must be fully executed before booking a shipment.',
|
||||
);
|
||||
@@ -1234,7 +1251,10 @@ export class ContractBookingService {
|
||||
}
|
||||
|
||||
// Path A — customer (or staff) once the contract is executed.
|
||||
if (!['FULLY_EXECUTED', 'CONTRACT_ACTIVE'].includes(contract.status)) {
|
||||
if (
|
||||
!['FULLY_EXECUTED', 'CONTRACT_ACTIVE'].includes(contract.status) &&
|
||||
!(allowExpired && contract.status === 'EXPIRED')
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Contract must be fully executed before booking a shipment.',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user