mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 18:55:42 +00:00
update the Consolidation and wagon filling on booking and Add GET /bookings/reference-data endpoint for booking form catalog (yards, containers, services, shipping lines, cargo types)
This commit is contained in:
@@ -42,7 +42,7 @@ export class BookingsController {
|
|||||||
summary: "Create a new freight booking",
|
summary: "Create a new freight booking",
|
||||||
description:
|
description:
|
||||||
"Accepts all booking fields as form fields + dynamic file keys (e.g. passport, license). " +
|
"Accepts all booking fields as form fields + dynamic file keys (e.g. passport, license). " +
|
||||||
"Auto-enables consolidation when containerType=20FT and odd quantity.",
|
"Auto-enables consolidation when container quantity does not fill a whole wagon; attempts partner match or PENDING_CONSOLIDATION.",
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
description:
|
description:
|
||||||
@@ -150,8 +150,8 @@ export class BookingsController {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Request freight consolidation",
|
summary: "Request freight consolidation",
|
||||||
description:
|
description:
|
||||||
"Searches for a compatible 20FT partner (same origin, destination, tradeDirection). " +
|
"Searches for a partner whose container quantity complements yours to fill whole wagon(s) " +
|
||||||
"If a partner is found, both bookings are paired. If not, the booking enters the consolidation queue.",
|
"(same route, same container type). Pairs on match or sets PENDING_CONSOLIDATION with a status message.",
|
||||||
})
|
})
|
||||||
requestConsolidation(@Param("id", ParseUUIDPipe) id: string) {
|
requestConsolidation(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.bookingsService.requestConsolidation(id);
|
return this.bookingsService.requestConsolidation(id);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MinioModule } from '../minio/minio.module';
|
|||||||
import { RuleEngineModule } from '../rule-engine/rule-engine.module';
|
import { RuleEngineModule } from '../rule-engine/rule-engine.module';
|
||||||
import { BookingsController } from './bookings.controller';
|
import { BookingsController } from './bookings.controller';
|
||||||
import { BookingsRepository } from './bookings.repository';
|
import { BookingsRepository } from './bookings.repository';
|
||||||
|
import { ConsolidationService } from './consolidation.service';
|
||||||
import { BookingsService } from './bookings.service';
|
import { BookingsService } from './bookings.service';
|
||||||
import { BookingApprovalStep } from './entities/booking-approval-step.entity';
|
import { BookingApprovalStep } from './entities/booking-approval-step.entity';
|
||||||
import { BookingCargoModifier } from './entities/booking-cargo-modifier.entity';
|
import { BookingCargoModifier } from './entities/booking-cargo-modifier.entity';
|
||||||
@@ -29,7 +30,7 @@ import { Booking } from './entities/booking.entity';
|
|||||||
RuleEngineModule,
|
RuleEngineModule,
|
||||||
],
|
],
|
||||||
controllers: [BookingsController],
|
controllers: [BookingsController],
|
||||||
providers: [BookingsService, BookingsRepository],
|
providers: [BookingsService, BookingsRepository, ConsolidationService],
|
||||||
exports: [BookingsService],
|
exports: [BookingsService],
|
||||||
})
|
})
|
||||||
export class BookingsModule {}
|
export class BookingsModule {}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BaseRepository } from '@edr/api-common';
|
import { BaseRepository } from '@edr/api-common';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DataSource, In, IsNull, Not, Repository } from 'typeorm';
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { ContainerType } from '../rule-engine/entities/container-type.entity';
|
import { ContainerType } from '../rule-engine/entities/container-type.entity';
|
||||||
import { BookingApprovalStep } from './entities/booking-approval-step.entity';
|
import { BookingApprovalStep } from './entities/booking-approval-step.entity';
|
||||||
@@ -128,20 +128,63 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
|||||||
return Number(result?.total ?? 0);
|
return Number(result?.total ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Find a compatible consolidation partner. */
|
/**
|
||||||
async findConsolidationPartner(booking: Booking): Promise<Booking | null> {
|
* Find another booking whose container quantity complements this one to fill whole wagon(s)
|
||||||
return this.repository.findOne({
|
* (same route, same container type, partial wagon on both sides).
|
||||||
where: {
|
*/
|
||||||
allowConsolidation: true,
|
async findComplementaryConsolidationPartner(
|
||||||
|
booking: Booking,
|
||||||
|
slot: {
|
||||||
|
containerTypeId: string;
|
||||||
|
quantity: number;
|
||||||
|
containersPerWagon: number;
|
||||||
|
},
|
||||||
|
): Promise<Booking | null> {
|
||||||
|
const { containerTypeId, quantity, containersPerWagon: perWagon } = slot;
|
||||||
|
|
||||||
|
return this.repository
|
||||||
|
.createQueryBuilder('b')
|
||||||
|
.innerJoinAndSelect('b.bookingContainers', 'bc')
|
||||||
|
.innerJoin('bc.containerType', 'ct')
|
||||||
|
.where('b.id != :bookingId', { bookingId: booking.id })
|
||||||
|
.andWhere('b.allowConsolidation = true')
|
||||||
|
.andWhere('b.consolidationPartnerId IS NULL')
|
||||||
|
.andWhere('b.status IN (:...statuses)', {
|
||||||
|
statuses: ['DRAFT', 'PENDING_CONSOLIDATION'],
|
||||||
|
})
|
||||||
|
.andWhere('b.originYardId = :originYardId', {
|
||||||
originYardId: booking.originYardId,
|
originYardId: booking.originYardId,
|
||||||
|
})
|
||||||
|
.andWhere('b.destinationYardId = :destinationYardId', {
|
||||||
destinationYardId: booking.destinationYardId,
|
destinationYardId: booking.destinationYardId,
|
||||||
|
})
|
||||||
|
.andWhere('b.tradeDirection = :tradeDirection', {
|
||||||
tradeDirection: booking.tradeDirection,
|
tradeDirection: booking.tradeDirection,
|
||||||
consolidationPartnerId: IsNull(),
|
})
|
||||||
status: In(['DRAFT', 'PENDING_CONSOLIDATION']),
|
.andWhere('bc.containerTypeId = :containerTypeId', { containerTypeId })
|
||||||
id: Not(booking.id),
|
.andWhere('(bc.quantity % :perWagon) > 0', { perWagon })
|
||||||
},
|
.andWhere('((:quantity + bc.quantity) % :perWagon) = 0', {
|
||||||
order: { createdAt: 'ASC' },
|
quantity,
|
||||||
});
|
perWagon,
|
||||||
|
})
|
||||||
|
.orderBy('b.createdAt', 'ASC')
|
||||||
|
.getOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try each partial-wagon line until a complementary partner booking is found. */
|
||||||
|
async findConsolidationPartner(
|
||||||
|
booking: Booking,
|
||||||
|
slots: Array<{
|
||||||
|
containerTypeId: string;
|
||||||
|
quantity: number;
|
||||||
|
containersPerWagon: number;
|
||||||
|
}>,
|
||||||
|
): Promise<Booking | null> {
|
||||||
|
for (const slot of slots) {
|
||||||
|
const partner = await this.findComplementaryConsolidationPartner(booking, slot);
|
||||||
|
if (partner) return partner;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pair two bookings for consolidation. */
|
/** Pair two bookings for consolidation. */
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
RuleEngineService,
|
RuleEngineService,
|
||||||
} from '../rule-engine/rule-engine.service';
|
} from '../rule-engine/rule-engine.service';
|
||||||
import { BookingsRepository } from './bookings.repository';
|
import { BookingsRepository } from './bookings.repository';
|
||||||
|
import { ConsolidationService } from './consolidation.service';
|
||||||
import { CreateBookingContainerDto, CreateBookingDto } from './dto/create-booking.dto';
|
import { CreateBookingContainerDto, CreateBookingDto } from './dto/create-booking.dto';
|
||||||
import { FilterBookingDto } from './dto/filter-booking.dto';
|
import { FilterBookingDto } from './dto/filter-booking.dto';
|
||||||
import { UpdateBookingDto } from './dto/update-booking.dto';
|
import { UpdateBookingDto } from './dto/update-booking.dto';
|
||||||
@@ -31,6 +32,7 @@ export class BookingsService {
|
|||||||
private readonly customersService: CustomersService,
|
private readonly customersService: CustomersService,
|
||||||
private readonly ruleEngineService: RuleEngineService,
|
private readonly ruleEngineService: RuleEngineService,
|
||||||
private readonly containerTypesService: ContainerTypesService,
|
private readonly containerTypesService: ContainerTypesService,
|
||||||
|
private readonly consolidationService: ConsolidationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Generate a unique booking reference number. */
|
/** Generate a unique booking reference number. */
|
||||||
@@ -79,19 +81,66 @@ export class BookingsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve auto-consolidation for odd-quantity 20ft containers. */
|
/**
|
||||||
|
* Enable consolidation when any container line leaves a wagon partially filled
|
||||||
|
* (e.g. 1×20ft on a 2-slot wagon, 1×10ft on a 4-slot wagon), unless opted out.
|
||||||
|
*/
|
||||||
private async resolveConsolidation(
|
private async resolveConsolidation(
|
||||||
containers: CreateBookingContainerDto[],
|
containers: CreateBookingContainerDto[],
|
||||||
explicit?: boolean,
|
explicit?: boolean,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (explicit === false) return false;
|
if (explicit === false) return false;
|
||||||
for (const c of containers) {
|
const needs = await this.consolidationService.needsConsolidation(
|
||||||
const ct = await this.containerTypesService.findById(c.containerTypeId);
|
containers.map((c) => ({
|
||||||
if (ct.sizeFt === 20 && c.quantity % 2 !== 0) return true;
|
containerTypeId: c.containerTypeId,
|
||||||
}
|
quantity: c.quantity,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
if (needs) return true;
|
||||||
return explicit ?? false;
|
return explicit ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Search for a complementary partner; pair or queue as PENDING_CONSOLIDATION. */
|
||||||
|
private async tryAutoConsolidate(booking: Booking): Promise<{
|
||||||
|
booking: Booking;
|
||||||
|
messages: string[];
|
||||||
|
}> {
|
||||||
|
const messages: string[] = [];
|
||||||
|
|
||||||
|
if (!booking.allowConsolidation || booking.consolidationPartnerId) {
|
||||||
|
return { booking, messages };
|
||||||
|
}
|
||||||
|
|
||||||
|
const slots = await this.consolidationService.slotsFromBooking(booking);
|
||||||
|
if (slots.length === 0) {
|
||||||
|
return { booking, messages };
|
||||||
|
}
|
||||||
|
|
||||||
|
const partner = await this.bookingsRepository.findConsolidationPartner(
|
||||||
|
booking,
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (partner) {
|
||||||
|
await this.bookingsRepository.pairConsolidation(booking.id, partner.id);
|
||||||
|
const paired = await this.findById(booking.id);
|
||||||
|
messages.push(
|
||||||
|
this.consolidationService.describePaired(partner.reference, slots),
|
||||||
|
);
|
||||||
|
return { booking: paired, messages };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.status === 'DRAFT') {
|
||||||
|
await this.bookingsRepository.update(booking.id, {
|
||||||
|
status: 'PENDING_CONSOLIDATION',
|
||||||
|
} as never);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = await this.findById(booking.id);
|
||||||
|
messages.push(this.consolidationService.describePending(pending, slots));
|
||||||
|
return { booking: pending, messages };
|
||||||
|
}
|
||||||
|
|
||||||
/** Create a new freight booking. */
|
/** Create a new freight booking. */
|
||||||
async create(
|
async create(
|
||||||
dto: CreateBookingDto,
|
dto: CreateBookingDto,
|
||||||
@@ -175,7 +224,14 @@ export class BookingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const full = await this.findById(booking.id);
|
let full = await this.findById(booking.id);
|
||||||
|
|
||||||
|
if (allowConsolidation) {
|
||||||
|
const consolidation = await this.tryAutoConsolidate(full);
|
||||||
|
full = consolidation.booking;
|
||||||
|
warnings.push(...consolidation.messages);
|
||||||
|
}
|
||||||
|
|
||||||
return { booking: full, warnings };
|
return { booking: full, warnings };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +302,14 @@ export class BookingsService {
|
|||||||
await this.filesService.uploadMany(id, 'bookings', files);
|
await this.filesService.uploadMany(id, 'bookings', files);
|
||||||
}
|
}
|
||||||
|
|
||||||
const booking = await this.findById(id);
|
let booking = await this.findById(id);
|
||||||
|
|
||||||
|
if (allowConsolidation && !booking.consolidationPartnerId) {
|
||||||
|
const consolidation = await this.tryAutoConsolidate(booking);
|
||||||
|
booking = consolidation.booking;
|
||||||
|
warnings.push(...consolidation.messages);
|
||||||
|
}
|
||||||
|
|
||||||
return { booking, warnings };
|
return { booking, warnings };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +591,7 @@ export class BookingsService {
|
|||||||
booking: Booking;
|
booking: Booking;
|
||||||
partner: Booking | null;
|
partner: Booking | null;
|
||||||
paired: boolean;
|
paired: boolean;
|
||||||
|
message: string;
|
||||||
}> {
|
}> {
|
||||||
const booking = await this.findById(id);
|
const booking = await this.findById(id);
|
||||||
|
|
||||||
@@ -535,10 +599,12 @@ export class BookingsService {
|
|||||||
throw new BadRequestException('Booking is not eligible for consolidation');
|
throw new BadRequestException('Booking is not eligible for consolidation');
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasOdd20Ft = await this.hasOdd20FtContainer(booking);
|
const needs = await this.consolidationService.needsConsolidationFromBooking(
|
||||||
if (!hasOdd20Ft) {
|
booking,
|
||||||
|
);
|
||||||
|
if (!needs) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'Only bookings with odd-quantity 20ft containers need consolidation',
|
'Booking already fills whole wagon(s) for all container lines; consolidation is not required',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,32 +612,17 @@ export class BookingsService {
|
|||||||
throw new ConflictException('Booking is already paired for consolidation');
|
throw new ConflictException('Booking is already paired for consolidation');
|
||||||
}
|
}
|
||||||
|
|
||||||
const partner = await this.bookingsRepository.findConsolidationPartner(booking);
|
const result = await this.tryAutoConsolidate(booking);
|
||||||
|
const partner = result.booking.consolidationPartnerId
|
||||||
|
? await this.findById(result.booking.consolidationPartnerId)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (partner) {
|
return {
|
||||||
await this.bookingsRepository.pairConsolidation(booking.id, partner.id);
|
booking: result.booking,
|
||||||
return {
|
partner,
|
||||||
booking: await this.findById(id),
|
paired: partner !== null,
|
||||||
partner: await this.findById(partner.id),
|
message: result.messages[0] ?? '',
|
||||||
paired: true,
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.bookingsRepository.update(booking.id, {
|
|
||||||
status: 'PENDING_CONSOLIDATION',
|
|
||||||
} as never);
|
|
||||||
return { booking: await this.findById(id), partner: null, paired: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async hasOdd20FtContainer(booking: Booking): Promise<boolean> {
|
|
||||||
const containers = booking.bookingContainers ?? [];
|
|
||||||
for (const bc of containers) {
|
|
||||||
const ct =
|
|
||||||
bc.containerType ??
|
|
||||||
(await this.containerTypesService.findById(bc.containerTypeId));
|
|
||||||
if (ct.sizeFt === 20 && bc.quantity % 2 !== 0) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeConsolidation(id: string): Promise<{ booking: Booking; partner: Booking }> {
|
async removeConsolidation(id: string): Promise<{ booking: Booking; partner: Booking }> {
|
||||||
@@ -593,11 +644,26 @@ export class BookingsService {
|
|||||||
booking: Booking;
|
booking: Booking;
|
||||||
partner: Booking | null;
|
partner: Booking | null;
|
||||||
splitBilling: { bookingShare: number; partnerShare: number } | null;
|
splitBilling: { bookingShare: number; partnerShare: number } | null;
|
||||||
|
wagonSlots: Awaited<ReturnType<ConsolidationService['slotsFromBooking']>>;
|
||||||
|
statusMessage: string;
|
||||||
}> {
|
}> {
|
||||||
const booking = await this.findById(id);
|
const booking = await this.findById(id);
|
||||||
|
const wagonSlots = await this.consolidationService.slotsFromBooking(booking);
|
||||||
|
|
||||||
if (!booking.consolidationPartnerId) {
|
if (!booking.consolidationPartnerId) {
|
||||||
return { booking, partner: null, splitBilling: null };
|
const statusMessage =
|
||||||
|
booking.status === 'PENDING_CONSOLIDATION'
|
||||||
|
? this.consolidationService.describePending(booking, wagonSlots)
|
||||||
|
: wagonSlots.length > 0
|
||||||
|
? 'Consolidation may be required; no partner paired yet.'
|
||||||
|
: 'No wagon consolidation needed.';
|
||||||
|
return {
|
||||||
|
booking,
|
||||||
|
partner: null,
|
||||||
|
splitBilling: null,
|
||||||
|
wagonSlots,
|
||||||
|
statusMessage,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const partner = await this.findById(booking.consolidationPartnerId);
|
const partner = await this.findById(booking.consolidationPartnerId);
|
||||||
@@ -608,6 +674,11 @@ export class BookingsService {
|
|||||||
bookingShare: Number(booking.totalAmount),
|
bookingShare: Number(booking.totalAmount),
|
||||||
partnerShare: Number(partner.totalAmount),
|
partnerShare: Number(partner.totalAmount),
|
||||||
},
|
},
|
||||||
|
wagonSlots,
|
||||||
|
statusMessage: this.consolidationService.describePaired(
|
||||||
|
partner.reference,
|
||||||
|
wagonSlots,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ContainerTypesService } from '../rule-engine/services/container-types.service';
|
||||||
|
import { Booking } from './entities/booking.entity';
|
||||||
|
|
||||||
|
export interface ConsolidationSlot {
|
||||||
|
containerTypeId: string;
|
||||||
|
containerTypeCode: string;
|
||||||
|
quantity: number;
|
||||||
|
containersPerWagon: number;
|
||||||
|
remainder: number;
|
||||||
|
slotsNeeded: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsolidationAttemptResult {
|
||||||
|
booking: Booking;
|
||||||
|
partner: Booking | null;
|
||||||
|
paired: boolean;
|
||||||
|
messages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Containers that fit on one wagon for a given container type (inverse of wagons_per_unit). */
|
||||||
|
export function containersPerWagon(wagonsPerUnit: number): number {
|
||||||
|
const wpu = Number(wagonsPerUnit);
|
||||||
|
if (!wpu || wpu <= 0) return 1;
|
||||||
|
return Math.max(1, Math.round(1 / wpu));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wagonRemainder(quantity: number, perWagon: number): number {
|
||||||
|
const r = quantity % perWagon;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slotsNeededToFillWagon(quantity: number, perWagon: number): number {
|
||||||
|
const remainder = wagonRemainder(quantity, perWagon);
|
||||||
|
if (remainder === 0) return 0;
|
||||||
|
return perWagon - remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Two bookings' quantities for the same type complete whole wagon(s). */
|
||||||
|
export function quantitiesComplementWagon(
|
||||||
|
q1: number,
|
||||||
|
q2: number,
|
||||||
|
perWagon: number,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
wagonRemainder(q1, perWagon) > 0 &&
|
||||||
|
wagonRemainder(q2, perWagon) > 0 &&
|
||||||
|
(q1 + q2) % perWagon === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConsolidationService {
|
||||||
|
constructor(private readonly containerTypesService: ContainerTypesService) {}
|
||||||
|
|
||||||
|
async slotsFromContainerLines(
|
||||||
|
lines: Array<{ containerTypeId: string; quantity: number }>,
|
||||||
|
): Promise<ConsolidationSlot[]> {
|
||||||
|
const slots: ConsolidationSlot[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const ct = await this.containerTypesService.findById(line.containerTypeId);
|
||||||
|
const perWagon = containersPerWagon(Number(ct.wagonsPerUnit));
|
||||||
|
const remainder = wagonRemainder(line.quantity, perWagon);
|
||||||
|
if (remainder === 0) continue;
|
||||||
|
slots.push({
|
||||||
|
containerTypeId: line.containerTypeId,
|
||||||
|
containerTypeCode: ct.code,
|
||||||
|
quantity: line.quantity,
|
||||||
|
containersPerWagon: perWagon,
|
||||||
|
remainder,
|
||||||
|
slotsNeeded: perWagon - remainder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
async slotsFromBooking(booking: Booking): Promise<ConsolidationSlot[]> {
|
||||||
|
const lines =
|
||||||
|
booking.bookingContainers?.map((bc) => ({
|
||||||
|
containerTypeId: bc.containerTypeId,
|
||||||
|
quantity: bc.quantity,
|
||||||
|
})) ?? [];
|
||||||
|
return this.slotsFromContainerLines(lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
async needsConsolidation(
|
||||||
|
lines: Array<{ containerTypeId: string; quantity: number }>,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const slots = await this.slotsFromContainerLines(lines);
|
||||||
|
return slots.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async needsConsolidationFromBooking(booking: Booking): Promise<boolean> {
|
||||||
|
const slots = await this.slotsFromBooking(booking);
|
||||||
|
return slots.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
describePending(_booking: Booking, slots: ConsolidationSlot[]): string {
|
||||||
|
if (slots.length === 0) {
|
||||||
|
return 'Booking does not require wagon consolidation.';
|
||||||
|
}
|
||||||
|
const parts = slots.map(
|
||||||
|
(s) =>
|
||||||
|
`${s.slotsNeeded} more × ${s.containerTypeCode} (${s.containersPerWagon} per wagon; you have ${s.quantity})`,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
`No compatible partner found yet. Your booking is queued (PENDING_CONSOLIDATION). ` +
|
||||||
|
`Waiting for: ${parts.join('; ')}. You will be notified when another customer fills the wagon.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describePaired(partnerReference: string, slots: ConsolidationSlot[]): string {
|
||||||
|
const parts = slots.map(
|
||||||
|
(s) =>
|
||||||
|
`${s.containerTypeCode}: ${s.quantity} + partner fills wagon (${s.containersPerWagon} per wagon)`,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
`Consolidation partner found (${partnerReference}). ` +
|
||||||
|
`Shared wagon confirmed: ${parts.join('; ')}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user