Merge pull request #591 from Tria-plc/Truckdetantion

Truckdetantion

Receive to warehouse form cleanup and trimming
Wiring GRN with bookings
Fixed Slow GRN
plus fixed Freight order genration and rendering
This commit is contained in:
Hagernesh Tadesse
2026-07-10 10:35:56 +03:00
committed by GitHub
5 changed files with 121 additions and 106 deletions

View File

@@ -11,6 +11,7 @@ import { RuleEngineModule } from '../rule-engine/rule-engine.module';
import { FileUploadSettingsModule } from '../file-upload-settings/file-upload-settings.module'; import { FileUploadSettingsModule } from '../file-upload-settings/file-upload-settings.module';
import { SignaturesModule } from '../signatures/signatures.module'; import { SignaturesModule } from '../signatures/signatures.module';
import { BillingModule } from '../billing/billing.module'; import { BillingModule } from '../billing/billing.module';
import { DocumentsModule } from '../billing/documents/documents.module';
import { FirstMileModule } from '../first-mile/first-mile.module'; import { FirstMileModule } from '../first-mile/first-mile.module';
import { LastMileModule } from '../last-mile/last-mile.module'; import { LastMileModule } from '../last-mile/last-mile.module';
import { BookingContractService } from './booking-contract.service'; import { BookingContractService } from './booking-contract.service';
@@ -68,6 +69,7 @@ import { VehiclesModule } from "../vehicles/vehicles.module";
CustomerTruckContainer, CustomerTruckContainer,
]), ]),
BillingModule, BillingModule,
DocumentsModule,
NotificationsModule, NotificationsModule,
NotificationInboxModule, NotificationInboxModule,
forwardRef(() => FirstMileModule), forwardRef(() => FirstMileModule),

View File

@@ -50,7 +50,8 @@ import { Booking } from './entities/booking.entity';
import { BookingContainerAllocation } from './entities/booking-container-allocation.entity'; import { BookingContainerAllocation } from './entities/booking-container-allocation.entity';
import { FileRecord } from '../files/entities/file.entity'; import { FileRecord } from '../files/entities/file.entity';
import { CustomerTruckAssignmentDto } from './dto/customer-truck-assignment.dto'; 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). */ /** Paginated booking list: flat `total` (backoffice) + `meta` block (portal). */
export interface PaginatedBookings { export interface PaginatedBookings {
@@ -99,7 +100,7 @@ export class BookingsService {
private readonly containerTypesService: ContainerTypesService, private readonly containerTypesService: ContainerTypesService,
private readonly consolidationService: ConsolidationService, private readonly consolidationService: ConsolidationService,
private readonly vehiclesService: VehiclesService, private readonly vehiclesService: VehiclesService,
private readonly contractPdfService: ContractPdfService, private readonly pdfRender: PdfRenderService,
private readonly events: EventEmitter2, private readonly events: EventEmitter2,
) {} ) {}
@@ -170,7 +171,12 @@ export class BookingsService {
); );
const html = this.buildCustomerTruckFreightOrderHtml(booking, trucks); 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 { return {
filename: `freight-order-${booking.reference.replace(/[^a-zA-Z0-9_-]+/g, '-')}.pdf`, filename: `freight-order-${booking.reference.replace(/[^a-zA-Z0-9_-]+/g, '-')}.pdf`,
buffer, buffer,
@@ -262,21 +268,10 @@ export class BookingsService {
containers: string | null; containers: string | null;
}>, }>,
): string { ): string {
const esc = (v: unknown) => this.escapeHtml(String(v ?? '-'));
const assignedAt = booking.customerTruckAssignedAt const assignedAt = booking.customerTruckAssignedAt
? new Date(booking.customerTruckAssignedAt).toLocaleString('en-GB') ? 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]) => `<tr><th>${this.escapeHtml(label)}</th><td>${this.escapeHtml(value || '-')}</td></tr>`)
.join('');
// Fall back to the legacy single-truck booking columns when there are no // Fall back to the legacy single-truck booking columns when there are no
// multi-truck rows (bookings assigned before the multi-truck feature). // multi-truck rows (bookings assigned before the multi-truck feature).
@@ -297,44 +292,63 @@ export class BookingsService {
] ]
: []; : [];
const truckBlocks = truckList const truckRows = truckList
.map((t, i) => { .map(
const rows: Array<[string, string | null | undefined]> = [ (t, i) => `<tr>
['Truck Plate Number', t.plateNumber], <td class="num">${i + 1}</td>
['Driver Name', t.driverName], <td>${esc(t.plateNumber)}</td>
['Truck Type', t.truckType], <td>${esc(t.driverName)}</td>
['Containers Loaded', t.containers], <td>${esc(t.truckType)}</td>
[ <td>${esc(t.containers)}</td>
'Arrival', <td>${t.arrivedAt ? esc(new Date(t.arrivedAt).toLocaleString('en-GB')) : 'Awaiting arrival'}</td>
t.arrivedAt ? new Date(t.arrivedAt).toLocaleString('en-GB') : 'Awaiting arrival', </tr>`,
], )
];
const html = rows
.map(
([label, value]) =>
`<tr><th>${this.escapeHtml(label)}</th><td>${this.escapeHtml(value || '-')}</td></tr>`,
)
.join('');
return `<div class="truck"><h2>Truck ${i + 1}</h2><table>${html}</table></div>`;
})
.join(''); .join('');
const copy = (watermark: string) => ` const copy = (watermark: string) => `
<section class="copy"> <section class="copy">
<div class="watermark">${this.escapeHtml(watermark)}</div> <div class="watermark">${esc(watermark)}</div>
<header> <div class="top">
<div> <div>
<div class="brand">Ethio-Djibouti Railway S.C.</div>
<h1>Freight Order</h1> <h1>Freight Order</h1>
<p>Customer external truck assignment — ${truckList.length} truck${truckList.length !== 1 ? 's' : ''}</p> <div class="subtitle">Customer external truck assignment — ${truckList.length} truck${truckList.length !== 1 ? 's' : ''}</div>
</div> </div>
<strong>${this.escapeHtml(booking.reference)}</strong> <div class="meta">
</header> Booking
<table>${bookingRowHtml}</table> <strong>${esc(booking.reference)}</strong>
${truckBlocks} Generated: ${esc(new Date().toLocaleString('en-GB'))}
</div>
</div>
<div class="summary">
<div class="tile"><span>Client</span><strong>${esc(booking.company?.name)}</strong></div>
<div class="tile"><span>Client ID</span><strong>${esc(booking.companyId)}</strong></div>
<div class="tile"><span>Trade direction</span><strong>${esc(booking.tradeDirection)}</strong></div>
<div class="tile"><span>Freight type</span><strong>${esc(booking.freightType)}</strong></div>
<div class="tile"><span>Assigned at</span><strong>${esc(assignedAt)}</strong></div>
<div class="tile"><span>Booking status</span><strong>${esc(booking.status)}</strong></div>
</div>
<table>
<thead>
<tr>
<th class="num">#</th>
<th>Truck plate</th>
<th>Driver</th>
<th>Truck type</th>
<th>Containers loaded</th>
<th>Arrival</th>
</tr>
</thead>
<tbody>${truckRows}</tbody>
</table>
<div class="notice">
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.
</div>
<div class="signatures"> <div class="signatures">
<div>Customer / Carrier Signature</div> <div class="line">Customer / Carrier signature — date</div>
<div>Port Operations Verification</div> <div class="line">Port operations verification — date</div>
<div>Gate Security Verification</div> <div class="line">Gate security verification — date</div>
</div> </div>
</section>`; </section>`;
@@ -342,21 +356,30 @@ export class BookingsService {
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Freight Order</title>
<style> <style>
body { font-family: Arial, sans-serif; color: #10202f; margin: 0; } @page { size: A4 portrait; margin: 10mm; }
.copy { position: relative; min-height: 46vh; padding: 28px 32px; page-break-inside: avoid; border-bottom: 1px dashed #94a3b8; } * { box-sizing: border-box; }
.watermark { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 34px; font-weight: 800; color: rgba(16, 32, 47, 0.08); transform: rotate(-18deg); pointer-events: none; } body { margin: 0; color: #0f172a; font-family: Arial, sans-serif; }
header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 3px solid #0a9f6a; padding-bottom: 14px; margin-bottom: 18px; } .copy { position: relative; padding: 24px 28px; page-break-inside: avoid; border-bottom: 1px dashed #94a3b8; }
h1 { margin: 0; font-size: 28px; letter-spacing: 0; } .watermark { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 30px; font-weight: 800; color: rgba(15, 23, 42, 0.07); transform: rotate(-18deg); pointer-events: none; }
h2 { margin: 18px 0 8px; font-size: 14px; color: #0a6f4d; } .top { display: flex; justify-content: space-between; border-bottom: 3px solid #0f766e; padding-bottom: 10px; gap: 24px; }
p { margin: 4px 0 0; color: #64748b; } .brand { font-size: 11px; color: #475569; text-transform: uppercase; letter-spacing: .08em; font-weight: 700; }
strong { font-size: 16px; color: #0a9f6a; } h1 { margin: 6px 0 0; font-size: 25px; line-height: 1.05; }
table { width: 100%; border-collapse: collapse; position: relative; z-index: 1; margin-bottom: 6px; } .subtitle { margin-top: 4px; color: #64748b; font-size: 12px; }
th, td { border: 1px solid #cbd5e1; padding: 9px 10px; text-align: left; font-size: 12px; } .meta { text-align: right; font-size: 11px; color: #475569; min-width: 210px; }
th { width: 34%; background: #f1f5f9; } .meta strong { display: block; margin: 4px 0; color: #0f172a; font-size: 15px; }
.truck { page-break-inside: avoid; } .summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 14px 0; }
.signatures { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 34px; font-size: 11px; color: #475569; position: relative; z-index: 1; } .tile { border: 1px solid #cbd5e1; padding: 8px; min-height: 48px; }
.signatures div { border-top: 1px solid #334155; padding-top: 8px; min-height: 28px; } .tile span { display: block; color: #64748b; font-size: 9px; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; }
.tile strong { font-size: 11px; }
table { width: 100%; border-collapse: collapse; position: relative; z-index: 1; }
th { background: #f8fafc; color: #475569; text-align: left; }
th, td { border: 1px solid #cbd5e1; padding: 6px 7px; font-size: 10.5px; vertical-align: top; }
.num { text-align: right; width: 26px; }
.notice { margin-top: 10px; border-left: 4px solid #0f766e; background: #f0fdfa; padding: 8px 10px; font-size: 10px; color: #134e4a; }
.signatures { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 30px; position: relative; z-index: 1; }
.line { border-top: 1px solid #334155; padding-top: 7px; font-size: 9px; color: #475569; min-height: 30px; }
</style> </style>
</head> </head>
<body> <body>

View File

@@ -207,6 +207,7 @@ export interface EligibleBookingRow {
customerTin: string | null; customerTin: string | null;
customerPhone: string | null; customerPhone: string | null;
containerNumber: string | null; containerNumber: string | null;
sealNumbers: string | null;
containerQuantity: number | null; containerQuantity: number | null;
containerPackagingType: string | null; containerPackagingType: string | null;
cargoDescription: string | null; cargoDescription: string | null;
@@ -774,7 +775,8 @@ export class WarehouseInventoryService {
company.name AS "customer", company.name AS "customer",
company.tin AS "customerTin", company.tin AS "customerTin",
COALESCE(company.contact_person_phone, company.phone, company.general_manager_phone, company.etrade_phone) AS "customerPhone", COALESCE(company.contact_person_phone, company.phone, company.general_manager_phone, company.etrade_phone) AS "customerPhone",
bc.container_numbers AS "containerNumber", COALESCE(bcu.unit_numbers, bc.container_numbers) AS "containerNumber",
bcu.seal_numbers AS "sealNumbers",
bc.container_quantity AS "containerQuantity", bc.container_quantity AS "containerQuantity",
bc.container_packaging_type AS "containerPackagingType", bc.container_packaging_type AS "containerPackagingType",
(NULLIF(TRIM(COALESCE(b.last_mile_delivery_address, '')), '') IS NOT NULL (NULLIF(TRIM(COALESCE(b.last_mile_delivery_address, '')), '') IS NOT NULL
@@ -831,6 +833,14 @@ export class WarehouseInventoryService {
LEFT JOIN freight.container_types container_type ON container_type.id = booking_container.container_type_id LEFT JOIN freight.container_types container_type ON container_type.id = booking_container.container_type_id
WHERE booking_container.booking_id = b.id AND booking_container.deleted_at IS NULL WHERE booking_container.booking_id = b.id AND booking_container.deleted_at IS NULL
) bc ON true ) bc ON true
LEFT JOIN LATERAL (
SELECT string_agg(NULLIF(unit.container_number, ''), ', ' ORDER BY unit.container_number) AS unit_numbers,
string_agg(DISTINCT NULLIF(unit.seal_number, ''), ', ') AS seal_numbers
FROM freight.booking_container_units unit
JOIN freight.booking_container line
ON line.id = unit.booking_container_id AND line.deleted_at IS NULL
WHERE line.booking_id = b.id AND unit.deleted_at IS NULL
) bcu ON true
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT first_mile.id, first_mile.status, first_mile.vehicle_id SELECT first_mile.id, first_mile.status, first_mile.vehicle_id
FROM freight.first_mile first_mile FROM freight.first_mile first_mile
@@ -881,11 +891,18 @@ export class WarehouseInventoryService {
}> = []; }> = [];
await this.dataSource.transaction(async (manager) => { await this.dataSource.transaction(async (manager) => {
await this.validateLocation(manager, { const { warehouse, yard, zone } = await this.validateLocation(manager, {
warehouseId: dto.warehouseId, warehouseId: dto.warehouseId,
yardId: dto.yardId, yardId: dto.yardId,
zoneId: dto.zoneId, zoneId: dto.zoneId,
}); });
// The receive location is whatever the operator selected above — never a
// hand-typed string. Stamp it on the truck entrance for the GRN/notes.
if (dto.truckEntrance && !dto.truckEntrance.warehouseCodeLocation) {
dto.truckEntrance.warehouseCodeLocation = [warehouse.code, yard.code, zone.code]
.filter(Boolean)
.join(' / ');
}
for (const bookingId of dto.bookingIds) { for (const bookingId of dto.bookingIds) {
const skip = (reason: string) => { const skip = (reason: string) => {

View File

@@ -150,9 +150,6 @@ interface TruckEntranceFormState {
assignedEquipmentNumber: string; assignedEquipmentNumber: string;
customsSealNumber: string; customsSealNumber: string;
declarationNumber: string; declarationNumber: string;
incoterms: string;
hsCodes: string;
itemCode: string;
itemDescription: string; itemDescription: string;
packagingType: string; packagingType: string;
unitCount: number | ''; unitCount: number | '';
@@ -162,7 +159,6 @@ interface TruckEntranceFormState {
volumeDimensions: string; volumeDimensions: string;
conditionAtReceipt: string; conditionAtReceipt: string;
damagedRejectedQuantity: number | ''; damagedRejectedQuantity: number | '';
warehouseCodeLocation: string;
driverName: string; driverName: string;
driverPhone: string; driverPhone: string;
driverLicenseNumber: string; driverLicenseNumber: string;
@@ -205,9 +201,6 @@ const emptyTruckEntrance = (): TruckEntranceFormState => ({
assignedEquipmentNumber: '', assignedEquipmentNumber: '',
customsSealNumber: '', customsSealNumber: '',
declarationNumber: '', declarationNumber: '',
incoterms: '',
hsCodes: '',
itemCode: '',
itemDescription: '', itemDescription: '',
packagingType: '', packagingType: '',
unitCount: '', unitCount: '',
@@ -217,7 +210,6 @@ const emptyTruckEntrance = (): TruckEntranceFormState => ({
volumeDimensions: '', volumeDimensions: '',
conditionAtReceipt: '', conditionAtReceipt: '',
damagedRejectedQuantity: '', damagedRejectedQuantity: '',
warehouseCodeLocation: '',
driverName: '', driverName: '',
driverPhone: '', driverPhone: '',
driverLicenseNumber: '', driverLicenseNumber: '',
@@ -239,9 +231,6 @@ const toTruckEntrancePayload = (form: TruckEntranceFormState): TruckEntrancePayl
assignedEquipmentNumber: form.assignedEquipmentNumber.trim() || undefined, assignedEquipmentNumber: form.assignedEquipmentNumber.trim() || undefined,
customsSealNumber: form.customsSealNumber.trim() || undefined, customsSealNumber: form.customsSealNumber.trim() || undefined,
declarationNumber: form.declarationNumber.trim() || undefined, declarationNumber: form.declarationNumber.trim() || undefined,
incoterms: form.incoterms.trim() || undefined,
hsCodes: form.hsCodes.trim() || undefined,
itemCode: form.itemCode.trim() || undefined,
itemDescription: form.itemDescription.trim() || undefined, itemDescription: form.itemDescription.trim() || undefined,
packagingType: form.packagingType.trim() || undefined, packagingType: form.packagingType.trim() || undefined,
unitCount: form.unitCount === '' ? undefined : Number(form.unitCount), unitCount: form.unitCount === '' ? undefined : Number(form.unitCount),
@@ -251,7 +240,6 @@ const toTruckEntrancePayload = (form: TruckEntranceFormState): TruckEntrancePayl
volumeDimensions: form.volumeDimensions.trim() || undefined, volumeDimensions: form.volumeDimensions.trim() || undefined,
conditionAtReceipt: form.conditionAtReceipt.trim() || undefined, conditionAtReceipt: form.conditionAtReceipt.trim() || undefined,
damagedRejectedQuantity: form.damagedRejectedQuantity === '' ? undefined : Number(form.damagedRejectedQuantity), damagedRejectedQuantity: form.damagedRejectedQuantity === '' ? undefined : Number(form.damagedRejectedQuantity),
warehouseCodeLocation: form.warehouseCodeLocation.trim() || undefined,
driverName: form.driverName.trim(), driverName: form.driverName.trim(),
driverPhone: form.driverPhone.trim(), driverPhone: form.driverPhone.trim(),
driverLicenseNumber: form.driverLicenseNumber.trim() || undefined, driverLicenseNumber: form.driverLicenseNumber.trim() || undefined,
@@ -296,6 +284,10 @@ const truckEntranceFromBookings = (bookings: EligibleBooking[]): {
const truckType = commonNonEmptyValue( const truckType = commonNonEmptyValue(
bookings.map((booking) => booking.firstMileTruckType || booking.customerTruckType), bookings.map((booking) => booking.firstMileTruckType || booking.customerTruckType),
); );
const customsSealNumber = commonNonEmptyValue(bookings.map((booking) => booking.sealNumbers));
// Booking's declared cargo weight (tonnes) — the receive-time net until re-weighed.
const bookingWeight = bookings.length === 1 ? Number(bookings[0]?.weight ?? '') : NaN;
const netWeightKg: number | '' = Number.isFinite(bookingWeight) && bookingWeight > 0 ? bookingWeight : '';
const edrDigitalBookingId = const edrDigitalBookingId =
bookings.length === 1 bookings.length === 1
? bookings[0]?.reference ?? bookings[0]?.id ?? '' ? bookings[0]?.reference ?? bookings[0]?.id ?? ''
@@ -321,9 +313,11 @@ const truckEntranceFromBookings = (bookings: EligibleBooking[]): {
customerPhone, customerPhone,
edrDigitalBookingId, edrDigitalBookingId,
assignedEquipmentNumber, assignedEquipmentNumber,
customsSealNumber,
itemDescription, itemDescription,
packagingType, packagingType,
unitCount, unitCount,
netWeightKg,
grossWeightKg: '', grossWeightKg: '',
truckPlateNumber, truckPlateNumber,
trailerPlateNumber, trailerPlateNumber,
@@ -331,6 +325,7 @@ const truckEntranceFromBookings = (bookings: EligibleBooking[]): {
driverPhone, driverPhone,
driverLicenseNumber, driverLicenseNumber,
truckType, truckType,
driverSignatoryName: driverName,
}, },
lockedFields: { lockedFields: {
ownerName: Boolean(ownerName), ownerName: Boolean(ownerName),
@@ -550,38 +545,19 @@ function TruckEntranceFields({
)} )}
<Text size="sm" fw={600} mt="xs">Customs and compliance</Text> <Text size="sm" fw={600} mt="xs">Customs and compliance</Text>
<Group grow>
<TextInput
label="Declaration / Bill of Entry number"
value={value.declarationNumber}
onChange={(e) => onChange({ ...value, declarationNumber: e.currentTarget.value })}
/>
<TextInput
label="Incoterms"
value={value.incoterms}
onChange={(e) => onChange({ ...value, incoterms: e.currentTarget.value })}
/>
</Group>
<TextInput <TextInput
label="HS codes" label="Declaration / Bill of Entry number"
value={value.hsCodes} value={value.declarationNumber}
onChange={(e) => onChange({ ...value, hsCodes: e.currentTarget.value })} onChange={(e) => onChange({ ...value, declarationNumber: e.currentTarget.value })}
/> />
<Text size="sm" fw={600} mt="xs">Physical cargo specifications</Text> <Text size="sm" fw={600} mt="xs">Physical cargo specifications</Text>
<Group grow> <TextInput
<TextInput label="Item description"
label="Item code" value={value.itemDescription}
value={value.itemCode} readOnly={lockedFields?.itemDescription}
onChange={(e) => onChange({ ...value, itemCode: e.currentTarget.value })} onChange={(e) => onChange({ ...value, itemDescription: e.currentTarget.value })}
/> />
<TextInput
label="Item description"
value={value.itemDescription}
readOnly={lockedFields?.itemDescription}
onChange={(e) => onChange({ ...value, itemDescription: e.currentTarget.value })}
/>
</Group>
<Group grow> <Group grow>
<Select <Select
label="Packaging type" label="Packaging type"
@@ -626,11 +602,6 @@ function TruckEntranceFields({
onChange={(v) => onChange({ ...value, damagedRejectedQuantity: v === '' ? '' : Number(v) })} onChange={(v) => onChange({ ...value, damagedRejectedQuantity: v === '' ? '' : Number(v) })}
/> />
</Group> </Group>
<TextInput
label="Warehouse code and location"
value={value.warehouseCodeLocation}
onChange={(e) => onChange({ ...value, warehouseCodeLocation: e.currentTarget.value })}
/>
<Group grow> <Group grow>
<TextInput <TextInput
label="Driver signatory" label="Driver signatory"

View File

@@ -388,6 +388,8 @@ export interface EligibleBooking {
customerTin: string | null; customerTin: string | null;
customerPhone: string | null; customerPhone: string | null;
containerNumber: string | null; containerNumber: string | null;
/** Distinct seal numbers from the booking's container units, comma-joined. */
sealNumbers: string | null;
containerQuantity: number | null; containerQuantity: number | null;
containerPackagingType: string | null; containerPackagingType: string | null;
cargoDescription: string | null; cargoDescription: string | null;