mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #1179 from Tria-plc/eims-integration
fix: stamp export self-haul truck arrival on receive to warehouse
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { WarehouseInventoryService } from './warehouse-inventory.service';
|
||||
|
||||
/**
|
||||
* Export self-haul has no separate "truck arrived" gate action the way import
|
||||
* does (release()'s arrival branch, fired later when a truck shows up to
|
||||
* COLLECT already-warehoused goods) — the truck delivering cargo TO the
|
||||
* warehouse arrives and is received in the same act, so receive()/
|
||||
* bulkReceive() must stamp customer_truck_assignments.arrived_at themselves.
|
||||
*/
|
||||
type Marker = (
|
||||
manager: { query: jest.Mock },
|
||||
bookingId: string,
|
||||
plateNumber: string | null | undefined,
|
||||
) => Promise<void>;
|
||||
|
||||
function makeMarker() {
|
||||
const service = Object.create(WarehouseInventoryService.prototype) as Record<string, unknown>;
|
||||
const marker = (
|
||||
service as unknown as { markCustomerTruckArrived: Marker }
|
||||
).markCustomerTruckArrived.bind(service);
|
||||
return marker;
|
||||
}
|
||||
|
||||
describe('markCustomerTruckArrived', () => {
|
||||
it('stamps arrival matched by booking + plate', async () => {
|
||||
const marker = makeMarker();
|
||||
const manager = { query: jest.fn().mockResolvedValue(undefined) };
|
||||
|
||||
await marker(manager, 'b-1', 'AAA-2323');
|
||||
|
||||
expect(manager.query).toHaveBeenCalledTimes(1);
|
||||
const [sql, params] = manager.query.mock.calls[0];
|
||||
expect(sql).toMatch(/UPDATE freight\.customer_truck_assignments/);
|
||||
expect(sql).toMatch(/UPPER\(a\.plate_number\) = UPPER\(\$2\)/);
|
||||
expect(params).toEqual(['b-1', 'AAA-2323']);
|
||||
});
|
||||
|
||||
it('trims the plate before matching', async () => {
|
||||
const marker = makeMarker();
|
||||
const manager = { query: jest.fn().mockResolvedValue(undefined) };
|
||||
|
||||
await marker(manager, 'b-1', ' AAA-2323 ');
|
||||
|
||||
expect(manager.query.mock.calls[0][1]).toEqual(['b-1', 'AAA-2323']);
|
||||
});
|
||||
|
||||
it('no-ops on a missing/blank plate — no query, nothing to match on', async () => {
|
||||
const marker = makeMarker();
|
||||
const manager = { query: jest.fn() };
|
||||
|
||||
await marker(manager, 'b-1', undefined);
|
||||
await marker(manager, 'b-1', ' ');
|
||||
|
||||
expect(manager.query).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1643,6 +1643,12 @@ export class WarehouseInventoryService {
|
||||
[bookingId],
|
||||
);
|
||||
|
||||
// Export self-haul: this receive IS the truck's arrival — see
|
||||
// markCustomerTruckArrived / receive()'s single-booking mirror.
|
||||
if (dto.direction === 'EXPORT') {
|
||||
await this.markCustomerTruckArrived(manager, bookingId, truckEntrance?.truckPlateNumber);
|
||||
}
|
||||
|
||||
await this.activityLog.record(
|
||||
{
|
||||
activityType: 'INVENTORY_RECEIVED',
|
||||
@@ -2886,6 +2892,13 @@ export class WarehouseInventoryService {
|
||||
);
|
||||
}
|
||||
|
||||
// Export self-haul: this receive IS the truck's arrival — stamp it on
|
||||
// its own customer_truck_assignments row (mirror of import's arrival,
|
||||
// see markCustomerTruckArrived).
|
||||
if (dto.bookingId && bookingDirection === 'EXPORT') {
|
||||
await this.markCustomerTruckArrived(manager, dto.bookingId, truckEntrance.truckPlateNumber);
|
||||
}
|
||||
|
||||
await this.activityLog.record(
|
||||
{
|
||||
activityType: 'INVENTORY_RECEIVED',
|
||||
@@ -6008,6 +6021,33 @@ export class WarehouseInventoryService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPORT self-haul mirror of the customer truck lifecycle IMPORT already has:
|
||||
* import stamps a truck's arrival on the SEPARATE gate action that comes
|
||||
* later (release()'s arrival branch, when the customer's truck shows up to
|
||||
* COLLECT already-warehoused goods). Export has no such separate step — the
|
||||
* truck delivering cargo TO the warehouse arrives and is received in the
|
||||
* same act, so receive()/bulkReceive() themselves are the arrival event.
|
||||
* Matched by plate (not assignmentId — neither receive endpoint carries
|
||||
* one), same as release()'s departure-branch self-haul UPDATE.
|
||||
*/
|
||||
private async markCustomerTruckArrived(
|
||||
manager: EntityManager,
|
||||
bookingId: string,
|
||||
plateNumber: string | null | undefined,
|
||||
): Promise<void> {
|
||||
const plate = plateNumber?.trim();
|
||||
if (!plate) return;
|
||||
await manager.query(
|
||||
`UPDATE freight.customer_truck_assignments a
|
||||
SET arrived_at = COALESCE(a.arrived_at, NOW()), updated_at = NOW()
|
||||
WHERE a.booking_id = $1
|
||||
AND UPPER(a.plate_number) = UPPER($2)
|
||||
AND a.deleted_at IS NULL`,
|
||||
[bookingId, plate],
|
||||
);
|
||||
}
|
||||
|
||||
private async getBookingTruckEntranceSource(
|
||||
manager: EntityManager,
|
||||
bookingId: string,
|
||||
@@ -6027,6 +6067,10 @@ export class WarehouseInventoryService {
|
||||
firstMileDriverPhone?: string | null;
|
||||
firstMileDriverLicenseNumber?: string | null;
|
||||
firstMileTruckType?: string | null;
|
||||
customerTruckPlateNumber?: string | null;
|
||||
customerTruckDriverName?: string | null;
|
||||
customerTruckType?: string | null;
|
||||
customerTruckContainerNumber?: string | null;
|
||||
}> {
|
||||
const [booking] = await manager.query(
|
||||
`SELECT b.reference AS "reference",
|
||||
@@ -6046,7 +6090,24 @@ export class WarehouseInventoryService {
|
||||
) AS "firstMileDriverName",
|
||||
driver.phone_number AS "firstMileDriverPhone",
|
||||
driver.license_number AS "firstMileDriverLicenseNumber",
|
||||
v.vehicle_type AS "firstMileTruckType"
|
||||
v.vehicle_type AS "firstMileTruckType",
|
||||
-- Self-haul truck assigned via the portal — export delivering to
|
||||
-- the warehouse or import collecting from it. Same pattern as
|
||||
-- eligibleBookings/importQueueByStatuses: multi-truck self-haul
|
||||
-- writes plates/drivers to customer_truck_assignments and leaves
|
||||
-- the booking columns null, so read the assignments first and
|
||||
-- keep the legacy column as the fallback for single-truck
|
||||
-- bookings written before that table existed.
|
||||
COALESCE((SELECT string_agg(DISTINCT cta.plate_number, ', ')
|
||||
FROM freight.customer_truck_assignments cta
|
||||
WHERE cta.booking_id = b.id AND cta.deleted_at IS NULL),
|
||||
b.customer_truck_plate_number) AS "customerTruckPlateNumber",
|
||||
COALESCE((SELECT string_agg(DISTINCT cta.driver_name, ', ')
|
||||
FROM freight.customer_truck_assignments cta
|
||||
WHERE cta.booking_id = b.id AND cta.deleted_at IS NULL),
|
||||
b.customer_truck_driver_name) AS "customerTruckDriverName",
|
||||
b.customer_truck_type AS "customerTruckType",
|
||||
b.customer_truck_container_number AS "customerTruckContainerNumber"
|
||||
FROM freight.bookings b
|
||||
LEFT JOIN freight.companies company ON company.id = b.company_id
|
||||
${primaryContactUserJoin('company')}
|
||||
|
||||
Reference in New Issue
Block a user