import { resolveParcel, itemUnitWeights, estimateItemsSize } from '../shared/utils/parcel'; import { orderToCheckoutValidatePayload } from '../features/order-ship/utils/orderToCheckoutValidatePayload'; import ordersToCheckoutValidatePayload from '../features/orders/utils/ordersToCheckoutValidatePayload'; import { buildQuotePayloadFromParcel } from '../features/order-ship/utils/orderToQuotePayload'; import type { Order, ParcelDimensions, PluginSettings, Quote } from '../types'; const quote = { service: { slug: 'service-slug' }, tags: [], availableExtras: [], dates: [], } as unknown as Quote; const makeSettings = ( over: object = {} ) => ( { shipping: { country: 'GB', countryName: 'United Kingdom', contactName: 'Shop', email: 's@example.com', town: 'London', property: '1', postcode: 'SW1A 1AA', street: 'Street', }, tax: {}, packages: [], defaultPackageId: '', overrides: { defaultWeightKg: 2 }, ...over, } ) as unknown as PluginSettings; const item = ( weight: number | string | null, quantity = 1, over: object = {} ) => ( { product_id: 1, variation_id: 0, name: 'Ceramic mug', quantity, total: '20.00', dimensions: { length: '', width: '', height: '', weight }, tariff_code: '691110', country_of_manufacture: 'GB', ...over, } ); const makeOrder = ( items: object[], parcel: ParcelDimensions | null = null, units = { weight: 'kg', dimension: 'cm' } ) => ( { id: 1502, shipping: { name: 'Test Customer', address_1: '1 High Street', city: 'Leeds', postcode: 'LS1 1AA', country: 'GB', phone: '', }, customer: { name: 'Test Customer', email: 'test@example.com' }, items, units, parcel, cheapestQuote: quote, } ) as unknown as Order; // ------------------------------------------------------------------------- describe( 'resolveParcel', () => { it( 'uses the weight entered in Packaging first', () => { const parcel = { length: 30, width: 20, height: 10, weight: 1 }; const result = resolveParcel( makeOrder( [ item( 3, 2 ) ], parcel ), makeSettings() ); expect( result ).toMatchObject( { ...parcel, weightSource: 'packaging' } ); } ); it( 'otherwise adds up the product weights by quantity', () => { const result = resolveParcel( makeOrder( [ item( 1.5, 2 ), item( 0.5, 1 ) ] ), makeSettings() ); expect( result.weight ).toBeCloseTo( 3.5 ); expect( result.weightSource ).toBe( 'products' ); expect( result.missingWeightItems ).toEqual( [] ); } ); it( 'counts the Default Weight per unit for each product without a weight', () => { const result = resolveParcel( makeOrder( [ item( '', 2 ), item( null, 1 ) ] ), makeSettings() ); expect( result.weight ).toBe( 6 ); expect( result.weightSource ).toBe( 'default' ); expect( result.defaultWeight ).toBe( 2 ); expect( result.missingWeightItems ).toHaveLength( 2 ); } ); it( 'falls back to 1 kg in store units when there is no Default Weight', () => { const settings = makeSettings( { overrides: {} } ); expect( resolveParcel( makeOrder( [ item( '' ) ] ), settings ).weight ).toBe( 1 ); const inGrams = makeOrder( [ item( '' ) ], null, { weight: 'g', dimension: 'cm' } ); expect( resolveParcel( inGrams, settings ).weight ).toBeCloseTo( 1000 ); } ); it( 'adds the Default Weight for the products without one to the others', () => { const result = resolveParcel( makeOrder( [ item( 1 ), item( '', 1, { name: 'Tea towel' } ) ] ), makeSettings() ); expect( result.weight ).toBe( 3 ); expect( result.weightSource ).toBe( 'default' ); expect( result.missingWeightItems ).toEqual( [ 'Tea towel' ] ); } ); it( 'takes the size from the products, then the default package, then 30×20×15 cm', () => { const sized = makeOrder( [ item( 1, 1, { dimensions: { length: 40, width: 30, height: 5, weight: 1 } } ), ] ); // An item without all three sides gives no size. const unsized = makeOrder( [ item( 1, 1, { dimensions: { length: 40, width: '', height: '', weight: 1 } } ), ] ); const settings = makeSettings( { packages: [ { id: 'pkg_1', name: 'Box', length: 25, width: 15, height: 0, weight: 0 } ], defaultPackageId: 'pkg_1', } ); expect( resolveParcel( sized, settings ) ).toMatchObject( { length: 40, width: 30, height: 5 } ); expect( resolveParcel( unsized, settings ) ).toMatchObject( { length: 25, width: 15, height: 15 } ); expect( resolveParcel( unsized, makeSettings() ) ).toMatchObject( { length: 30, width: 20, height: 15, } ); } ); it( 'converts the 30×20×15 cm fallback to the store unit', () => { const order = makeOrder( [ item( 1 ) ], null, { weight: 'kg', dimension: 'mm' } ); expect( resolveParcel( order, makeSettings() ) ).toMatchObject( { length: 300, width: 200, height: 150, } ); } ); it( 'works without settings', () => { expect( resolveParcel( makeOrder( [ item( '' ) ] ), null ).weight ).toBe( 1 ); } ); } ); // ------------------------------------------------------------------------- describe( 'itemUnitWeights', () => { it( 'keeps each product weight', () => { const order = makeOrder( [ item( 1, 2 ), item( 0.5, 1 ) ] ); expect( itemUnitWeights( order, makeSettings() ) ).toEqual( [ 1, 0.5 ] ); } ); it( 'uses the Default Weight for a product without one, never a share of another', () => { const order = makeOrder( [ item( 1, 1 ), item( '', 2 ) ] ); expect( itemUnitWeights( order, makeSettings() ) ).toEqual( [ 1, 2 ] ); } ); it( 'uses 1 kg when there is no Default Weight', () => { const order = makeOrder( [ item( '', 1 ) ] ); expect( itemUnitWeights( order, makeSettings( { overrides: {} } ) ) ).toEqual( [ 1 ] ); } ); } ); // ------------------------------------------------------------------------- describe( 'booking payloads use the resolved parcel', () => { it( 'single order: books the Packaging parcel for products without weights', () => { const parcel = { length: 30, width: 20, height: 10, weight: 1 }; const payload = orderToCheckoutValidatePayload( makeOrder( [ item( '', 2 ) ], parcel ), makeSettings(), quote, [] ); const sent = payload?.items[ 0 ].parcels[ 0 ]; expect( sent ).toMatchObject( parcel ); // The entered parcel weight does not change the item weights. expect( sent?.contents?.[ 0 ].weight ).toBe( 2 ); } ); it( 'single order: keeps a weighted item at its own weight next to one without', () => { const payload = orderToCheckoutValidatePayload( makeOrder( [ item( 0.5, 1, { name: 'Cookies' } ), item( '', 1, { name: 'Pouch' } ) ] ), makeSettings(), quote, [] ); const sent = payload?.items[ 0 ].parcels[ 0 ]; expect( sent?.weight ).toBe( 2.5 ); expect( sent?.contents?.map( ( c ) => c.weight ) ).toEqual( [ 0.5, 2 ] ); } ); it( 'single order: books the Default Weight when nothing else is set', () => { const payload = orderToCheckoutValidatePayload( makeOrder( [ item( '' ) ] ), makeSettings(), quote, [] ); expect( payload?.items[ 0 ].parcels[ 0 ].weight ).toBe( 2 ); } ); it( 'bulk: never books a parcel weight of 0', () => { const payload = ordersToCheckoutValidatePayload( { orders: [ makeOrder( [ item( '', 1 ) ] ) ], settings: makeSettings(), } ); const sent = payload?.items[ 0 ].parcels[ 0 ]; expect( sent?.weight ).toBe( 2 ); expect( sent?.contents?.[ 0 ].weight ).toBe( 2 ); } ); it( 'bulk: still books the product weights when they are set', () => { const payload = ordersToCheckoutValidatePayload( { orders: [ makeOrder( [ item( 0.75, 2 ) ] ) ], settings: makeSettings(), } ); expect( payload?.items[ 0 ].parcels[ 0 ].weight ).toBe( 1.5 ); } ); it( 'quotes the same single parcel that is booked', () => { const parcel = { length: 30.4, width: 20, height: 10, weight: 1.25 }; const order = makeOrder( [ item( '', 2 ), item( 3, 1 ) ], parcel ); const quotePayload = buildQuotePayloadFromParcel( order, makeSettings().shipping as any, resolveParcel( order, makeSettings() ) ); expect( quotePayload?.parcels ).toEqual( [ { ref: '1502', weight: 1.25, length: 30.4, width: 20, height: 10, value: 4000 }, ] ); const booked = orderToCheckoutValidatePayload( order, makeSettings(), quote, [] ); expect( booked?.items[ 0 ].parcels[ 0 ] ).toMatchObject( { weight: 1.25, length: 30.4, width: 20, height: 10, } ); } ); it( 'leaves rounding to the server, so fractional store units match the booking', () => { // A metre store: the 30×20×15 cm fallback is 0.3×0.2×0.15 m and must not be rounded. const order = makeOrder( [ item( 1 ) ], null, { weight: 'kg', dimension: 'm' } ); const parcel = resolveParcel( order, makeSettings() ); const quoted = buildQuotePayloadFromParcel( order, makeSettings().shipping as any, parcel ); const booked = orderToCheckoutValidatePayload( order, makeSettings(), quote, [] ); expect( quoted?.parcels[ 0 ] ).toMatchObject( { length: 0.3, width: 0.2, height: 0.15 } ); expect( booked?.items[ 0 ].parcels[ 0 ] ).toMatchObject( { length: 0.3, width: 0.2, height: 0.15 } ); } ); } ); // ------------------------------------------------------------------------- describe( 'estimateItemsSize', () => { const sized = ( l: number, w: number, h: number, quantity = 1 ) => item( 1, quantity, { dimensions: { length: l, width: w, height: h, weight: 1 } } ); it( 'keeps a single item as it is', () => { expect( estimateItemsSize( makeOrder( [ sized( 30, 20, 10 ) ] ) ) ).toEqual( { length: 30, width: 20, height: 10, } ); } ); it( 'stacks the quantity, so two of an item need twice the height', () => { expect( estimateItemsSize( makeOrder( [ sized( 30, 20, 10, 2 ) ] ) ) ).toEqual( { length: 30, width: 20, height: 20, } ); } ); it( 'stacks a smaller item on one that fills the footprint', () => { // Mini Pouch 10×10×10 fills the 10×10 footprint, so Cookies 10×5×2 goes on top: 12 cm, not 11. expect( estimateItemsSize( makeOrder( [ sized( 10, 10, 10 ), sized( 10, 5, 2 ) ] ) ) ).toEqual( { length: 10, width: 10, height: 12 } ); } ); it( 'does not care which way round the sides were entered', () => { expect( estimateItemsSize( makeOrder( [ sized( 30, 10, 5 ), sized( 10, 5, 30 ) ] ) ) ).toEqual( { length: 30, width: 10, height: 10 } ); } ); it( 'stacks every item on the widest footprint, so the box is never too short', () => { // A flat 40×40×2 item covers the footprint, so the 12×10×10 item lies on it: 12 cm. expect( estimateItemsSize( makeOrder( [ sized( 40, 40, 2 ), sized( 12, 10, 10 ) ] ) ) ).toEqual( { length: 40, width: 40, height: 12 } ); } ); it( 'drops float noise when adding up decimal sides', () => { expect( estimateItemsSize( makeOrder( [ sized( 0.3, 0.2, 0.1 ), sized( 0.3, 0.2, 0.2 ) ] ) ) ).toEqual( { length: 0.3, width: 0.2, height: 0.3 } ); } ); it( 'leaves out items without all three sides, and gives null when none has a size', () => { const partial = item( 1, 1, { dimensions: { length: 50, width: '', height: '', weight: 1 } } ); expect( estimateItemsSize( makeOrder( [ sized( 30, 20, 10 ), partial ] ) ) ).toEqual( { length: 30, width: 20, height: 10, } ); expect( estimateItemsSize( makeOrder( [ partial ] ) ) ).toBeNull(); } ); } );