Merge pull request #1399 from Tria-plc/freight_feature/usermanagement

feat(train-scheduling): mid-route consist changes, audit history, saf…
This commit is contained in:
marshal
2026-08-23 06:59:27 +03:00
committed by GitHub
34 changed files with 2532 additions and 202 deletions

View File

@@ -698,8 +698,8 @@ export class BookingWagonCancellationService {
booking,
whole ? 'Booking cancelled — credit available' : 'Wagon cancellation confirmed',
whole
? `All wagons of ${booking.reference} are cancelled. Your paid freight is kept as credit — rebook any day while your contract is valid.`
: `${row.wagonsCancelled} wagon(s) of ${booking.reference} are cancelled. Your paid freight is kept as credit — rebook any day while your contract is valid.`,
? `All wagons of ${booking.reference} are cancelled. Your paid freight is kept as credit — rebook it on any coming train day.`
: `${row.wagonsCancelled} wagon(s) of ${booking.reference} are cancelled. Your paid freight is kept as credit — rebook it on any coming train day.`,
);
}
this.logger.log(
@@ -759,16 +759,6 @@ export class BookingWagonCancellationService {
if (!source.contractId) {
throw new BadRequestException('The original booking has no contract to rebook under.');
}
// Friendly pre-check; createUnderContract re-asserts inside its own guards.
if (
source.contractValidUntil &&
new Date(source.contractValidUntil).getTime() < Date.now()
) {
throw new BadRequestException(
'Contract validity has expired — ask EDR staff to extend the contract before rebooking.',
);
}
const createDto = this.buildRebookDto(row, dto.scheduledDate, dto.containers);
// Same currency as the source booking — the credit is in it.
createDto.paymentCurrency = source.paymentCurrency ?? undefined;
@@ -779,6 +769,9 @@ export class BookingWagonCancellationService {
// System actor: carries the create-booking key so the GL gate passes on
// Path B (customs-clearance) contracts; harmless on Path A.
{ permissions: [{ key: FREIGHT_PERMS.contracts.createBooking }] },
// The freight was paid while the contract was live — the credit stays
// redeemable even after the contract's validity lapses.
{ allowExpiredContract: true },
);
const newBookingId = created.booking.id;

View File

@@ -657,16 +657,18 @@ export class BookingsController {
}
@Post('wagon-cancellations/:cancellationId/withdraw')
@ApiOperation({ summary: 'Withdraw a fee-pending wagon cancellation (owner, or staff with the void permission)' })
@ApiOperation({ summary: 'Withdraw a fee-pending wagon cancellation — STAFF ONLY (void permission). A customer cancellation is final; only an admin can revert it.' })
async withdrawWagonCancellation(
@Param('cancellationId', ParseUUIDPipe) cancellationId: string,
@CurrentUser() user: TCurrentUser,
) {
await this.assertWagonCancellationActor(
cancellationId,
user,
FREIGHT_PERMS.bookings.wagonCancellationVoid,
);
// Customer cancellations are irreversible from the portal — no owner
// fallback here. Only staff holding the void permission can revert one.
if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.wagonCancellationVoid)) {
throw new ForbiddenException(
'A cancellation request cannot be withdrawn from the portal — contact EDR staff.',
);
}
return this.wagonCancellationService.withdraw(cancellationId);
}