mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
fix issue
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
import { BookingsRepository } from './bookings.repository';
|
||||
import {
|
||||
RebookCancelledWagonsDto,
|
||||
RebookContainerLineDto,
|
||||
RequestWagonCancellationDto,
|
||||
} from './dto/wagon-cancellation.dto';
|
||||
import { Booking } from './entities/booking.entity';
|
||||
@@ -45,8 +46,11 @@ import {
|
||||
BookingWagonCancellation,
|
||||
CancelledQuantities,
|
||||
CancelledUnitSnapshot,
|
||||
WAGON_CANCEL_FEE_INVOICE_TYPE,
|
||||
} from './entities/booking-wagon-cancellation.entity';
|
||||
|
||||
export { WAGON_CANCEL_FEE_INVOICE_TYPE };
|
||||
|
||||
/**
|
||||
* rates.rate_type of the cancellation fee — an existing rate-engine type
|
||||
* (trigger CANCELLATION, never auto-applied to booking pricing). Staff
|
||||
@@ -59,8 +63,6 @@ export const WAGON_CANCELLATION_FEE_RATE_TYPE = 'CANCELLATION_FEE';
|
||||
/** `booking_container.container_size` is stored as "20ft"/"40ft" — `Number()` on it is NaN. */
|
||||
const sizeFtOf = (size: string | number | null | undefined): number =>
|
||||
parseInt(String(size ?? ''), 10);
|
||||
/** invoices.type of the fee invoice — the settlement branch key in BookingInvoiceService. */
|
||||
export const WAGON_CANCEL_FEE_INVOICE_TYPE = 'WAGON_CANCEL_FEE';
|
||||
|
||||
const round2 = (n: number): number => Math.round(n * 100) / 100;
|
||||
const round3 = (n: number): number => Math.round(n * 1000) / 1000;
|
||||
@@ -767,7 +769,7 @@ export class BookingWagonCancellationService {
|
||||
);
|
||||
}
|
||||
|
||||
const createDto = this.buildRebookDto(row, dto.scheduledDate);
|
||||
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;
|
||||
const created = await this.contractBooking.createUnderContract(
|
||||
@@ -1460,11 +1462,25 @@ export class BookingWagonCancellationService {
|
||||
private buildRebookDto(
|
||||
row: BookingWagonCancellation,
|
||||
scheduledDate: string,
|
||||
overrides?: RebookContainerLineDto[],
|
||||
): CreateBookingUnderContractDto {
|
||||
const dto: CreateBookingUnderContractDto = { scheduledDate };
|
||||
const q = row.cancelledQuantities;
|
||||
|
||||
if (q.bySize && Object.keys(q.bySize).length) {
|
||||
// Unit overrides may rename containers, change seals and VGM — but the
|
||||
// cancelled sizes and quantities are the contract of the credit: a size
|
||||
// not on the credit, or a wrong unit count, is rejected.
|
||||
const overrideBySize = new Map(
|
||||
(overrides ?? []).map((o) => [o.containerSize, o.units]),
|
||||
);
|
||||
for (const size of overrideBySize.keys()) {
|
||||
if (!(size in q.bySize)) {
|
||||
throw new BadRequestException(
|
||||
`The credit has no ${size} containers — sizes and quantities must match the cancelled booking.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const units = q.units ?? [];
|
||||
dto.containers = Object.entries(q.bySize).map(([size, quantity]) => {
|
||||
const sized = units.filter((u) => u.containerSize === size);
|
||||
@@ -1473,13 +1489,23 @@ export class BookingWagonCancellationService {
|
||||
`Credit is missing unit snapshots for size ${size} (${sized.length}/${quantity}) — contact EDR support.`,
|
||||
);
|
||||
}
|
||||
const replacement = overrideBySize.get(size);
|
||||
if (replacement && replacement.length !== quantity) {
|
||||
throw new BadRequestException(
|
||||
`The credit covers exactly ${quantity} × ${size} — you entered ${replacement.length}. Quantities cannot change on a rebook.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
containerSize: size,
|
||||
quantity,
|
||||
units: sized.map((u) => ({
|
||||
containerNumber: u.containerNumber,
|
||||
sealNumber: u.sealNumber ?? undefined,
|
||||
vgmTons: u.vgmTons,
|
||||
// Hazardous/reefer flags always ride from the snapshot (the cargo is
|
||||
// the same cargo); number/seal/VGM come from the override when given.
|
||||
units: sized.map((u, i) => ({
|
||||
containerNumber: replacement?.[i]?.containerNumber ?? u.containerNumber,
|
||||
sealNumber: replacement
|
||||
? (replacement[i]?.sealNumber ?? undefined)
|
||||
: (u.sealNumber ?? undefined),
|
||||
vgmTons: replacement?.[i]?.vgmTons ?? u.vgmTons,
|
||||
isHazardous: u.isHazardous,
|
||||
isReefer: u.isReefer,
|
||||
})),
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
FreightType,
|
||||
} from './entities/booking.entity';
|
||||
import { Booking } from './entities/booking.entity';
|
||||
import { BookingWagonCancellation } from './entities/booking-wagon-cancellation.entity';
|
||||
import { BookingContainerAllocation } from './entities/booking-container-allocation.entity';
|
||||
import { FileRecord } from '../files/entities/file.entity';
|
||||
import { CustomerTruckAssignmentDto } from './dto/customer-truck-assignment.dto';
|
||||
@@ -2262,16 +2263,25 @@ export class BookingsService {
|
||||
return this.findById(booking.id);
|
||||
}
|
||||
|
||||
/** Upload documents for a DRAFT booking. */
|
||||
/**
|
||||
* Upload documents for a DRAFT booking — or for a booking created by
|
||||
* rebooking a wagon-cancellation credit, whose paperwork may have changed
|
||||
* with the new containers (old documents stay; new ones ride alongside).
|
||||
*/
|
||||
async uploadDocuments(
|
||||
id: string,
|
||||
files: Express.Multer.File[],
|
||||
): Promise<Booking> {
|
||||
const booking = await this.findById(id);
|
||||
if (booking.status !== 'DRAFT') {
|
||||
throw new BadRequestException(
|
||||
'Documents can only be uploaded for DRAFT bookings',
|
||||
);
|
||||
const rebooked = await this.dataSource
|
||||
.getRepository(BookingWagonCancellation)
|
||||
.findOne({ where: { rebookedBookingId: id } });
|
||||
if (!rebooked) {
|
||||
throw new BadRequestException(
|
||||
'Documents can only be uploaded for DRAFT bookings',
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.filesService.uploadMany(id, 'bookings', files);
|
||||
return this.findById(id);
|
||||
|
||||
@@ -67,10 +67,60 @@ export class RequestWagonCancellationDto {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class RebookUnitDto {
|
||||
@ApiProperty({ description: 'Container number for the rebooked unit' })
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
containerNumber!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Seal number' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
sealNumber?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'VGM (tons) of the unit' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
vgmTons?: number;
|
||||
}
|
||||
|
||||
export class RebookContainerLineDto {
|
||||
@ApiProperty({ description: 'Container size as stored on the credit, e.g. "20ft"' })
|
||||
@IsString()
|
||||
containerSize!: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'The rebooked units for this size — count MUST equal the cancelled quantity',
|
||||
type: [RebookUnitDto],
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RebookUnitDto)
|
||||
units!: RebookUnitDto[];
|
||||
}
|
||||
|
||||
export class RebookCancelledWagonsDto {
|
||||
@ApiProperty({ description: 'Shipment day the credit is rebooked onto (ISO date)' })
|
||||
@IsDateString()
|
||||
scheduledDate!: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Optional unit overrides: container number / seal / VGM may change, but ' +
|
||||
'sizes and quantities must match the cancelled booking exactly. Sizes ' +
|
||||
'omitted here keep their original units.',
|
||||
type: [RebookContainerLineDto],
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RebookContainerLineDto)
|
||||
containers?: RebookContainerLineDto[];
|
||||
}
|
||||
|
||||
export class FilterWagonCancellationsDto {
|
||||
|
||||
@@ -5,6 +5,9 @@ import { Invoice } from '../../billing/entities/invoice.entity';
|
||||
import { Rate } from '../../rule-engine/entities/rate.entity';
|
||||
import { Booking } from './booking.entity';
|
||||
|
||||
/** `invoices.type` of the wagon-cancellation fee invoice — the settlement branch key in BookingInvoiceService. */
|
||||
export const WAGON_CANCEL_FEE_INVOICE_TYPE = 'WAGON_CANCEL_FEE';
|
||||
|
||||
export const WAGON_CANCELLATION_STATUSES = [
|
||||
// Requested; fee invoice open; wagons still allocated to the customer.
|
||||
'FEE_PENDING',
|
||||
|
||||
Reference in New Issue
Block a user