Container return and Price per KM for Vehcles

This commit is contained in:
Hagernesh
2026-07-31 09:13:13 +00:00
parent 7f87e2d8ca
commit 90fef6856b
3 changed files with 113 additions and 34 deletions

View File

@@ -87,6 +87,12 @@ interface CarriageAcceptanceWagonRow {
sealNumbers: string | null;
}
/** A received-but-not-yet-marshalled export line, standing in for a wagon row. */
interface CarriageAcceptanceReceivedRow {
allocatedWeightTons: string | null;
containerNumbers: string | null;
}
const URGENT_PRIORITY_THRESHOLD = 1000;
const NEEDS_ACTION_STATUSES = [
'SUBMITTED',
@@ -233,7 +239,7 @@ export class BookingsService {
*/
async carriageAcceptanceSheet(bookingId: string): Promise<{ filename: string; buffer: Buffer }> {
const booking = await this.findById(bookingId);
const wagons: CarriageAcceptanceWagonRow[] = await this.dataSource.query(
let wagons: CarriageAcceptanceWagonRow[] = await this.dataSource.query(
`SELECT tsw.sequence_no AS "sequenceNo",
COALESCE(wt.code, wt.name) AS "wagonType",
w.wagon_number AS "wagonNumber",
@@ -264,13 +270,57 @@ export class BookingsService {
ORDER BY tsw.sequence_no`,
[bookingId],
);
if (wagons.length === 0) {
throw new BadRequestException(
'No wagons are allocated to this booking yet — the carriage acceptance sheet is issued after wagon allocation',
);
// Export acceptance happens at the warehouse gate, not at marshalling: EDR
// takes custody of the cargo when it receives it, and the customer is handed
// this sheet then — before the booking is put on a train. So a received
// export booking gets its sheet off the received cargo, wagon columns blank
// until the consist exists. Import keeps the allocation gate: nothing is
// accepted from the customer before the wagons carry it.
//
// Receipt is proven by the warehouse GRN, but the GRN is warehouse paperwork
// and never appears on this sheet — it is only the signal that EDR has taken
// the cargo, which is what the customer's sheet attests to.
const pendingWagons = wagons.length === 0;
if (pendingWagons) {
const receivedLines: CarriageAcceptanceReceivedRow[] =
booking.tradeDirection === 'EXPORT'
? await this.dataSource.query(
`SELECT inv.weight AS "allocatedWeightTons",
c.container_number AS "containerNumbers"
FROM freight.warehouse_inventory inv
LEFT JOIN freight.containers c
ON c.id = inv.container_id AND c.deleted_at IS NULL
WHERE inv.booking_id = $1 AND inv.deleted_at IS NULL
AND COALESCE(NULLIF(TRIM(inv.grn_number), ''), '') <> ''
ORDER BY inv.created_at`,
[bookingId],
)
: [];
if (receivedLines.length === 0) {
throw new BadRequestException(
booking.tradeDirection === 'EXPORT'
? 'This export booking has no GRN yet — receive the cargo at the warehouse before issuing the carriage acceptance sheet'
: 'No wagons are allocated to this booking yet — the carriage acceptance sheet is issued after wagon allocation',
);
}
wagons = receivedLines.map((row, index) => ({
sequenceNo: index + 1,
wagonType: null,
wagonNumber: null,
tareWeightTons: null,
equatedLength: null,
loadCapacityTons: null,
allocatedWeightTons: row.allocatedWeightTons,
trainNumber: null,
departureAt: null,
marshalledAt: null,
arrivalAt: null,
containerNumbers: row.containerNumbers,
sealNumbers: null,
}));
}
const html = this.buildCarriageAcceptanceSheetHtml(booking, wagons);
const html = this.buildCarriageAcceptanceSheetHtml(booking, wagons, { pendingWagons });
const buffer = await this.pdfRender.htmlToPdfBuffer(html, {
label: 'carriage acceptance sheet',
fallback: (prepared) => buildTabularFallbackPdf(prepared),
@@ -299,6 +349,7 @@ export class BookingsService {
private buildCarriageAcceptanceSheetHtml(
booking: Booking,
wagons: CarriageAcceptanceWagonRow[],
{ pendingWagons }: { pendingWagons: boolean },
): string {
const esc = (v: unknown) => this.escapeHtml(String(v ?? '-'));
const num = (v: unknown, digits = 3) => (Number(v) || 0).toFixed(digits);
@@ -424,7 +475,11 @@ export class BookingsService {
</tbody>
<tfoot>
<tr>
<td colspan="3">Total wagons: ${wagons.length} (full ${fullWagons} / empty ${wagons.length - fullWagons})</td>
<td colspan="3">${
pendingWagons
? `Received lines: ${wagons.length} — wagons pending marshalling`
: `Total wagons: ${wagons.length} (full ${fullWagons} / empty ${wagons.length - fullWagons})`
}</td>
<td class="num">${num(totals.tare, 2)}</td>
<td class="num">${num(totals.length)}</td>
<td class="num">${num(totals.capacity)}</td>
@@ -435,9 +490,14 @@ export class BookingsService {
</table>
<div class="notice">
The wagons listed above are accepted for carriage under booking ${esc(booking.reference)}.
Wagon identity, container and seal numbers must be verified against the physical consist
before the sheet is signed.
${
pendingWagons
? `The cargo listed above is accepted for carriage under booking ${esc(booking.reference)}.
Wagon identity and seal numbers are filled in when the booking is marshalled onto a train.`
: `The wagons listed above are accepted for carriage under booking ${esc(booking.reference)}.
Wagon identity, container and seal numbers must be verified against the physical consist
before the sheet is signed.`
}
</div>
<div class="signatures">