Files
edr-platform/apps/edr-freight-api/src/modules/warehouses/double-handling-gate.spec.ts
Hagernesh 6983645347 feat(warehouses): gate double-handling fee on a per-booking Yes/No
Double handling billed every import with a matching rule. Add
bookings.double_handling (+ set_at/by), charge only when Yes, expose a
PATCH endpoint (import-only, locked after invoicing, audited) and Yes/No
items in the inventory row menu.
2026-07-25 10:45:34 +00:00

62 lines
2.3 KiB
TypeScript

import { WarehouseFeeService } from './warehouse-fee.service';
/**
* Double handling bills ONLY when warehouse staff answered Yes after
* unloading. Undecided (null) or No must produce a zero charge even when a
* matching DOUBLE_HANDLING_FEE rule exists.
*/
type Item = Parameters<WarehouseFeeService['previewForInventory']> extends unknown
? Record<string, unknown>
: never;
const svc = Object.create(WarehouseFeeService.prototype) as {
computeDoubleHandling: (
rule: Record<string, unknown> | null,
item: Item,
now: Date,
billingCurrency: string,
) => Promise<{ amount: number; billableUnits: number }>;
normalizeCurrency: (c?: string | null) => string;
convertAmount: (a: number, from: string, to: string) => Promise<number>;
resolveBulkQuantity: (item: Item) => { quantity: number; unitLabel: string };
};
// No exchange service on a bare prototype — bill in the rule's own currency.
svc.normalizeCurrency = (c) => (c ? String(c).toUpperCase() : 'USD');
svc.convertAmount = async (a) => a;
const rule = { basis: 'PER_CONTAINER', ratePerDay: 100, currency: 'USD', id: 'r1', name: 'DH' };
const item = (doubleHandling: boolean | null) => ({
tradeDirection: 'IMPORT',
freightType: 'CONTAINER',
inventoryQuantity: 2,
bookingContainerCount: 3,
inventoryWeight: 10,
cargoUnitOfMeasure: 'PER_TON',
doubleHandling,
}) as unknown as Item;
describe('double handling gate', () => {
it('bills rate x containers when the booking is flagged Yes', async () => {
const out = await svc.computeDoubleHandling(rule, item(true), new Date(), 'USD');
expect(out.billableUnits).toBe(3);
expect(out.amount).toBe(300);
});
it('charges nothing when the answer is No', async () => {
const out = await svc.computeDoubleHandling(rule, item(false), new Date(), 'USD');
expect(out.billableUnits).toBe(0);
expect(out.amount).toBe(0);
});
it('charges nothing while the answer is undecided', async () => {
const out = await svc.computeDoubleHandling(rule, item(null), new Date(), 'USD');
expect(out.amount).toBe(0);
});
it('charges nothing for export even when flagged Yes', async () => {
const exportItem = { ...(item(true) as Record<string, unknown>), tradeDirection: 'EXPORT' } as Item;
const out = await svc.computeDoubleHandling(rule, exportItem, new Date(), 'USD');
expect(out.amount).toBe(0);
});
});