mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 21:50:57 +00:00
Implement clearance-first booking flow and completion process for customs contracts
This commit is contained in:
@@ -14,9 +14,10 @@ describe('BookingTransitionService — finalizeClearance gate', () => {
|
||||
serviceType: { includesCustoms: false }, // no output set → only the input gate
|
||||
};
|
||||
|
||||
// Input set has two required docs.
|
||||
// Input set has two required docs. Non-customs bookings resolve to the
|
||||
// ONE_TIME self-clearance document set.
|
||||
const inputSetting = {
|
||||
code: 'clearance_import_container_without_customs',
|
||||
code: 'contract_clearance_selfclear_import_container',
|
||||
fields: [
|
||||
{ fileKey: 'commercial_invoice', isRequired: true },
|
||||
{ fileKey: 'packing_list', isRequired: true },
|
||||
@@ -198,7 +199,7 @@ describe('BookingTransitionService — finalizeClearance customs output gate', (
|
||||
*/
|
||||
describe('BookingTransitionService — submitClearanceDocuments required-fields gate', () => {
|
||||
const inputSetting = {
|
||||
code: 'clearance_import_container_without_customs',
|
||||
code: 'contract_clearance_selfclear_import_container',
|
||||
fields: [
|
||||
{ fileKey: 'commercial_invoice', fileLabel: 'Commercial invoice', isRequired: true },
|
||||
{ fileKey: 'packing_list', fileLabel: 'Packing list', isRequired: true },
|
||||
|
||||
@@ -8,8 +8,10 @@ describe('clearance.util — clearanceSettingCode', () => {
|
||||
expect(clearanceSettingCode('IMPORT', 'CONTAINER', true)).toBe(
|
||||
'clearance_import_container_with_customs',
|
||||
);
|
||||
// Non-customs bookings self-clear with the same document set a ONE_TIME
|
||||
// self-clear contract uses.
|
||||
expect(clearanceSettingCode('IMPORT', 'CONTAINER', false)).toBe(
|
||||
'clearance_import_container_without_customs',
|
||||
'contract_clearance_selfclear_import_container',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -18,7 +20,7 @@ describe('clearance.util — clearanceSettingCode', () => {
|
||||
'clearance_export_bulk_with_customs',
|
||||
);
|
||||
expect(clearanceSettingCode('EXPORT', 'BULK', false)).toBe(
|
||||
'clearance_export_bulk_without_customs',
|
||||
'contract_clearance_selfclear_export_bulk',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,8 +29,14 @@ export function clearanceSettingCode(
|
||||
const op = operationFor(tradeDirection);
|
||||
if (!op) return null;
|
||||
const freight = freightFor(freightType);
|
||||
const customs = includesCustoms ? 'with_customs' : 'without_customs';
|
||||
return `clearance_${op}_${freight}_${customs}`;
|
||||
// Non-customs (Path A) bookings self-clear: the customer proves his own
|
||||
// clearance with the SAME smaller document set a ONE_TIME self-clear
|
||||
// contract uses (customs declaration, release permit, …) — not the
|
||||
// GL-oriented booking sets.
|
||||
if (!includesCustoms) {
|
||||
return `contract_clearance_selfclear_${op}_${freight}`;
|
||||
}
|
||||
return `clearance_${op}_${freight}_with_customs`;
|
||||
}
|
||||
|
||||
/** The GL-output (customs output) setting code, keyed on op + freight. */
|
||||
|
||||
@@ -700,6 +700,16 @@ describe('BookingBatchService — PAID reconcile', () => {
|
||||
bookingsRepository.findBatchPoolByCorridorDay
|
||||
.mockResolvedValueOnce([waiting])
|
||||
.mockResolvedValue([]);
|
||||
// expire()'s paid-guard and reserve()'s idempotency guard both re-read the
|
||||
// booking fresh — answer with the matching row, not the paidBooking default
|
||||
// (which would make the guard rescue-allocate the lapsed reservation).
|
||||
const byId: Record<string, Booking> = { lapsed, waiting };
|
||||
dataSource
|
||||
.getRepository()
|
||||
.findOne.mockImplementation(
|
||||
async (opts: { where?: { id?: string } }) =>
|
||||
byId[opts?.where?.id ?? ''] ?? null,
|
||||
);
|
||||
|
||||
await service.settleDueReservations(trainId);
|
||||
|
||||
@@ -725,6 +735,14 @@ describe('BookingBatchService — PAID reconcile', () => {
|
||||
return Promise.resolve(reads === 1 ? [lapsed] : []);
|
||||
});
|
||||
bookingsRepository.findBatchPoolByCorridorDay.mockResolvedValue([]);
|
||||
// expire()'s paid-guard re-reads the booking fresh — answer with the
|
||||
// (unpaid) lapsed row, not the paidBooking default.
|
||||
dataSource
|
||||
.getRepository()
|
||||
.findOne.mockImplementation(
|
||||
async (opts: { where?: { id?: string } }) =>
|
||||
opts?.where?.id === 'lapsed' ? lapsed : null,
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
service.settleDueReservations(trainId),
|
||||
@@ -733,6 +751,37 @@ describe('BookingBatchService — PAID reconcile', () => {
|
||||
|
||||
expect(notifier.expired).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('never expires a reservation whose payment landed — allocates it instead', async () => {
|
||||
const latePaid = booking('late-paid', 50, {
|
||||
status: 'SELECTED_FOR_BATCH',
|
||||
paymentDeadline: new Date(Date.now() - 60_000),
|
||||
});
|
||||
bookingsRepository.findReservedForSchedule
|
||||
.mockResolvedValueOnce([latePaid])
|
||||
.mockResolvedValue([]);
|
||||
bookingsRepository.findBatchPoolByCorridorDay.mockResolvedValue([]);
|
||||
// The payment webhook flipped paymentStatus between the settle's list
|
||||
// read and expire()'s fresh re-read — the deadline had already passed.
|
||||
dataSource
|
||||
.getRepository()
|
||||
.findOne.mockImplementation(
|
||||
async (opts: { where?: { id?: string } }) =>
|
||||
opts?.where?.id === 'late-paid'
|
||||
? { ...latePaid, paymentStatus: 'PAID' }
|
||||
: null,
|
||||
);
|
||||
|
||||
await service.settleDueReservations(trainId);
|
||||
|
||||
// Money was taken → the booking boards. Never expired.
|
||||
expect(notifier.expired).not.toHaveBeenCalled();
|
||||
expect(notifier.secured).toHaveBeenCalledTimes(1);
|
||||
expect(trainScheduleBookingsRepository.createMany).toHaveBeenCalledWith(
|
||||
[{ trainScheduleId: trainId, bookingId: 'late-paid' }],
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -428,7 +428,19 @@ export class BookingBatchService implements OnModuleInit {
|
||||
where: { id: bookingId },
|
||||
relations: { company: true },
|
||||
});
|
||||
if (!booking?.trainScheduleId) return;
|
||||
if (!booking) return;
|
||||
if (!booking.trainScheduleId) {
|
||||
// A paid booking with no train is money taken and nothing boarding —
|
||||
// scream so staff pin it to a schedule manually (batch board / assign).
|
||||
if (booking.paymentStatus === "PAID" || booking.status === "PAID") {
|
||||
this.logger.error(
|
||||
`PAID booking ${booking.reference ?? bookingId} has no train_schedule_id — ` +
|
||||
`its reservation was likely expired before the payment landed. ` +
|
||||
`Assign it to a schedule manually from the batch board.`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isBatchPaid =
|
||||
booking.status === "SELECTED_FOR_BATCH" ||
|
||||
@@ -2124,8 +2136,38 @@ export class BookingBatchService implements OnModuleInit {
|
||||
* Expire an unpaid reservation and free its capacity. With day-level pooling we
|
||||
* also clear `trainScheduleId` so the booking is no longer pinned to the train
|
||||
* it failed to pay for — it's back in the day pool for staff to act on.
|
||||
* `reason` picks the customer message: 'payment' (pay window lapsed) or
|
||||
* 'no-capacity' (no train on the chosen day could take the booking).
|
||||
*
|
||||
* PAID GUARD: a booking whose payment has landed is never expired — money was
|
||||
* taken, so it boards, even when the webhook arrived after the deadline or the
|
||||
* settle read a stale row. It allocates onto the train it was selected for; if
|
||||
* the wagon planner then finds no physical wagon, the booking stays linked and
|
||||
* staff assign wagons manually. Consolidated bookings are exempt from the
|
||||
* rescue: the shared wagon is both-or-neither, and settleReserved owns that
|
||||
* pair decision.
|
||||
*/
|
||||
private async expire(booking: Booking): Promise<void> {
|
||||
private async expire(
|
||||
booking: Booking,
|
||||
reason: "payment" | "no-capacity" = "payment",
|
||||
): Promise<void> {
|
||||
if (!booking.consolidationPartnerId) {
|
||||
const fresh = await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.findOne({ where: { id: booking.id }, relations: { company: true } });
|
||||
const paid =
|
||||
fresh != null &&
|
||||
(fresh.paymentStatus === "PAID" || fresh.status === "PAID");
|
||||
const paidScheduleId = fresh?.trainScheduleId ?? booking.trainScheduleId;
|
||||
if (paid && paidScheduleId) {
|
||||
this.logger.log(
|
||||
`[BATCH] expire skipped for ${booking.reference} — payment already ` +
|
||||
`landed; allocating on schedule ${paidScheduleId} instead`,
|
||||
);
|
||||
await this.allocate(paidScheduleId, fresh, "paid");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const freedScheduleId = booking.trainScheduleId;
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
trainScheduleId: null,
|
||||
@@ -2146,13 +2188,84 @@ export class BookingBatchService implements OnModuleInit {
|
||||
// (emits `booking.invoice.expired`). Domain owns the reaction; billing stays
|
||||
// source-agnostic.
|
||||
await this.billing.expirePayable(Freight.InvoiceSource.Booking, booking.id, "PREPAID");
|
||||
this.notifier.expired(booking);
|
||||
if (reason === "no-capacity") {
|
||||
this.notifier.expiredNoCapacity(booking);
|
||||
} else {
|
||||
this.notifier.expired(booking);
|
||||
}
|
||||
this.logger.log(
|
||||
`[BATCH] EXPIRED ${booking.reference} — payment window passed; freed its ` +
|
||||
`wagons back to the pool for top-up`,
|
||||
`[BATCH] EXPIRED ${booking.reference} — ` +
|
||||
(reason === "no-capacity"
|
||||
? "no train on its day had capacity left"
|
||||
: "payment window passed; freed its wagons back to the pool for top-up"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* End-of-day sweep: once a schedule's window cycle concludes and NO other
|
||||
* train on the same route-day can still run a cycle, the waiting pool for
|
||||
* that day is dead — a FULLY_EXECUTED booking left in it would wait forever.
|
||||
* Expire every leftover commercial booking and tell the customers to rebook
|
||||
* another day. Government bookings are never auto-expired (they preempt).
|
||||
* Returns how many bookings were expired.
|
||||
*/
|
||||
async expireLeftoverDayPool(scheduleId: string): Promise<number> {
|
||||
const schedule = await this.trainSchedulesRepository.findById(scheduleId);
|
||||
if (!schedule?.scheduledDepartureDate) return 0;
|
||||
const day = eatDay(schedule.scheduledDepartureDate);
|
||||
const group: RouteDayGroup = {
|
||||
originYardId: schedule.originStationId,
|
||||
destinationYardId: schedule.destinationStationId,
|
||||
day,
|
||||
};
|
||||
|
||||
// Another train on this route-day that can still take bookings keeps the
|
||||
// pool alive — when IT concludes, its own sweep runs this check again.
|
||||
const siblings = await this.trainSchedulesRepository.findAll({
|
||||
where: [
|
||||
{
|
||||
originStationId: group.originYardId,
|
||||
destinationStationId: group.destinationYardId,
|
||||
status: TrainScheduleStatusEnum.Draft,
|
||||
},
|
||||
{
|
||||
originStationId: group.originYardId,
|
||||
destinationStationId: group.destinationYardId,
|
||||
status: TrainScheduleStatusEnum.Scheduled,
|
||||
},
|
||||
],
|
||||
});
|
||||
const anotherTrainStillOpen = siblings.some(
|
||||
(s) =>
|
||||
s.id !== schedule.id &&
|
||||
s.scheduledDepartureDate != null &&
|
||||
eatDay(s.scheduledDepartureDate) === day &&
|
||||
s.windowPhase !== "DONE" &&
|
||||
s.bookingWindowStatus !== "FULL",
|
||||
);
|
||||
if (anotherTrainStillOpen) return 0;
|
||||
|
||||
const corridorYards = await this.corridorYardsForRouteDay(group);
|
||||
const pool = corridorYards.length
|
||||
? await this.bookingsRepository.findBatchPoolByCorridorDay(corridorYards, day)
|
||||
: await this.bookingsRepository.findBatchPoolByRouteDay(
|
||||
group.originYardId,
|
||||
group.destinationYardId,
|
||||
day,
|
||||
);
|
||||
const leftovers = pool.filter((b) => !b.isGovernment);
|
||||
for (const booking of leftovers) {
|
||||
await this.expire(booking, "no-capacity");
|
||||
}
|
||||
if (leftovers.length) {
|
||||
this.logger.log(
|
||||
`[BATCH] ${this.groupLabel(group)}: no train left with capacity — ` +
|
||||
`expired ${leftovers.length} waiting booking(s)`,
|
||||
);
|
||||
}
|
||||
return leftovers.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union of stop yards across the day's fillable schedules on this corridor —
|
||||
* the same pool scope fillRouteDay uses, so full-route AND sub-corridor bookings
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ClearanceMilestoneService } from '../contracts/clearance-milestone.serv
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { TrainScheduleBooking } from '../train-schedules/entities/train-schedule-booking.entity';
|
||||
import { WagonBookingAllocation } from '../train-schedules/entities/wagon-booking-allocation.entity';
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
import { WagonMovement } from '../wagons/entities/wagon-movement.entity';
|
||||
@@ -139,7 +140,7 @@ export class BookingJourneyService {
|
||||
.leftJoinAndSelect('booking.originYard', 'originYard')
|
||||
.leftJoinAndSelect('booking.destinationYard', 'destinationYard')
|
||||
.innerJoin(
|
||||
'freight.train_schedule_bookings',
|
||||
TrainScheduleBooking,
|
||||
'tsb',
|
||||
'tsb.booking_id = booking.id AND tsb.train_schedule_id = :scheduleId AND tsb.deleted_at IS NULL',
|
||||
{ scheduleId },
|
||||
@@ -218,8 +219,10 @@ export class BookingJourneyService {
|
||||
const bookings = await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.createQueryBuilder('booking')
|
||||
// Entity-class join: a raw 'freight.table' string is parsed by TypeORM as
|
||||
// an alias.property path ("freight" alias was not found) — runtime 500.
|
||||
.innerJoin(
|
||||
'freight.train_schedule_bookings',
|
||||
TrainScheduleBooking,
|
||||
'tsb',
|
||||
'tsb.booking_id = booking.id AND tsb.train_schedule_id = :scheduleId AND tsb.deleted_at IS NULL',
|
||||
{ scheduleId },
|
||||
@@ -350,7 +353,7 @@ export class BookingJourneyService {
|
||||
.createQueryBuilder('alloc')
|
||||
.innerJoinAndSelect('alloc.trainSetWagon', 'slot')
|
||||
.innerJoin(
|
||||
'freight.train_schedules',
|
||||
TrainSchedule,
|
||||
'schedule',
|
||||
'schedule.train_set_id = slot.train_set_id AND schedule.id = :scheduleId',
|
||||
{ scheduleId },
|
||||
|
||||
@@ -141,6 +141,22 @@ export class BookingNotifierService {
|
||||
this.inApp(b, 'Payment window expired', msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every train on the booking's chosen day filled up (or no further train runs)
|
||||
* before the waiting list reached this booking — it expired unplaced. HIGH so
|
||||
* the customer hears about it by email/SMS and rebooks another day.
|
||||
*/
|
||||
expiredNoCapacity(b: Booking): void {
|
||||
const msg =
|
||||
`Booking ${b.reference ?? b.id} could not be placed: every train for your selected day ` +
|
||||
`is full and no other train is scheduled that day. The booking has expired — ` +
|
||||
`please rebook for another day. No re-approval is needed.`;
|
||||
void this.notifyContact(b, msg, 'EXPIRED (NO CAPACITY)');
|
||||
this.inApp(b, 'No capacity — booking expired', msg, {
|
||||
priority: NotificationPriority.HIGH,
|
||||
});
|
||||
}
|
||||
|
||||
scheduleFull(b: Booking): void {
|
||||
this.logger.warn(
|
||||
`SCHEDULE FULL — ${this.ref(b)} could not be placed; change schedule, pick another day, or cancel.`,
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('BookingWindowService — window state machine', () => {
|
||||
isScheduleFull: jest.Mock;
|
||||
hasLiveReservations: jest.Mock;
|
||||
refreshWindowStatus: jest.Mock;
|
||||
expireLeftoverDayPool: jest.Mock;
|
||||
};
|
||||
let trainSchedulesRepository: { findById: jest.Mock; findAll: jest.Mock };
|
||||
let trainSchedulingService: { finalizeSchedule: jest.Mock; getWindowConfig: jest.Mock };
|
||||
@@ -73,6 +74,7 @@ describe('BookingWindowService — window state machine', () => {
|
||||
// No reservation is mid-pay-window by default, so the cycle concludes.
|
||||
hasLiveReservations: jest.fn().mockResolvedValue(false),
|
||||
refreshWindowStatus: jest.fn().mockResolvedValue(undefined),
|
||||
expireLeftoverDayPool: jest.fn().mockResolvedValue(0),
|
||||
};
|
||||
trainSchedulesRepository = {
|
||||
findById: jest.fn().mockResolvedValue(null),
|
||||
@@ -186,6 +188,8 @@ describe('BookingWindowService — window state machine', () => {
|
||||
expect(batch.setWindow).toHaveBeenCalledWith(scheduleId, 'FULL');
|
||||
expect(s.windowPhase).toBe('DONE');
|
||||
expect(trainSchedulingService.finalizeSchedule).toHaveBeenCalledWith(scheduleId);
|
||||
// The day's leftover waiting list is swept once this train is done.
|
||||
expect(batch.expireLeftoverDayPool).toHaveBeenCalledWith(scheduleId);
|
||||
});
|
||||
|
||||
it('conclude: NOT full + a cycle fits before departure → REOPEN (back to PRE_WINDOW)', async () => {
|
||||
@@ -210,6 +214,8 @@ describe('BookingWindowService — window state machine', () => {
|
||||
});
|
||||
await concludeCycle(s, new Date('2026-07-01T02:30:04.000Z'));
|
||||
expect(s.windowPhase).toBe('DONE');
|
||||
// No further train can run for this day → leftover waiting list is swept.
|
||||
expect(batch.expireLeftoverDayPool).toHaveBeenCalledWith(scheduleId);
|
||||
});
|
||||
|
||||
it('no transition fires before its deadline (idempotent tick)', async () => {
|
||||
|
||||
@@ -357,6 +357,10 @@ export class BookingWindowService implements OnModuleInit {
|
||||
this.logger.log(
|
||||
`[WINDOW] ${schedule.id} conclude → train FULL — window DONE, finalizing`,
|
||||
);
|
||||
// This train is done. If no other train on the route-day can still take
|
||||
// the waiting list, those bookings have nowhere to go — expire + notify
|
||||
// them now instead of leaving them FULLY_EXECUTED forever.
|
||||
await this.bookingBatchService.expireLeftoverDayPool(schedule.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,6 +394,9 @@ export class BookingWindowService implements OnModuleInit {
|
||||
`[WINDOW] ${schedule.id} conclude → not full but no cycle fits before ` +
|
||||
`departure — window DONE`,
|
||||
);
|
||||
// No further cycle on this train. Same sweep as the FULL branch: if no
|
||||
// sibling train can still take the day's waiting list, expire + notify.
|
||||
await this.bookingBatchService.expireLeftoverDayPool(schedule.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import { BadRequestException, Injectable, Logger, NotFoundException } from '@nes
|
||||
import { Between, DataSource, EntityManager, FindManyOptions, ILike, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
|
||||
|
||||
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { Cargo } from '../cargoes/entities/cargoes.entity';
|
||||
import { Company } from '../companies/entities/company.entity';
|
||||
import { Container } from '../container-management/entities/container.entity';
|
||||
import { CargoType } from '../rule-engine/entities/cargo-type.entity';
|
||||
import { InterchangeDocumentsService } from '../interchange-documents/interchange-documents.service';
|
||||
import type { InterchangeDocument } from '../interchange-documents/entities/interchange-document.entity';
|
||||
import { LastMileService } from '../last-mile/last-mile.service';
|
||||
@@ -3651,23 +3655,25 @@ export class WarehouseInventoryService {
|
||||
.leftJoinAndSelect('inv.warehouse', 'warehouse')
|
||||
.leftJoinAndSelect('inv.yard', 'yard')
|
||||
.leftJoinAndSelect('inv.zone', 'zone')
|
||||
.leftJoin('freight.bookings', 'booking', 'booking.id = inv.booking_id')
|
||||
.leftJoin('freight.companies', 'company', 'company.id = booking.company_id')
|
||||
// Entity-class joins: TypeORM parses a raw 'freight.table' string as an
|
||||
// alias.property path ("freight" alias was not found) — runtime 500.
|
||||
.leftJoin(Booking, 'booking', 'booking.id = inv.booking_id')
|
||||
.leftJoin(Company, 'company', 'company.id = booking.company_id')
|
||||
.leftJoin(
|
||||
'freight.containers',
|
||||
Container,
|
||||
'container',
|
||||
`((inv.container_id IS NOT NULL AND container.id = inv.container_id)
|
||||
OR (inv.container_id IS NULL AND container.booking_id = inv.booking_id))
|
||||
AND container.deleted_at IS NULL`,
|
||||
)
|
||||
.leftJoin(
|
||||
'freight.cargoes',
|
||||
Cargo,
|
||||
'cargo',
|
||||
`((inv.cargo_id IS NOT NULL AND cargo.id = inv.cargo_id)
|
||||
OR (inv.cargo_id IS NULL AND cargo.booking_id = inv.booking_id))
|
||||
AND cargo.deleted_at IS NULL`,
|
||||
)
|
||||
.leftJoin('freight.cargo_types', 'cargo_type', 'cargo_type.id = cargo.cargo_type_id')
|
||||
.leftJoin(CargoType, 'cargo_type', 'cargo_type.id = cargo.cargo_type_id')
|
||||
.addSelect('booking.reference', 'b_reference')
|
||||
.addSelect('company.name', 'c_name')
|
||||
.addSelect('container.container_number', 'ct_number')
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { Route } from '../routes/entities/route.entity';
|
||||
import { WarehouseInventory } from './entities/warehouse-inventory.entity';
|
||||
|
||||
/**
|
||||
@@ -50,9 +52,11 @@ export class WarehouseSchedulingAdapterService {
|
||||
.leftJoinAndSelect('inv.warehouse', 'warehouse')
|
||||
.leftJoinAndSelect('inv.yard', 'yard')
|
||||
.leftJoinAndSelect('inv.zone', 'zone')
|
||||
.innerJoin('freight.bookings', 'booking', 'booking.id = inv.booking_id')
|
||||
// Entity-class joins: TypeORM parses a raw 'freight.table' string as an
|
||||
// alias.property path ("freight" alias was not found) — runtime 500.
|
||||
.innerJoin(Booking, 'booking', 'booking.id = inv.booking_id')
|
||||
.innerJoin(
|
||||
'freight.routes',
|
||||
Route,
|
||||
'route',
|
||||
'route.id = :routeId AND (route.origin_yard_id = booking.origin_yard_id OR route.destination_yard_id = booking.destination_yard_id)',
|
||||
{ routeId },
|
||||
|
||||
Reference in New Issue
Block a user