@@ -24,9 +24,11 @@ import { BookingInvoiceService } from '../bookings/booking-invoice.service';
import { validate20ftWeightPairing } from '../bookings/container-pairing.util' ;
import { TrainSchedulingGlobalRules } from '../train-scheduling/entities/train-scheduling-global-rules.entity' ;
import { TrainSchedulingService } from '../train-scheduling/train-scheduling.service' ;
import { BookingBatchService } from '../train-scheduling/booking-batch.service' ;
import { ContainerTypesService } from '../rule-engine/services/container-types.service' ;
import { RuleEngineService } from '../rule-engine/rule-engine.service' ;
import { ContainerType } from '../rule-engine/entities/container-type.entity' ;
import { CargoType } from '../rule-engine/entities/cargo-type.entity' ;
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry' ;
import { hasFreightPermission } from '../../common/freight-permission.util' ;
@@ -40,6 +42,11 @@ import { CreateBookingUnderContractDto } from './dto/create-booking-under-contra
/** Statuses that still occupy the single active-booking slot of a ONE_TIME contract. */
const TERMINAL_BOOKING_STATUSES = [ 'EXPIRED' , 'CANCELLED' , 'COMPLETED' , 'REJECTED' ] ;
/** Bookings that never shipped release their quantity hold on the contract. */
const RELEASING_BOOKING_STATUSES = [ 'CANCELLED' , 'REJECTED' , 'EXPIRED' ] ;
const round3 = ( n : number ) : number = > Math . round ( n * 1000 ) / 1000 ;
export interface CreateBookingUnderContractResult {
booking : Booking ;
warnings : string [ ] ;
@@ -74,6 +81,8 @@ export class ContractBookingService {
private readonly dataSource : DataSource ,
@Inject ( forwardRef ( ( ) = > TrainSchedulingService ) )
private readonly trainSchedulingService : TrainSchedulingService ,
@Inject ( forwardRef ( ( ) = > BookingBatchService ) )
private readonly bookingBatchService : BookingBatchService ,
@Inject ( forwardRef ( ( ) = > BookingTransitionService ) )
private readonly bookingTransitionService : BookingTransitionService ,
) { }
@@ -90,10 +99,22 @@ export class ContractBookingService {
// A contract whose quantity cap was fully booked is completed — no further
// bookings, even while contract validity and a booking window are still
// open. Capacity released after closure (a cancelled/expired booking)
// reopens the contract on the next booking attempt.
// reopens the contract on the next booking attempt. A ONE_TIME contract
// only closes via a finished split chain, so its room is the outstanding
// split remainder rather than a cap line.
if ( contract . status === 'CONTRACT_CLOSED' ) {
const capacity = await this . computeCapacity ( contract ) ;
const hasRoom = capacity . some ( ( c ) = > c . remain ing == null || c . remaining > 0 ) ;
let hasRoom : boolean ;
if ( contract . contractK ind ! == 'GENERAL' ) {
const outstanding = await this . splitOutstanding ( contract ) ;
hasRoom = outstanding
? contract . freightType === 'CONTAINER'
? [ . . . outstanding . bySize . values ( ) ] . some ( ( s ) = > s . outstanding > 0 )
: ( outstanding . bulk ? . outstanding ? ? 0 ) > 0.001
: false ;
} else {
const capacity = await this . computeCapacity ( contract ) ;
hasRoom = capacity . some ( ( c ) = > c . remaining == null || c . remaining > 0 ) ;
}
if ( ! hasRoom ) {
throw new BadRequestException (
'This contract is completed — the full contracted quantity has been booked.' ,
@@ -121,12 +142,21 @@ export class ContractBookingService {
// 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),
// letting the customer re-book within contract validity (doc §10.4).
// EXCEPTION — split chain: a paid partial split (booking.isSplit) releases
// the slot for the leftover, but the next booking must take the WHOLE
// remainder; the customer cannot start any other booking on the contract.
// If the remainder splits again the same rule repeats until the cap is
// exhausted and the contract completes.
if ( contract . contractKind === 'ONE_TIME' ) {
const active = await this . countActive Bookings ( contractId ) ;
if ( active > 0 ) {
throw new BadRequestException (
'This one-time contract already has an a ctive b ooking.' ,
) ;
if ( await this . hasSplit Booking( contractId ) ) {
await this . assertExactRemainder ( contract , dto ) ;
} else {
const active = await this . countA ctiveB ookings ( contractId ) ;
if ( active > 0 ) {
throw new BadRequestException (
'This one-time contract already has an active booking.' ,
) ;
}
}
} else {
// GENERAL: draw down against the cargo quantity cap until it is full.
@@ -181,6 +211,10 @@ export class ContractBookingService {
scheduledDate : dto.scheduledDate ? ? null ,
direction : contract.tradeDirection ? ? null ,
} ) ;
// EXPORT rides whole or not at all (no split concept): reject the booking
// up front when no single open train on the day can carry it, telling the
// customer how much space is still bookable.
await this . assertExportTrainSpace ( contract , route , dto ) ;
}
// Hard capacity gate: a container line whose total weight exceeds the
@@ -573,6 +607,31 @@ export class ContractBookingService {
Number ( booking . cargoTotalWeightVgm ) > 0 ;
const warnings : string [ ] = [ ] ;
// EXPORT rides whole or not at all (no split concept): the chosen day must
// have a single open train that carries the whole booking. First completion
// sizes from the dto's cargo; a changes-requested resubmit (cargo already
// persisted, only the day re-picked) sizes from the booking itself.
if ( contract . tradeDirection === 'EXPORT' ) {
if ( hasCargo ) {
const probe = Object . assign (
Object . create ( Object . getPrototypeOf ( booking ) ) ,
booking ,
{ scheduledDate : new Date ( dto . scheduledDate ) } ,
) as Booking ;
const report = await this . bookingBatchService . exportSpaceReport ( probe ) ;
if ( ! report . scheduleId ) {
throw new BadRequestException (
report . fullMessage ? ? 'Not enough train space for this day.' ,
) ;
}
} else {
await this . assertExportTrainSpace ( contract , null , dto , {
originYardId : booking.originYardId ? ? null ,
destinationYardId : booking.destinationYardId ? ? null ,
} ) ;
}
}
// First completion persists cargo and draws contract capacity; a resubmit
// after OPERATION_CHANGES_REQUESTED already has its cargo and only re-picks
// the shipment day.
@@ -869,6 +928,196 @@ export class ContractBookingService {
. getCount ( ) ;
}
/**
* Whether the contract is in split-remainder mode: some booking on it was
* reduced by a paid partial batch offer and still holds capacity. A split
* booking that never shipped (CANCELLED / REJECTED / EXPIRED) releases its
* hold and the contract falls back to the plain single-slot rule — the
* customer can rebook the whole quantity again.
*/
private async hasSplitBooking ( contractId : string ) : Promise < boolean > {
const count = await this . dataSource
. getRepository ( Booking )
. createQueryBuilder ( 'b' )
. where ( 'b.contract_id = :contractId' , { contractId } )
. andWhere ( 'b.is_split = true' )
. andWhere ( 'b.status NOT IN (:...releasing)' , { releasing : RELEASING_BOOKING_STATUSES } )
. getCount ( ) ;
return count > 0 ;
}
/**
* Outstanding split remainder of a ONE_TIME contract: what the FIRST split
* booking carried before its reduction (its pre_split_quantities snapshot —
* one-time contracts have no quantity cap to derive this from) minus
* everything currently booked on the contract. Bookings that never shipped
* (CANCELLED / REJECTED / EXPIRED) release their share. Null when the
* contract has no live split booking.
*/
private async splitOutstanding (
contract : Contract ,
) : Promise < { bySize : Map < string , { total : number ; outstanding : number } > ; bulk : { total : number ; outstanding : number } | null } | null > {
const first = await this . dataSource
. getRepository ( Booking )
. createQueryBuilder ( 'b' )
. where ( 'b.contract_id = :contractId' , { contractId : contract.id } )
. andWhere ( 'b.is_split = true' )
. andWhere ( 'b.status NOT IN (:...releasing)' , { releasing : RELEASING_BOOKING_STATUSES } )
. orderBy ( 'b.created_at' , 'ASC' )
. getOne ( ) ;
if ( ! first ? . preSplitQuantities ) return null ;
const booked = await this . bookedQuantities ( contract ) ;
if ( contract . freightType === 'CONTAINER' ) {
const bySize = new Map < string , { total : number ; outstanding : number } > ( ) ;
for ( const [ size , total ] of Object . entries ( first . preSplitQuantities . bySize ? ? { } ) ) {
bySize . set ( size , {
total : Number ( total ) ,
outstanding : Math.max ( 0 , Number ( total ) - ( booked . bySize . get ( size ) ? ? 0 ) ) ,
} ) ;
}
return { bySize , bulk : null } ;
}
const total = Number ( first . preSplitQuantities . bulkTons ? ? 0 ) ;
return {
bySize : new Map ( ) ,
bulk : { total , outstanding : Math.max ( 0 , round3 ( total - booked . bulk ) ) } ,
} ;
}
/**
* EXPORT whole-booking single-train gate. Export bookings never split — the
* entire booking must ride ONE open train on the chosen day. When no train
* fits it whole (trying every fillable train on the corridor, earliest
* first), reject BEFORE anything is written, with the largest still-bookable
* space (tons for bulk via the cargo type's wagon type; wagons/containers
* for container freight) so the customer knows what he CAN book.
*/
private async assertExportTrainSpace (
contract : Contract ,
route : ContractRoute | null ,
dto : CreateBookingUnderContractDto ,
yards ? : { originYardId : string | null ; destinationYardId : string | null } ,
) : Promise < void > {
if ( contract . tradeDirection !== 'EXPORT' || ! dto . scheduledDate ) return ;
const probe = await this . buildExportProbe ( contract , route , dto , yards ) ;
const report = await this . bookingBatchService . exportSpaceReport ( probe ) ;
if ( report . scheduleId ) return ;
throw new BadRequestException (
report . fullMessage ? ? 'Not enough train space for this day.' ,
) ;
}
/**
* Unsaved booking twin carrying exactly what the batch engine's capacity
* math reads: yards + day for the leg, container lines WITH their container
* type (wagon-type FK) for TEU/wagon sizing, or bulk tons + cargo type
* (wagon-type FK) for tons→wagons conversion.
*/
private async buildExportProbe (
contract : Contract ,
route : ContractRoute | null ,
dto : CreateBookingUnderContractDto ,
yards ? : { originYardId : string | null ; destinationYardId : string | null } ,
) : Promise < Booking > {
const probe = new Booking ( ) ;
probe . freightType = contract . freightType ;
probe . tradeDirection = contract . tradeDirection ;
probe . scheduledDate = dto . scheduledDate ? new Date ( dto . scheduledDate ) : null ;
// Entity types are non-nullable; a missing yard just makes legOf() match no
// train, which surfaces as "no export train for this day" — the right failure.
probe . originYardId = ( yards ? . originYardId ? ? route ? . originYardId ) as string ;
probe . destinationYardId = ( yards ? . destinationYardId ? ?
route ? . destinationYardId ) as string ;
if ( contract . freightType === 'CONTAINER' ) {
const lines = await Promise . all (
( dto . containers ? ? [ ] ) . map ( async ( line ) = > {
const ct = await this . resolveContainerTypeForSize (
line . containerSize ,
contract . isReefer || ( line . reeferQuantity ? ? 0 ) > 0 ,
) ;
const bc = new BookingContainer ( ) ;
bc . containerSize = line . containerSize ;
bc . quantity = line . quantity ;
bc . containerTypeId = ct . id ;
bc . containerType = ct ;
bc . wagonsRequired = Math . ceil ( line . quantity * Number ( ct . wagonsPerUnit ? ? 1 ) ) ;
bc . totalVgmTons = ( line . units ? ? [ ] ) . reduce (
( sum , u ) = > sum + Number ( u . vgmTons ? ? 0 ) ,
0 ,
) ;
return bc ;
} ) ,
) ;
probe . bookingContainers = lines ;
probe . cargoTotalWeightVgm = lines . reduce (
( sum , l ) = > sum + Number ( l . totalVgmTons ? ? 0 ) ,
0 ,
) ;
return probe ;
}
probe . cargoTotalWeightVgm = this . resolveBulkTons ( dto ) ;
const cargoTypeId = this . resolveCargoTypeId ( contract , dto ) ;
probe . cargoTypeId = cargoTypeId ;
if ( cargoTypeId ) {
probe . cargoType =
( await this . dataSource
. getRepository ( CargoType )
. findOne ( { where : { id : cargoTypeId } } ) ) ? ? undefined ;
}
return probe ;
}
/**
* ONE_TIME split chain: the next booking must take the WHOLE outstanding
* remainder — a one-time contract is a single shipment, so the only way it
* fragments is the system splitting it on train capacity, never the customer
* choosing a partial amount.
*/
private async assertExactRemainder (
contract : Contract ,
dto : CreateBookingUnderContractDto ,
) : Promise < void > {
const outstanding = await this . splitOutstanding ( contract ) ;
if ( ! outstanding ) return ; // no live split booking — nothing to pin the remainder to
if ( contract . freightType === 'CONTAINER' ) {
const sizes = new Set < string > ( [
. . . outstanding . bySize . keys ( ) ,
. . . ( dto . containers ? ? [ ] ) . map ( ( l ) = > l . containerSize ? ? '' ) ,
] ) ;
for ( const size of sizes ) {
const remaining = outstanding . bySize . get ( size ) ? . outstanding ? ? 0 ;
const requested = ( dto . containers ? ? [ ] )
. filter ( ( l ) = > ( l . containerSize ? ? '' ) === size )
. reduce ( ( sum , l ) = > sum + Number ( l . quantity ? ? 0 ) , 0 ) ;
if ( requested !== remaining ) {
throw new BadRequestException (
` This one-time contract was split — the next booking must take the whole remainder: ` +
` ${ remaining } × ${ size || 'container' } container(s), got ${ requested } . ` ,
) ;
}
}
return ;
}
const requested =
( dto . bulkLines ? ? [ ] ) . reduce (
( sum , b ) = > sum + ( b . cargoWeightTons ? ? b . itemCount ? ? 0 ) ,
0 ,
) || this . resolveBulkTons ( dto ) || 0 ;
const remaining = outstanding . bulk ? . outstanding ? ? 0 ;
// 0.001 t tolerance absorbs the 3-decimal rounding applied to split weights.
if ( Math . abs ( requested - remaining ) > 0.001 ) {
throw new BadRequestException (
` This one-time contract was split — the next booking must take the whole ` +
` remaining ${ remaining } tons, got ${ requested } . ` ,
) ;
}
}
// ── GENERAL contract quantity cap (draw-down) ──────────────────────────────
/**
@@ -980,34 +1229,96 @@ export class ContractBookingService {
} ) ;
}
/**
* Capacity as shown to bookers (the /:id/capacity endpoint): GENERAL cap
* lines as-is, or — for a ONE_TIME contract in split-remainder mode —
* synthesized lines whose cap is the first split booking's pre-split
* snapshot and whose remaining is the outstanding remainder, i.e. the exact
* quantity the next booking must take.
*/
async capacityView (
contract : Contract ,
) : Promise <
Array < {
containerSize? : string | null ;
cargoTypeId? : string | null ;
cap : number | null ;
booked : number ;
remaining : number | null ;
} >
> {
const capacity = await this . computeCapacity ( contract ) ;
if ( capacity . length > 0 || contract . contractKind === 'GENERAL' ) {
return capacity ;
}
const outstanding = await this . splitOutstanding ( contract ) ;
if ( ! outstanding ) return capacity ;
if ( contract . freightType === 'CONTAINER' ) {
return [ . . . outstanding . bySize . entries ( ) ] . map ( ( [ size , s ] ) = > ( {
containerSize : size ,
cargoTypeId : null ,
cap : s.total ,
booked : s.total - s . outstanding ,
remaining : s.outstanding ,
} ) ) ;
}
const bulk = outstanding . bulk ;
if ( ! bulk ) return [ ] ;
return [
{
containerSize : null ,
cargoTypeId : null ,
cap : bulk.total ,
booked : round3 ( bulk . total - bulk . outstanding ) ,
remaining : bulk.outstanding ,
} ,
] ;
}
/**
* Complete the contract once its quantity cap is fully consumed. Runs after
* every booking created under a GENERAL contract (including a split remainder
* being rebooked): when no capped scope line has capacity left, the contract
* moves to CONTRACT_CLOSED even though its validity window is still open —
* blocking further bookings and shipment requests, including inside an open
* booking window. Never throws: a status hiccup must not undo the booking
* that was just created.
* every booking created under a GENERAL contract, and under a ONE_TIME
* contract in split-remainder mode (a split remainder being rebooked): when
* no capped scope line has capacity left, the contract moves to
* CONTRACT_CLOSED even though its validity window is still open — blocking
* further bookings and shipment requests, including inside an open booking
* window. Never throws: a status hiccup must not undo the booking that was
* just created.
*/
private async maybeCompleteContract ( contract : Contract ) : Promise < void > {
try {
// ONE_TIME contracts are governed by the single-active-booking slot (and
// are promoted to GENERAL on split), so only GENERAL completes by cap.
if ( contract . contractKind !== 'GENERAL' ) return ;
if ( ! [ 'CONTRACT_ACTIVE' , 'FULLY_EXECUTED' ] . includes ( contract . status ) ) return ;
const capacity = await this . computeCapacity ( contract ) ;
if ( capacity . length === 0 ) return ; // uncapped — completes only by expiry
// 0.001 tolerance absorbs bulk-ton float rounding (split weights round to
// 3 decimals); cont ainer caps are integers and unaffected.
const exhausted = capacity . every (
( c ) = > c . remaining != null && c . remaining <= 0.001 ,
) ;
if ( ! exhausted ) return ;
// ONE_TIME contracts are governed by the single-active-booking slot, so
// they normally complete by expiry — EXCEPT once a booking was split: the
// rem aind er chain draws down the split booking's pre-split snapshot, and
// the contract completes when the outstanding remainder hits zero.
// (An unsplit ONE_TIME never completes here, so re-booking after an
// expired unpaid booking keeps working.)
if ( contract . contractKind !== 'GENERAL' ) {
const outstanding = await this . splitOutstanding ( contract ) ;
if ( ! outstanding ) return ;
// 0.001 tolerance absorbs bulk-ton float rounding (split weights round
// to 3 decimals); container quantities are integers and unaffected.
const exhausted =
contract . freightType === 'CONTAINER'
? [ . . . outstanding . bySize . values ( ) ] . every ( ( s ) = > s . outstanding <= 0 )
: ( outstanding . bulk ? . outstanding ? ? 0 ) <= 0.001 ;
if ( ! exhausted ) return ;
} else {
const capacity = await this . computeCapacity ( contract ) ;
if ( capacity . length === 0 ) return ; // uncapped — completes only by expiry
const exhausted = capacity . every (
( c ) = > c . remaining != null && c . remaining <= 0.001 ,
) ;
if ( ! exhausted ) return ;
}
await this . contractsRepository . update ( contract . id , {
status : 'CONTRACT_CLOSED' ,
} as never ) ;
this . logger . log (
` Contract ${ contract . reference } quantity cap fully booked — completed; no further bookings within validity. ` ,
` Contract ${ contract . reference } quantity fully booked — completed; no further bookings within validity. ` ,
) ;
} catch ( err ) {
this . logger . error (
@@ -1025,7 +1336,7 @@ export class ContractBookingService {
private async bookedQuantities (
contract : Contract ,
) : Promise < { bySize : Map < string , number > ; bulk : number } > {
const releasing = [ 'CANCELLED' , 'REJECTED' , 'EXPIRED' ] ;
const releasing = RELEASING_BOOKING_STATUSES ;
if ( contract . freightType === 'CONTAINER' ) {
const rows = await this . dataSource
. getRepository ( BookingContainer )
@@ -1213,6 +1524,8 @@ export class ContractBookingService {
currency : string | null ;
pairingErrors : string [ ] ;
capacityErrors : string [ ] ;
containerClashErrors : string [ ] ;
spaceErrors : string [ ] ;
lineItems : PriceLineItemDto [ ] ;
totalAmount : number ;
} > {
@@ -1227,6 +1540,8 @@ export class ContractBookingService {
currency : null ,
pairingErrors : [ ] ,
capacityErrors : [ ] ,
containerClashErrors : [ ] ,
spaceErrors : [ ] ,
lineItems : [ ] ,
totalAmount : 0 ,
} ;
@@ -1312,12 +1627,50 @@ export class ContractBookingService {
contract . tradeDirection ,
) ;
// A physical container rides one train only — surface a clash with another
// active booking on the same day + route in the preview, so the form can
// hard-block before the create call rejects with the same rule.
let containerClashErrors : string [ ] = [ ] ;
if ( dto . scheduledDate ) {
const numbers = lines . flatMap ( ( line ) = >
( line . units ? ? [ ] )
. map ( ( u ) = > ( u . containerNumber ? ? '' ) . trim ( ) . toUpperCase ( ) )
. filter ( ( n ) = > n . length > 0 ) ,
) ;
const clashes = await this . findContainerClashesOnTrain (
[ . . . new Set ( numbers ) ] ,
dto . scheduledDate ,
{
originYardId : route?.originYardId ,
destinationYardId : route?.destinationYardId ,
} ,
) ;
containerClashErrors = clashes . map (
( c ) = >
` ${ c . containerNumber } is already booked on ${ c . reference } for this shipment day. ` ,
) ;
}
// EXPORT rides whole or not at all — surface the single-train space check
// in the preview so the form hard-blocks BEFORE the create call rejects
// with the same message (including how much space is still bookable).
let spaceErrors : string [ ] = [ ] ;
if ( contract . tradeDirection === 'EXPORT' && dto . scheduledDate ) {
const probe = await this . buildExportProbe ( contract , route , dto ) ;
const report = await this . bookingBatchService . exportSpaceReport ( probe ) ;
if ( ! report . scheduleId ) {
spaceErrors = [ report . fullMessage ? ? 'Not enough train space for this day.' ] ;
}
}
return {
overweightLines : computed.overweightLines ,
overweightSurchargeAmount ,
currency : computed.currency ,
pairingErrors ,
capacityErrors ,
containerClashErrors ,
spaceErrors ,
lineItems : computed.lineItems ,
totalAmount : computed.totalAmount ,
} ;
@@ -1445,6 +1798,37 @@ export class ContractBookingService {
route : { originYardId? : string | null ; destinationYardId? : string | null } ,
excludeBookingId? : string ,
) : Promise < void > {
const clashes = await this . findContainerClashesOnTrain (
numbers ,
scheduledDate ,
route ,
excludeBookingId ,
) ;
if ( clashes . length ) {
const detail = clashes
. map ( ( c ) = > ` ${ c . containerNumber } (booking ${ c . reference } ) ` )
. join ( ', ' ) ;
throw new ConflictException (
` Container(s) already booked on this route for ${ scheduledDate } : ${ detail } . ` +
'A container can only be on one booking per train — remove it or pick another shipment day.' ,
) ;
}
}
/**
* Container numbers among `numbers` that already sit on another active
* booking of the same train — same day and same route. One row per clashing
* number. Bookings without route yards (legacy rows) match on the day alone
* rather than let through.
*/
private async findContainerClashesOnTrain (
numbers : string [ ] ,
scheduledDate : string ,
route : { originYardId? : string | null ; destinationYardId? : string | null } ,
excludeBookingId? : string ,
) : Promise < Array < { containerNumber : string ; reference : string } > > {
if ( ! numbers . length ) return [ ] ;
const qb = this . dataSource
. getRepository ( BookingContainerUnit )
. createQueryBuilder ( 'unit' )
@@ -1474,18 +1858,7 @@ export class ContractBookingService {
}
const clashes : Array < { containerNumber : string ; reference : string } > =
await qb . getRawMany ( ) ;
if ( clashes . length ) {
const detail = [
. . . new Map ( clashes . map ( ( c ) = > [ c . containerNumber , c ] ) ) . values ( ) ,
]
. map ( ( c ) = > ` ${ c . containerNumber } (booking ${ c . reference } ) ` )
. join ( ', ' ) ;
throw new ConflictException (
` Container(s) already booked on this route for ${ scheduledDate } : ${ detail } . ` +
'A container can only be on one booking per train — remove it or pick another shipment day.' ,
) ;
}
return [ . . . new Map ( clashes . map ( ( c ) = > [ c . containerNumber , c ] ) ) . values ( ) ] ;
}
private async assert20ftPairableAtCreate (
@@ -1528,8 +1901,8 @@ export class ContractBookingService {
preferReefer : boolean ,
) : Promise < ContainerType > {
const sizeFt = parseInt ( size , 10 ) ;
const { data } = await this . containerTypesService . findAll ( { pageSize : 2 00 } ) ;
const types = data . filter ( ( t ) = > Number ( t . sizeFt ) === sizeFt && t . isActive !== false ) ;
const { items } = await this . containerTypesService . findAll ( { pageSize : 1 00 } ) ;
const types = items . filter ( ( t ) = > Number ( t . sizeFt ) === sizeFt && t . isActive !== false ) ;
if ( ! types . length ) {
throw new BadRequestException ( ` No container type configured for size ${ size } . ` ) ;
}