import { evaluateFieldComparison, isIossEligible, isRuleRequired, mapRuleSet, } from '../shared/utils/requirementRules'; import type { Rule, RuleSet, RuleSets } from '../types'; const CAP = 12910; // EUR 150 in GBP minor units const capped = ( cap?: number ) => ( { all: [ { field: 'customs.exportReason', compare: 'Eq' as const, value: 'sale' }, ...( cap === undefined ? [] : [ { field: 'parcels.value', compare: 'Lte' as const, value: cap } ] ), ], } ); const rule = ( key: string, over: Partial< Rule > = {} ): Rule => ( { key, required: false, ...over, } ); const data = ( exportReason: string, value?: number ) => ( { customs: { exportReason }, parcels: value === undefined ? {} : { value }, } ); describe( 'evaluateFieldComparison', () => { it( 'compares strings ignoring case', () => { expect( evaluateFieldComparison( 'Sale', 'sale' ) ).toBe( true ); expect( evaluateFieldComparison( 'gift', 'sale' ) ).toBe( false ); } ); it( 'supports NotEq and lists', () => { expect( evaluateFieldComparison( 'gift', 'sale', 'NotEq' ) ).toBe( true ); expect( evaluateFieldComparison( 'gift', [ 'sale', 'Gift' ] ) ).toBe( true ); } ); it( 'treats a null comparison as matching a missing or blank field', () => { expect( evaluateFieldComparison( undefined, null ) ).toBe( true ); expect( evaluateFieldComparison( '', null ) ).toBe( true ); expect( evaluateFieldComparison( 'x', null ) ).toBe( false ); } ); it.each( [ [ 100, 'Lte', 100, true ], [ 101, 'Lte', 100, false ], [ 99, 'Lt', 100, true ], [ 100, 'Lt', 100, false ], [ 101, 'Gt', 100, true ], [ 100, 'Gte', 100, true ], ] as const )( 'orders %s %s %s as %s', ( field, op, bound, expected ) => { expect( evaluateFieldComparison( field, bound, op ) ).toBe( expected ); } ); it( 'never satisfies an ordering with a missing or non-numeric field', () => { expect( evaluateFieldComparison( undefined, 100, 'Lte' ) ).toBe( false ); expect( evaluateFieldComparison( '50', 100, 'Lte' ) ).toBe( false ); } ); it( 'needs an ordering to hold against every listed value', () => { expect( evaluateFieldComparison( 5, [ 10, 3 ], 'Lte' ) ).toBe( false ); expect( evaluateFieldComparison( 5, [ 10, 7 ], 'Lte' ) ).toBe( true ); } ); } ); describe( 'isRuleRequired', () => { const merchant = rule( 'merchantProductId', { metadata: { requiredIf: capped( CAP ) }, } ); it( 'is required for a sale at or under the cap', () => { expect( isRuleRequired( merchant, data( 'Sale', 5000 ) ) ).toBe( true ); expect( isRuleRequired( merchant, data( 'Sale', CAP ) ) ).toBe( true ); } ); it( 'is not required for a gift or above the cap', () => { expect( isRuleRequired( merchant, data( 'Gift', 5000 ) ) ).toBe( false ); expect( isRuleRequired( merchant, data( 'Sale', CAP + 1 ) ) ).toBe( false ); } ); it( 'treats a missing parcel value as unsatisfied, not as zero', () => { expect( isRuleRequired( merchant, data( 'Sale' ) ) ).toBe( false ); } ); it( "keeps a rule's own required flag", () => { expect( isRuleRequired( { ...merchant, required: true }, data( 'Gift', 1 ) ) ).toBe( true ); } ); it( 'without a requiredIf is whatever required says', () => { expect( isRuleRequired( rule( 'standardProductId' ), data( 'Sale', 1 ) ) ).toBe( false ); } ); it( 'evaluates the sale clause alone when no cap was sent', () => { const noCap = rule( 'merchantProductId', { metadata: { requiredIf: capped() }, } ); expect( isRuleRequired( noCap, data( 'Sale', 9999999 ) ) ).toBe( true ); } ); it( 'inherits a value from the enclosing condition', () => { const inherited = rule( 'x', { metadata: { requiredIf: { value: 'sale', any: [ { field: 'customs.exportReason' } ], }, }, } ); expect( isRuleRequired( inherited, data( 'Sale', 1 ) ) ).toBe( true ); expect( isRuleRequired( inherited, data( 'Gift', 1 ) ) ).toBe( false ); } ); } ); describe( 'mapRuleSet', () => { const ruleSet: RuleSet = { displayName: 'Parcel content', rules: [ rule( 'description' ), rule( 'merchantProductId', { metadata: { requiredIf: capped( CAP ) } } ), rule( 'standardProductId' ), ], }; it( 'evaluates every rule and leaves the rest of the ruleset alone', () => { const mapped = mapRuleSet( ruleSet, data( 'Sale', 1000 ) ); expect( mapped?.rules.map( ( r ) => [ r.key, r.required ] ) ).toEqual( [ [ 'description', false ], [ 'merchantProductId', true ], [ 'standardProductId', false ], ] ); expect( mapped?.displayName ).toBe( 'Parcel content' ); } ); it( 'is undefined without a ruleset', () => { expect( mapRuleSet( undefined, {} ) ).toBeUndefined(); } ); } ); describe( 'isIossEligible', () => { const withIoss = ( maxParcelValue?: number ): RuleSets => ( { customs: { rules: [ rule( 'exportReason' ), rule( 'ioss', { metadata: maxParcelValue === undefined ? {} : { maxParcelValue } } ), ], }, } ); it( 'is eligible at or under the cap the API sends', () => { expect( isIossEligible( withIoss( CAP ), 5000 ) ).toBe( true ); expect( isIossEligible( withIoss( CAP ), CAP ) ).toBe( true ); } ); it( 'is not eligible above the cap', () => { expect( isIossEligible( withIoss( CAP ), CAP + 1 ) ).toBe( false ); } ); it( 'does not use a fixed 150 when the order is a different size to the API cap', () => { // 140 in the order's units is under a flat 150 but over the real cap of 129.10. expect( isIossEligible( withIoss( CAP ), 14000 ) ).toBe( false ); } ); it( 'is not eligible when the lane has no ioss rule, no cap, or no requirements', () => { expect( isIossEligible( withIoss(), 5000 ) ).toBe( false ); expect( isIossEligible( { customs: { rules: [ rule( 'exportReason' ) ] } }, 5000 ) ).toBe( false ); expect( isIossEligible( {}, 5000 ) ).toBe( false ); expect( isIossEligible( undefined, 5000 ) ).toBe( false ); expect( isIossEligible( null, 5000 ) ).toBe( false ); } ); } );