import { deriveProductIdentifierLines, getIdentifierLabelForField, isPlaceholderIdentifier, productIdentifierProblemMessage, setItemIdentifier, validateProductIdentifier, } from '../shared/utils/productIdentifierLines'; import type { Order, Rule, RuleSets } from '../types'; const CAP = 12910; const capped = { all: [ { field: 'customs.exportReason', compare: 'Eq' as const, value: 'sale' }, { field: 'parcels.value', compare: 'Lte' as const, value: CAP }, ], }; const rule = ( key: string, requiredIf = false ): Rule => ( { key, required: false, metadata: { maxParcelValue: CAP, ...( requiredIf && { requiredIf: capped } ) }, } ); /** What the API returns for an EU lane with the setting on. */ const euRules: RuleSets = { parcelContent: { rules: [ rule( 'description' ), rule( 'merchantProductId', true ), rule( 'manufacturerProductId', true ), rule( 'standardProductId' ), ], }, }; const item = ( over: object = {} ) => ( { product_id: 1, variation_id: 0, name: 'Ceramic mug', quantity: 1, dimensions: { length: 1, width: 1, height: 1, weight: 1 }, product_identifiers: {}, ...over, } ); // ------------------------------------------------------------------------- describe( 'validateProductIdentifier', () => { it( 'accepts an empty optional value, rejects an empty required one', () => { expect( validateProductIdentifier( undefined, false ) ).toBeUndefined(); expect( validateProductIdentifier( ' ', false ) ).toBeUndefined(); expect( validateProductIdentifier( undefined, true ) ).toBe( 'required' ); expect( validateProductIdentifier( ' ', true ) ).toBe( 'required' ); } ); it.each( [ 'none', 'None', ' N/A ', 'n.a.', 'NA', 'no code', 'No', 'unknown', 'TBC', '-', '...' ] )( 'rejects the placeholder %j on a required field', ( value ) => { expect( validateProductIdentifier( value, true ) ).toBe( 'placeholder' ); } ); it( 'does not police placeholders on an optional field', () => { expect( validateProductIdentifier( 'none', false ) ).toBeUndefined(); } ); it.each( [ 'SKU-1234', 'M-PID', 'NS-PID', '5012345678900', 'nonesuch-42' ] )( 'accepts %j', ( value ) => { expect( validateProductIdentifier( value, true ) ).toBeUndefined(); } ); it.each( [ '商品', 'ТОВАР', 'Ñandú', 'カップ123', 'منتج', 'ΠΡΟΪΟΝ' ] )( 'does not mistake the non-Latin code %j for a placeholder', ( value ) => { expect( isPlaceholderIdentifier( value ) ).toBe( false ); expect( validateProductIdentifier( value, true ) ).toBeUndefined(); } ); it( 'still treats wide punctuation as a placeholder', () => { expect( isPlaceholderIdentifier( '。。。' ) ).toBe( true ); } ); it.each( [ 'A|B', 'A:B' ] )( 'rejects the delimiter in %j on any field', ( value ) => { expect( validateProductIdentifier( value, true ) ).toBe( 'forbidden-chars' ); expect( validateProductIdentifier( value, false ) ).toBe( 'forbidden-chars' ); } ); } ); // ------------------------------------------------------------------------- describe( 'deriveProductIdentifierLines', () => { const derive = ( items: object[], ruleSets: RuleSets | undefined, parcelValue = 5000 ) => deriveProductIdentifierLines( { items: items as never, ruleSets, parcelValue } ); it( 'shows nothing without rules or requirements', () => { expect( derive( [ item() ], undefined ) ).toEqual( { lines: [], blocking: false } ); expect( derive( [ item() ], { parcelContent: { rules: [ rule( 'description' ) ] } } ).lines ).toEqual( [] ); } ); it( 'shows nothing above the cap', () => { const result = derive( [ item() ], euRules, CAP + 1 ); expect( result.lines ).toEqual( [] ); expect( result.blocking ).toBe( false ); } ); it( 'shows the three fields in the order the API sends them, two required', () => { const { lines } = derive( [ item() ], euRules ); expect( lines ).toHaveLength( 1 ); expect( lines[ 0 ] ).toMatchObject( { itemIndex: 0, name: 'Ceramic mug' } ); expect( lines[ 0 ].fields.map( ( f ) => [ f.key, f.required ] ) ).toEqual( [ [ 'merchantProductId', true ], [ 'manufacturerProductId', true ], [ 'standardProductId', false ], ] ); } ); it( 'prefills from the order item', () => { const { lines } = derive( [ item( { product_identifiers: { merchant_product_id: 'PID-A', manufacturer_product_id: 'MFR-1', standard_product_id: '4006381333931' } } ) ], euRules ); expect( lines[ 0 ].fields.map( ( f ) => f.value ) ).toEqual( [ 'PID-A', 'MFR-1', '4006381333931' ] ); } ); it( 'blocks while a required field is empty, and not once both hold real values', () => { const some = derive( [ item( { product_identifiers: { merchant_product_id: 'PID-B' } } ) ], euRules ); expect( some.blocking ).toBe( true ); expect( some.lines[ 0 ].fields[ 1 ].problem ).toBe( 'required' ); const all = derive( [ item( { product_identifiers: { merchant_product_id: 'PID-B', manufacturer_product_id: 'MFR-1' } } ) ], euRules ); expect( all.blocking ).toBe( false ); } ); it( 'blocks on a placeholder or a delimiter, but not on an optional barcode placeholder', () => { const ids = ( over: object ) => ( { merchant_product_id: 'PID', manufacturer_product_id: 'MFR', ...over } ); expect( derive( [ item( { product_identifiers: ids( { merchant_product_id: 'none' } ) } ) ], euRules ).blocking ).toBe( true ); expect( derive( [ item( { product_identifiers: ids( { manufacturer_product_id: 'A|B' } ) } ) ], euRules ).blocking ).toBe( true ); expect( derive( [ item( { product_identifiers: ids( { standard_product_id: 'none' } ) } ) ], euRules ).blocking ).toBe( false ); } ); it( 'shows every item that needs codes, keeping its index', () => { const { lines } = derive( [ item( { name: 'Mug' } ), item( { name: 'Spoon' } ) ], euRules ); expect( lines.map( ( l ) => [ l.itemIndex, l.name ] ) ).toEqual( [ [ 0, 'Mug' ], [ 1, 'Spoon' ] ] ); } ); it( 'hides the fields above the cap even when one holds a value, which stays on the item', () => { const held = item( { product_identifiers: { merchant_product_id: 'PID-A' } } ); const { lines, blocking } = derive( [ held ], euRules, CAP + 1 ); expect( lines ).toEqual( [] ); expect( blocking ).toBe( false ); // Still on the item, so it is still sent with the order. expect( held.product_identifiers ).toEqual( { merchant_product_id: 'PID-A' } ); } ); it( 'hides the fields when the API does not require them, however many hold a value', () => { const notRequired: RuleSets = { parcelContent: { rules: [ 'merchantProductId', 'manufacturerProductId', 'standardProductId' ].map( ( key ) => ( { key, required: false, metadata: { maxParcelValue: CAP }, } ) ) as Rule[], }, }; const { lines } = derive( [ item( { product_identifiers: { merchant_product_id: 'PID-B' } } ) ], notRequired ); expect( lines ).toEqual( [] ); } ); it( 'copes with an item that has no identifiers at all', () => { const { lines } = derive( [ item( { product_identifiers: undefined } ) ], euRules ); expect( lines[ 0 ].fields.every( ( f ) => f.value === '' ) ).toBe( true ); } ); } ); // ------------------------------------------------------------------------- describe( 'setItemIdentifier', () => { const order = { id: 1, items: [ item( { product_identifiers: { merchant_product_id: 'OLD', standard_product_id: '4006381333931' } } ), item( { name: 'Spoon' } ), ], } as unknown as Order; it( 'sets a trimmed value on one item', () => { const next = setItemIdentifier( order, 1, 'manufacturerProductId', ' MFR-1 ' ); expect( next.items[ 1 ].product_identifiers?.manufacturer_product_id ).toBe( 'MFR-1' ); } ); it( 'replaces a value and keeps the item\'s other identifiers', () => { const next = setItemIdentifier( order, 0, 'merchantProductId', 'NEW' ); expect( next.items[ 0 ].product_identifiers ).toEqual( { merchant_product_id: 'NEW', standard_product_id: '4006381333931', } ); } ); it( 'stores an empty string for a blank value, meaning none', () => { const next = setItemIdentifier( order, 0, 'merchantProductId', ' ' ); expect( next.items[ 0 ].product_identifiers?.merchant_product_id ).toBe( '' ); } ); it( 'leaves the other items and the original order alone', () => { const snapshot = JSON.stringify( order ); const next = setItemIdentifier( order, 0, 'merchantProductId', 'X' ); expect( next.items[ 1 ] ).toBe( order.items[ 1 ] ); expect( JSON.stringify( order ) ).toBe( snapshot ); } ); it( 'returns the order unchanged for an item that does not exist', () => { expect( setItemIdentifier( order, 9, 'merchantProductId', 'X' ) ).toBe( order ); } ); } ); // ------------------------------------------------------------------------- describe( 'productIdentifierProblemMessage', () => { it( 'names the field when it is required', () => { expect( productIdentifierProblemMessage( 'Item SKU', 'required' ) ).toBe( 'Item SKU is required' ); } ); it( 'explains a placeholder and a delimiter', () => { expect( productIdentifierProblemMessage( 'Item SKU', 'placeholder' ) ).toMatch( /placeholder/i ); expect( productIdentifierProblemMessage( 'Item SKU', 'forbidden-chars' ) ).toContain( '|' ); } ); } ); // ------------------------------------------------------------------------- describe( 'getIdentifierLabelForField', () => { it( 'names each identifier the way the form does', () => { expect( getIdentifierLabelForField( 'merchantProductId' ) ).toBe( 'Item SKU' ); expect( getIdentifierLabelForField( 'manufacturerProductId' ) ).toBe( 'Manufacturer ID' ); expect( getIdentifierLabelForField( 'standardProductId' ) ).toBe( 'Barcode' ); } ); it( 'ignores case, as the API pointer does', () => { expect( getIdentifierLabelForField( 'MANUFACTURERPRODUCTID' ) ).toBe( 'Manufacturer ID' ); } ); it( 'is null for any other field', () => { expect( getIdentifierLabelForField( 'tariffCode' ) ).toBeNull(); expect( getIdentifierLabelForField( '' ) ).toBeNull(); } ); } );