fix(freight): gate loading on booking paymentStatus only; default schedule voyage no. to train's voyage number

This commit is contained in:
marshal
2026-09-02 21:15:48 +00:00
parent d51bed5630
commit 3e274cc2a2
30 changed files with 629 additions and 117 deletions

View File

@@ -122,6 +122,7 @@ interface BookingSummaryRow {
id: string;
reference: string | null;
status: string | null;
paymentStatus: string | null;
customer: string | null;
}
@@ -1333,14 +1334,19 @@ export class WarehouseInventoryService {
return this.findById(saved.id);
}
/** Auto-load all READY_FOR_LOADING inventory whose booking is PAID. Unpaid stay pending. */
/**
* Auto-load all READY_FOR_LOADING inventory whose booking is paid (payment
* status PAID — the booking status is not consulted). Unpaid stay pending.
*/
async autoLoadReady(): Promise<AutoLoadResult> {
const ready = await this.inventoryRepository.findAll({ where: { status: 'READY_FOR_LOADING' } });
const result: AutoLoadResult = { loadedCount: 0, skippedCount: 0, results: [] };
for (const item of ready) {
const bookingStatus = item.bookingId ? await this.getBookingStatus(item.bookingId) : null;
if (bookingStatus !== 'PAID') {
const paymentStatus = item.bookingId
? await this.getBookingPaymentStatus(item.bookingId)
: null;
if (paymentStatus !== 'PAID') {
result.skippedCount += 1;
result.results.push({ inventoryId: item.id, status: 'SKIPPED', reason: 'Booking not PAID' });
continue;
@@ -3150,12 +3156,14 @@ export class WarehouseInventoryService {
throw new BadRequestException(`Inventory must be STORED to reserve (current: ${item.status})`);
}
const status = await this.getBookingStatus(dto.bookingId);
if (!status) {
const paymentStatus = await this.getBookingPaymentStatus(dto.bookingId);
if (paymentStatus === null) {
throw new NotFoundException(`Booking ${dto.bookingId} not found`);
}
if (status !== 'PAID') {
throw new BadRequestException(`Booking must be PAID to reserve inventory (current: ${status})`);
if (paymentStatus !== 'PAID') {
throw new BadRequestException(
`Booking must be paid to reserve inventory (payment status: ${paymentStatus})`,
);
}
await this.dataSource.transaction(async (manager) => {
@@ -6759,12 +6767,19 @@ export class WarehouseInventoryService {
};
}
private async getBookingStatus(bookingId: string): Promise<string | null> {
const [row]: Array<{ status: string | null }> = await this.dataSource.query(
'SELECT status FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL LIMIT 1',
/**
* The booking's PAYMENT status — the only signal loading/reservation gates
* use to decide "paid". Returns null when the booking does not exist;
* an existing booking with no payment status yet reads as PENDING.
*/
private async getBookingPaymentStatus(bookingId: string): Promise<string | null> {
const [row]: Array<{ paymentStatus: string | null }> = await this.dataSource.query(
`SELECT payment_status AS "paymentStatus"
FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
[bookingId],
);
return row?.status ?? null;
if (!row) return null;
return row.paymentStatus ?? 'PENDING';
}
private async attachBookingSummaries(items: WarehouseInventory[]): Promise<void> {
@@ -6772,7 +6787,8 @@ export class WarehouseInventoryService {
if (bookingIds.length === 0) return;
const rows: BookingSummaryRow[] = await this.dataSource.query(
`SELECT b.id, b.reference, b.status, company.name AS customer
`SELECT b.id, b.reference, b.status, b.payment_status AS "paymentStatus",
company.name AS customer
FROM freight.bookings b
LEFT JOIN freight.companies company ON company.id = b.company_id
WHERE b.id = ANY($1) AND b.deleted_at IS NULL`,
@@ -6786,6 +6802,7 @@ export class WarehouseInventoryService {
Object.assign(item, {
bookingReference: summary.reference,
bookingStatus: summary.status,
bookingPaymentStatus: summary.paymentStatus,
customerName: summary.customer,
});
});