diff --git a/src/actions/ebs-sync/ebs.js b/src/actions/ebs-sync/ebs.js index 24ba124..f2f466b 100644 --- a/src/actions/ebs-sync/ebs.js +++ b/src/actions/ebs-sync/ebs.js @@ -798,13 +798,35 @@ function resolvePaymentTerms() { return 'Immediate'; } +/** + * True when an item's custom.categories include a 'Commercial' category. + * categories should be string[] | { name, url_key }[], but custom is + * untrusted, so we defensively guard the array, the entry types, and the + * property access before comparing. + */ +function hasCommercialCategory(item) { + const categories = item.custom?.categories; + if (!Array.isArray(categories)) return false; + return categories.some((category) => { + if (typeof category === 'string') { + return category.toLowerCase() === 'commercial'; + } + if (category && typeof category === 'object') { + return typeof category.name === 'string' + && category.name.toLowerCase() === 'commercial'; + } + return false; + }); +} + /** * 'Commercial' when any item in the order is flagged as commercial - * (item.custom.isCommercial === true), otherwise 'Household'. + * (item.custom.isCommercial === true) or belongs to a 'Commercial' + * category, otherwise 'Household'. */ function resolveOrderType(order) { const hasCommercial = (order.items || []).some( - (item) => item.custom?.isCommercial === true, + (item) => item.custom?.isCommercial === true || hasCommercialCategory(item), ); return hasCommercial ? 'Commercial' : 'Household'; } diff --git a/test/ebs-sync/ebs-e2e.test.js b/test/ebs-sync/ebs-e2e.test.js index 29600cf..4c94739 100644 --- a/test/ebs-sync/ebs-e2e.test.js +++ b/test/ebs-sync/ebs-e2e.test.js @@ -758,6 +758,53 @@ describe('ebs-sync e2e', () => { await syncOrderToEbs(MOCK_CTX, MOCK_PARAMS, order, ccJournal); expect(capturedXml).toMatch(/Type="Commercial"/); }); + + test('string "Commercial" category produces Type="Commercial"', async () => { + const order = structuredClone(PP_BUNDLE_WARRANTY_ORDER); + order.items[0].custom = { categories: ['Kitchen', 'Commercial'] }; + await syncOrderToEbs(MOCK_CTX, MOCK_PARAMS, order, journal); + expect(capturedXml).toMatch(/Type="Commercial"/); + }); + + test('object "Commercial" category (name/url_key) produces Type="Commercial"', async () => { + const order = structuredClone(PP_BUNDLE_WARRANTY_ORDER); + order.items[0].custom = { + categories: [ + { name: 'Kitchen', url_key: 'kitchen' }, + { name: 'Commercial', url_key: 'commercial' }, + ], + }; + await syncOrderToEbs(MOCK_CTX, MOCK_PARAMS, order, journal); + expect(capturedXml).toMatch(/Type="Commercial"/); + }); + + test('"Commercial" category match is case-insensitive', async () => { + const order = structuredClone(PP_BUNDLE_WARRANTY_ORDER); + order.items[0].custom = { categories: ['commercial'] }; + order.items[1].custom = { categories: [{ name: 'COMMERCIAL', url_key: 'commercial' }] }; + await syncOrderToEbs(MOCK_CTX, MOCK_PARAMS, order, journal); + expect(capturedXml).toMatch(/Type="Commercial"/); + }); + + test('non-commercial categories leave Type="Household"', async () => { + const order = structuredClone(PP_BUNDLE_WARRANTY_ORDER); + order.items[0].custom = { + categories: ['Kitchen', { name: 'Household', url_key: 'household' }], + }; + await syncOrderToEbs(MOCK_CTX, MOCK_PARAMS, order, journal); + expect(capturedXml).toMatch(/Type="Household"/); + }); + + test('malformed categories are ignored and leave Type="Household"', async () => { + const order = structuredClone(PP_BUNDLE_WARRANTY_ORDER); + // categories not an array, plus null/non-object/missing-name entries + order.items[0].custom = { categories: 'Commercial' }; + order.items[1].custom = { + categories: [null, 42, { url_key: 'commercial' }, {}], + }; + await syncOrderToEbs(MOCK_CTX, MOCK_PARAMS, order, journal); + expect(capturedXml).toMatch(/Type="Household"/); + }); }); // ── Gift message ──────────────────────────────────────────────────────