From 140c5a6469e473d13e1c671a22f613c6512772a5 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Fri, 10 Jul 2026 07:04:41 +0000 Subject: [PATCH] fix(bookings): freight order renders styled on Chromium-less servers The customer-truck freight order went through ContractPdfService, whose only fallback is a crude text dump - so on servers without Chromium the document rendered as unstyled text. - render via the shared PdfRenderService with the styled tabular fallback (same path as marshalling / release documents) - rebuild the freight-order HTML in the standard document shape: brand header, booking summary tiles, one trucks table (plate / driver / type / containers / arrival), gate notice, signature lines - which is also what the fallback parser reads - keep the two watermarked copies (Port Operations, Gate Security & Carrier) for the Chromium path; drop the now-unused ContractPdfService injection Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/modules/bookings/bookings.module.ts | 2 + .../src/modules/bookings/bookings.service.ts | 141 ++++++++++-------- 2 files changed, 84 insertions(+), 59 deletions(-) diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts index 4b3e3bcca..96ed3b701 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts @@ -11,6 +11,7 @@ import { RuleEngineModule } from '../rule-engine/rule-engine.module'; import { FileUploadSettingsModule } from '../file-upload-settings/file-upload-settings.module'; import { SignaturesModule } from '../signatures/signatures.module'; import { BillingModule } from '../billing/billing.module'; +import { DocumentsModule } from '../billing/documents/documents.module'; import { FirstMileModule } from '../first-mile/first-mile.module'; import { LastMileModule } from '../last-mile/last-mile.module'; import { BookingContractService } from './booking-contract.service'; @@ -68,6 +69,7 @@ import { VehiclesModule } from "../vehicles/vehicles.module"; CustomerTruckContainer, ]), BillingModule, + DocumentsModule, NotificationsModule, NotificationInboxModule, forwardRef(() => FirstMileModule), diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index dac739fbf..9f72c7409 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -50,7 +50,8 @@ import { Booking } from './entities/booking.entity'; import { BookingContainerAllocation } from './entities/booking-container-allocation.entity'; import { FileRecord } from '../files/entities/file.entity'; import { CustomerTruckAssignmentDto } from './dto/customer-truck-assignment.dto'; -import { ContractPdfService } from '../../contracts/contract-pdf.service'; +import { PdfRenderService } from '../billing/documents/pdf-render.service'; +import { buildTabularFallbackPdf } from '../billing/documents/styled-pdf.util'; /** Paginated booking list: flat `total` (backoffice) + `meta` block (portal). */ export interface PaginatedBookings { @@ -99,7 +100,7 @@ export class BookingsService { private readonly containerTypesService: ContainerTypesService, private readonly consolidationService: ConsolidationService, private readonly vehiclesService: VehiclesService, - private readonly contractPdfService: ContractPdfService, + private readonly pdfRender: PdfRenderService, private readonly events: EventEmitter2, ) {} @@ -170,7 +171,12 @@ export class BookingsService { ); const html = this.buildCustomerTruckFreightOrderHtml(booking, trucks); - const buffer = await this.contractPdfService.htmlToPdfBuffer(html); + // Chromium when available; otherwise the styled tabular fallback (never the + // generic text dump — the freight order is an outward-facing gate document). + const buffer = await this.pdfRender.htmlToPdfBuffer(html, { + label: 'freight order', + fallback: (prepared) => buildTabularFallbackPdf(prepared), + }); return { filename: `freight-order-${booking.reference.replace(/[^a-zA-Z0-9_-]+/g, '-')}.pdf`, buffer, @@ -262,21 +268,10 @@ export class BookingsService { containers: string | null; }>, ): string { + const esc = (v: unknown) => this.escapeHtml(String(v ?? '-')); const assignedAt = booking.customerTruckAssignedAt ? new Date(booking.customerTruckAssignedAt).toLocaleString('en-GB') : '-'; - const bookingRows: Array<[string, string | null | undefined]> = [ - ['Booking Reference', booking.reference], - ['Client Name', booking.company?.name], - ['Client ID', booking.companyId], - ['Trade Direction', booking.tradeDirection], - ['Freight Type', booking.freightType], - ['Assigned At', assignedAt], - ['Booking Status', booking.status], - ]; - const bookingRowHtml = bookingRows - .map(([label, value]) => `${this.escapeHtml(label)}${this.escapeHtml(value || '-')}`) - .join(''); // Fall back to the legacy single-truck booking columns when there are no // multi-truck rows (bookings assigned before the multi-truck feature). @@ -297,44 +292,63 @@ export class BookingsService { ] : []; - const truckBlocks = truckList - .map((t, i) => { - const rows: Array<[string, string | null | undefined]> = [ - ['Truck Plate Number', t.plateNumber], - ['Driver Name', t.driverName], - ['Truck Type', t.truckType], - ['Containers Loaded', t.containers], - [ - 'Arrival', - t.arrivedAt ? new Date(t.arrivedAt).toLocaleString('en-GB') : 'Awaiting arrival', - ], - ]; - const html = rows - .map( - ([label, value]) => - `${this.escapeHtml(label)}${this.escapeHtml(value || '-')}`, - ) - .join(''); - return `

Truck ${i + 1}

${html}
`; - }) + const truckRows = truckList + .map( + (t, i) => ` + ${i + 1} + ${esc(t.plateNumber)} + ${esc(t.driverName)} + ${esc(t.truckType)} + ${esc(t.containers)} + ${t.arrivedAt ? esc(new Date(t.arrivedAt).toLocaleString('en-GB')) : 'Awaiting arrival'} + `, + ) .join(''); const copy = (watermark: string) => `
-
${this.escapeHtml(watermark)}
-
+
${esc(watermark)}
+
+
Ethio-Djibouti Railway S.C.

Freight Order

-

Customer external truck assignment — ${truckList.length} truck${truckList.length !== 1 ? 's' : ''}

+
Customer external truck assignment — ${truckList.length} truck${truckList.length !== 1 ? 's' : ''}
- ${this.escapeHtml(booking.reference)} -
- ${bookingRowHtml}
- ${truckBlocks} +
+ Booking + ${esc(booking.reference)} + Generated: ${esc(new Date().toLocaleString('en-GB'))} +
+ +
+
Client${esc(booking.company?.name)}
+
Client ID${esc(booking.companyId)}
+
Trade direction${esc(booking.tradeDirection)}
+
Freight type${esc(booking.freightType)}
+
Assigned at${esc(assignedAt)}
+
Booking status${esc(booking.status)}
+
+ + + + + + + + + + + + ${truckRows} +
#Truck plateDriverTruck typeContainers loadedArrival
+
+ Present this freight order at the warehouse gate. Each truck may only collect the + containers listed against it; the handover must be signed before any truck leaves. +
-
Customer / Carrier Signature
-
Port Operations Verification
-
Gate Security Verification
+
Customer / Carrier signature — date
+
Port operations verification — date
+
Gate security verification — date
`; @@ -342,21 +356,30 @@ export class BookingsService { + Freight Order