diff --git a/.changeset/cool-swans-wonder.md b/.changeset/cool-swans-wonder.md new file mode 100644 index 000000000..86ade9126 --- /dev/null +++ b/.changeset/cool-swans-wonder.md @@ -0,0 +1,5 @@ +--- +"shopify-buy": patch +--- + +Fix bug where a shipping discount could appear as if it was a line item discount diff --git a/.changeset/tough-phones-sip.md b/.changeset/tough-phones-sip.md new file mode 100644 index 000000000..49de36a5d --- /dev/null +++ b/.changeset/tough-phones-sip.md @@ -0,0 +1,5 @@ +--- +"shopify-buy": patch +--- + +Fix bug where adding multiple discount codes to the cart could inadvertently remove some discounts diff --git a/README.md b/README.md index 3344ee761..032b0b3d9 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ If you migrate to Storefront API Client, there is virtually no use case that can | shippingLine | ⚠️ | Not supported. Defaults to `null` | Same as above | | taxExempt | ⚠️ | Not supported. Defaults to `false` | The [Cart API](https://shopify.dev/docs/api/storefront/2025-01/objects/cart) is not tax aware, as taxes are currently handled in the Checkout flow. Remove any existing code depending on this field. | | taxesIncluded | ⚠️ | Not supported. Defaults to `false` | Same as above | +| discountApplications | ✅⚠️ | If a buyer's shipping address is unknown and a shipping discount is applied, shipping discount information is **no longer** returned | In this situation, the [Cart API](https://shopify.dev/docs/api/storefront/2025-01/objects/cart) does not return any information about the value of the shipping discount (eg: whether it's a 100% discount or a $5 off discount) #### Updated `.checkout` methods diff --git a/package-lock.json b/package-lock.json index b6586a893..0cb5fae23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "shopify-buy", - "version": "3.0.0", + "version": "3.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shopify-buy", - "version": "3.0.0", + "version": "3.0.6", "license": "MIT", "devDependencies": { "@changesets/cli": "^2.28.1", diff --git a/src/checkout-resource.js b/src/checkout-resource.js index 448e8d6b8..aa399ed0c 100644 --- a/src/checkout-resource.js +++ b/src/checkout-resource.js @@ -201,20 +201,41 @@ class CheckoutResource extends Resource { * @return {Promise|GraphModel} A promise resolving with the updated checkout. */ addDiscount(checkoutId, discountCode) { - return this.fetch(checkoutId).then((checkout) => { - const existingRootCodes = checkout.discountApplications.map( - (discountApplication) => discountApplication.code - ); + // We want access to Cart's `discountCodes` field, so we can't just use the + // existing `fetch` method since that also maps and removes the `discountCodes` field. + // We must therefore look at the raw Cart data to be able to see ALL existing discount codes, + // whether they are `applied` or not. + + // The query below is identical to the `fetch` method's query EXCEPT we don't call `mapCartPayload` here + return this.graphQLClient.send(cartNodeQuery, {id: checkoutId}).then(({model, data}) => { + return new Promise((resolve, reject) => { + try { + const cart = data.cart || data.node; + + if (!cart) { + return resolve(null); + } - const existingLineCodes = checkout.lineItems.map((lineItem) => { - return lineItem.discountAllocations.map( - ({discountApplication}) => discountApplication.code - ); - }); + return this.graphQLClient + .fetchAllPages(model.cart.lines, {pageSize: 250}) + .then((lines) => { + model.cart.attrs.lines = lines; + + return resolve(model.cart); + }); + } catch (error) { + if (error) { + reject(error); + } else { + reject([{message: 'an unknown error has occurred.'}]); + } + } - // get unique applied codes - const existingCodes = Array.from( - new Set([...existingRootCodes, ...existingLineCodes.flat()]) + return resolve(null); + }); + }).then((checkout) => { + const existingCodes = checkout.discountCodes.map( + (code) => code.code ); const variables = this.inputMapper.addDiscount( diff --git a/src/utilities/cart-discount-mapping.js b/src/utilities/cart-discount-mapping.js index 848129899..c966dee70 100644 --- a/src/utilities/cart-discount-mapping.js +++ b/src/utilities/cart-discount-mapping.js @@ -152,44 +152,84 @@ export function discountMapper({cartLineItems, cartDiscountAllocations, cartDisc }; } + // For each discount allocation, move the code/title field to be inside the discountApplication. + // This is because the code/title field is part of the discount allocation for a Cart, but part of + // the discount application for a Checkout + // + // CART EXAMPLE: | CHECKOUT EXAMPLE: + // "cart": { | "checkout": { + // "discountAllocations": [ | "discountApplications": { + // { | "nodes": [ + // "discountedAmount": { | { + // "amount": "18.0", | "targetSelection": "ALL", + // "currencyCode": "CAD" | "allocationMethod": "EACH", + // }, | "targetType": "SHIPPING_LINE", + // "discountApplication": { | "value": { + // "targetType": "SHIPPING_LINE", | "percentage": 100.0 + // "allocationMethod": "EACH", | }, + // "targetSelection": "ALL", | "code": "FREESHIPPINGALLCOUNTRIES", + // "value": { | "applicable": true + // "percentage": 100.0 | } + // } | ] + // }, | }, + // "code": "FREESHIPPINGALLCOUNTRIES" | } + // } | + // ] | + // "discountCodes": [ | + // { | + // "code": "FREESHIPPINGALLCOUNTRIES", | + // "applicable": true | + // } | + // ], | + // } | convertToCheckoutDiscountApplicationType(cartLineItems, cartDiscountAllocations); + // While both the Cart and Checkout API return discount allocations for line items and therefore appear similar, they are + // substantially different in how they handle order-level discounts. + // + // The Checkout API ONLY returns discount allocations as a field on line items (for both product-level and order-level discounts). + // Shipping discounts are only returned as part of `checkout.discountApplications` (and do NOT have any discount allocations). + // + // Unlike the Checkout API, the Cart API returns different types of discount allocations in 2 different places: + // 1. Discount allocations as a field on line items (for product-level discounts) + // 2. Discount allocations as a field on the Cart itself (for order-level discounts and shipping discounts) + // + // Therefore, to map the Cart API payload to the equivalent Checkout API payload, we need to go through all of the order-level discount + // allocations on the *Cart*, and determine which line item the discount is allocated to. But first, we must go through the cart-level + // discount allocations to split them into order-level and shipping-level discount allocations. + // - ONLY the order-level discount allocations go onto line items. + const [shippingDiscountAllocations, orderLevelDiscountAllocations] = cartDiscountAllocations.reduce((acc, discountAllocation) => { + if (discountAllocation.discountApplication.targetType === 'SHIPPING_LINE') { + acc[0].push(discountAllocation); + } else { + acc[1].push(discountAllocation); + } + + return acc; + }, [[], []]); const cartLinesWithAllDiscountAllocations = mergeCartOrderLevelDiscountAllocationsToCartLineDiscountAllocations({ lineItems: cartLineItems, orderLevelDiscountAllocationsForLines: findLineIdForEachOrderLevelDiscountAllocation( cartLineItems, - cartDiscountAllocations + orderLevelDiscountAllocations ) }); + // The Cart API and Checkout API have almost identical fields for discount applications, but the `value` field's behaviour (for fixed-amount discounts) + // is different. + // + // With the Checkout API, the `value` field of a discount application is equal to the SUM of all of the `allocatedAmount`s of all of the discount allocations + // for that discount. + // With the Cart API, the `value` field of a discount application is always equal to the `allocatedAmount` of the discount allocation that the discount + // application is inside of. Therefore, to map this to the equivalent Checkout API payload, we need to find all of the discount allocations for the same + // discount, and sum up all of the allocated amounts to determine the TOTAL value of the discount. const discountIdToDiscountApplicationMap = generateDiscountApplications( cartLinesWithAllDiscountAllocations, + shippingDiscountAllocations, cartDiscountCodes ); - cartDiscountCodes.forEach(({code, codeIsApplied}) => { - if (!codeIsApplied) { return; } - - // Check if the code exists in the map (case-insensitive) - let found = false; - - for (const [key] of discountIdToDiscountApplicationMap) { - if (key.toLowerCase() === code.toLowerCase()) { - found = true; - break; - } - } - if (!found) { - throw new Error( - `Discount code ${code} not found in discount application map. - Discount application map: ${JSON.stringify( - discountIdToDiscountApplicationMap - )}` - ); - } - }); - return { discountApplications: Array.from( discountIdToDiscountApplicationMap.values() @@ -222,7 +262,7 @@ function mergeCartOrderLevelDiscountAllocationsToCartLineDiscountAllocations({ }); } -function generateDiscountApplications(cartLinesWithAllDiscountAllocations, discountCodes) { +function generateDiscountApplications(cartLinesWithAllDiscountAllocations, shippingDiscountAllocations, discountCodes) { const discountIdToDiscountApplicationMap = new Map(); if (!cartLinesWithAllDiscountAllocations) { return discountIdToDiscountApplicationMap; } @@ -231,86 +271,94 @@ function generateDiscountApplications(cartLinesWithAllDiscountAllocations, disco if (!discountAllocations) { return; } discountAllocations.forEach((discountAllocation) => { - const discountApp = discountAllocation.discountApplication; - const discountId = getDiscountAllocationId(discountAllocation); + createCheckoutDiscountApplicationFromCartDiscountAllocation(discountAllocation, discountIdToDiscountApplicationMap, discountCodes); + }); + }); + + shippingDiscountAllocations.forEach((discountAllocation) => { + createCheckoutDiscountApplicationFromCartDiscountAllocation(discountAllocation, discountIdToDiscountApplicationMap, discountCodes); + }); + + return discountIdToDiscountApplicationMap; +} + +function createCheckoutDiscountApplicationFromCartDiscountAllocation(discountAllocation, discountIdToDiscountApplicationMap, discountCodes) { + const discountApp = discountAllocation.discountApplication; + const discountId = getDiscountAllocationId(discountAllocation); - if (!discountId) { + if (!discountId) { + throw new Error( + `Discount allocation must have either code or title in discountApplication: ${JSON.stringify( + discountAllocation + )}` + ); + } + + if (discountIdToDiscountApplicationMap.has(discountId.toLowerCase())) { + const existingDiscountApplication = + discountIdToDiscountApplicationMap.get(discountId.toLowerCase()); + + // if existingDiscountApplication.value is an amount rather than a percentage discount + if (existingDiscountApplication.value && 'amount' in existingDiscountApplication.value) { + existingDiscountApplication.value = { + amount: (Number(existingDiscountApplication.value.amount) + Number(discountAllocation.discountedAmount.amount)).toFixed(2), + currencyCode: existingDiscountApplication.value.currencyCode, + type: existingDiscountApplication.value.type + }; + } + } else { + let discountApplication = { + __typename: 'DiscountApplication', + targetSelection: discountApp.targetSelection, + allocationMethod: discountApp.allocationMethod, + targetType: discountApp.targetType, + value: discountApp.value, + hasNextPage: false, + hasPreviousPage: false + }; + + if ('code' in discountAllocation.discountApplication) { + const discountCode = discountCodes.find( + ({code}) => code.toLowerCase() === discountId.toLowerCase() + ); + + if (!discountCode) { throw new Error( - `Discount allocation must have either code or title in discountApplication: ${JSON.stringify( - discountAllocation + `Discount code ${discountId} not found in cart discount codes. Discount codes: ${JSON.stringify( + discountCodes )}` ); } - - if (discountIdToDiscountApplicationMap.has(discountId.toLowerCase())) { - const existingDiscountApplication = - discountIdToDiscountApplicationMap.get(discountId.toLowerCase()); - - // if existingDiscountApplication.value is an amount rather than a percentage discount - if (existingDiscountApplication.value && 'amount' in existingDiscountApplication.value) { - existingDiscountApplication.value = { - amount: (Number(existingDiscountApplication.value.amount) + Number(discountAllocation.discountedAmount.amount)).toFixed(2), - currencyCode: existingDiscountApplication.value.currencyCode, - type: existingDiscountApplication.value.type - }; + discountApplication = Object.assign({}, discountApplication, { + code: discountAllocation.discountApplication.code, + applicable: discountCode.applicable, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' } - } else { - let discountApplication = { - __typename: 'DiscountApplication', - targetSelection: discountApp.targetSelection, - allocationMethod: discountApp.allocationMethod, - targetType: discountApp.targetType, - value: discountApp.value, - hasNextPage: false, - hasPreviousPage: false - }; - - if ('code' in discountAllocation.discountApplication) { - const discountCode = discountCodes.find( - ({code}) => code.toLowerCase() === discountId.toLowerCase() - ); - - if (!discountCode) { - throw new Error( - `Discount code ${discountId} not found in cart discount codes. Discount codes: ${JSON.stringify( - discountCodes - )}` - ); - } - discountApplication = Object.assign({}, discountApplication, { - code: discountAllocation.discountApplication.code, - applicable: discountCode.applicable, - type: { - fieldBaseTypes: { - applicable: 'Boolean', - code: 'String' - }, - implementsNode: false, - kind: 'OBJECT', - name: 'DiscountApplication' - } - }); - } else { - discountApplication = Object.assign({}, discountApplication, { - title: discountAllocation.discountApplication.title, - type: { - fieldBaseTypes: { - applicable: 'Boolean', - title: 'String' - }, - implementsNode: false, - kind: 'OBJECT', - name: 'DiscountApplication' - } - }); + }); + } else { + discountApplication = Object.assign({}, discountApplication, { + title: discountAllocation.discountApplication.title, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + title: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' } + }); + } - discountIdToDiscountApplicationMap.set(discountId.toLowerCase(), discountApplication); - } - }); - }); - - return discountIdToDiscountApplicationMap; + discountIdToDiscountApplicationMap.set(discountId.toLowerCase(), discountApplication); + } } export function deepSortLines(lineItems) { diff --git a/test/client-checkout-discounts-integration-test.js b/test/client-checkout-discounts-integration-test.js index caef07be8..5f7a8e663 100644 --- a/test/client-checkout-discounts-integration-test.js +++ b/test/client-checkout-discounts-integration-test.js @@ -1,3 +1,4 @@ +/* eslint-disable id-length */ import assert from 'assert'; import Client from '../src/client'; @@ -71,7 +72,7 @@ function assertActualDiscountAllocationIsExpected(actual, expected) { expected.discountApplication ); - // Create a deep copy to avoid modifying the original object +// Create a deep copy to avoid modifying the original object const cleanedActualWithoutApplication = JSON.parse(JSON.stringify(actual)); const cleanedExpectedWithoutApplication = JSON.parse( JSON.stringify(expected) @@ -1189,7 +1190,7 @@ suite('client-checkout-discounts-integration-test', () => { ); }); }); - }); + }).timeout(5000); test('adds a percentage discount to a checkout with multiple line items via addDiscount', () => { const discountCode = '10PERCENTOFF'; @@ -1763,82 +1764,6 @@ suite('client-checkout-discounts-integration-test', () => { }); }); - test( - 'adds a free shipping discount to a checkout when shippingAddress is set via addDiscount', - () => { - const discountCode = 'FREESHIPPINGALLCOUNTRIES'; - - return client.checkout - .create({ - lineItems: [ - { - variantId: 'gid://shopify/ProductVariant/50850334310456', - quantity: 1 - } - ], - shippingAddress: { - country: 'United States', - province: 'New York' - } - }) - .then((checkout) => { - return client.checkout - .addDiscount(checkout.id, discountCode) - .then((updatedCheckout) => { - const expectedRootDiscountApplications = [ - { - __typename: 'DiscountCodeApplication', - targetSelection: 'ALL', - allocationMethod: 'EACH', - targetType: 'SHIPPING_LINE', - value: { - percentage: 100, - type: { - name: 'PricingValue', - kind: 'UNION' - } - }, - code: discountCode, - applicable: true, - type: { - name: 'DiscountCodeApplication', - kind: 'OBJECT', - fieldBaseTypes: { - applicable: 'Boolean', - code: 'String' - }, - implementsNode: false - }, - hasNextPage: false, - hasPreviousPage: false, - variableValues: { - checkoutId: - 'gid://shopify/Checkout/64315a82d62b5d89481c462a43f3a896?key=a27df729b11a037e5b844dbd41750b41', - discountCode - } - } - ]; - - assert.strictEqual( - updatedCheckout.discountApplications.length, - 1 - ); - - const expectedRootDiscountApplication = - expectedRootDiscountApplications[0]; - const actualRootDiscountApplication = - updatedCheckout.discountApplications[0]; - - assertActualDiscountApplicationIsExpected( - actualRootDiscountApplication, - expectedRootDiscountApplication - ); - }); - }); - } - ) - .timeout(3000); - test('it adds multiple discounts in one addDiscount transaction', () => { const discountCodes = ['10OFF', 'FREESHIPPINGALLCOUNTRIES']; @@ -1896,7 +1821,7 @@ suite('client-checkout-discounts-integration-test', () => { }); }); }); - }).timeout(3000); + }).timeout(5000); suite('addDiscount / not supported', () => { suite('empty checkout', () => { @@ -1968,7 +1893,7 @@ suite('client-checkout-discounts-integration-test', () => { }); }); }); - }).timeout(3000); + }).timeout(5000); test('it returns an error when the checkout ID is invalid', () => { const invalidCheckoutId = 'invalid-checkout-id'; @@ -1981,5 +1906,1437 @@ suite('client-checkout-discounts-integration-test', () => { }); }); }); + + suite('shipping discounts', () => { + test('cannot return shipping discount information for a checkout without a shipping address and no line items', async() => { + const discountCode = 'FREESHIPPINGALLCOUNTRIES'; + + return client.checkout + .create({}) + .then((checkout) => { + return client.checkout + .addDiscount(checkout.id, discountCode) + .then((updatedCheckout) => { + assert.strictEqual( + updatedCheckout.discountApplications.length, + 0 + ); + }); + }); + }).timeout(5000); + + test('cannot return shipping discount information for a checkout without a shipping address and a single line item', () => { + const discountCode = 'FREESHIPPINGALLCOUNTRIES'; + + return client.checkout + .create({ + lineItems: [ + { + variantId: 'gid://shopify/ProductVariant/50850334310456', + quantity: 1 + } + ] + }) + .then((checkout) => { + return client.checkout + .addDiscount(checkout.id, discountCode) + .then((updatedCheckout) => { + assert.strictEqual( + updatedCheckout.discountApplications.length, + 0 + ); + + assert.strictEqual(updatedCheckout.lineItems[0].discountAllocations.length, 0); + }); + }); + }).timeout(5000); + + test('cannot return shipping discount information for a checkout without a shipping address and multiple line items', () => { + const discountCode = 'FREESHIPPINGALLCOUNTRIES'; + + return client.checkout + .create({ + lineItems: [ + { + variantId: 'gid://shopify/ProductVariant/50850334310456', + quantity: 1 + } + ] + }) + .then((checkout) => { + return client.checkout + .addDiscount(checkout.id, discountCode) + .then((updatedCheckout) => { + assert.strictEqual( + updatedCheckout.discountApplications.length, + 0 + ); + + updatedCheckout.lineItems.forEach((lineItem) => { + assert.strictEqual(lineItem.discountAllocations.length, 0); + }); + }); + }); + }).timeout(5000); + + test('cannot return shipping discount information but returns other discount information for a checkout without a shipping address and multiple line items', async() => { + const discountCode1 = 'ORDERFIXED50OFF'; + const discountCode2 = 'FREESHIPPINGALLCOUNTRIES'; + const discountCode3 = '5OFFONCE'; + const discountCode4 = 'XGETY50OFF'; + + + const checkout = await client.checkout.create({ + lineItems: [ + { + variantId: 'gid://shopify/ProductVariant/50850334310456', + quantity: 5 + }, + { + variantId: 'gid://shopify/ProductVariant/50850336211000', + quantity: 10 + } + ] + }); + + // eslint-disable-next-line newline-after-var + let updatedCheckout = await client.checkout.addDiscount(checkout.id, discountCode1); + updatedCheckout = await client.checkout.addDiscount(updatedCheckout.id, discountCode2); + updatedCheckout = await client.checkout.addDiscount(updatedCheckout.id, discountCode3); + updatedCheckout = await client.checkout.addDiscount(updatedCheckout.id, discountCode4); + + const expectedRootDiscountApplications = [ + { + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + name: 'DiscountCodeApplication', + kind: 'OBJECT', + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false + }, + hasNextPage: false, + hasPreviousPage: false, + variableValues: { + checkoutId: + 'gid://shopify/Checkout/64315a82d62b5d89481c462a43f3a896?key=a27df729b11a037e5b844dbd41750b41', + discountCode: 'ORDERFIXED50OFF' + } + }, + { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '5.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: '5OFFONCE', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + { + targetSelection: 'ENTITLED', + allocationMethod: 'EACH', + targetType: 'LINE_ITEM', + value: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + code: 'XGETY50OFF', + applicable: true, + type: { + name: 'DiscountCodeApplication', + kind: 'OBJECT', + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false + }, + hasNextPage: false, + hasPreviousPage: false, + variableValues: { + checkoutId: + 'gid://shopify/Checkout/64315a82d62b5d89481c462a43f3a896?key=a27df729b11a037e5b844dbd41750b41', + discountCode: 'XGETY50OFF' + } + } + ]; + + assert.strictEqual(updatedCheckout.discountApplications.length, expectedRootDiscountApplications.length); + // Just in case the relative order is different + updatedCheckout.discountApplications.sort((a, b) => a.code.localeCompare(b.code)); + expectedRootDiscountApplications.sort((a, b) => a.code.localeCompare(b.code)); + updatedCheckout.discountApplications.forEach((discountApplication, index) => { + assertActualDiscountApplicationIsExpected( + discountApplication, + expectedRootDiscountApplications[index] + ); + }); + + const actualAndExpectedDiscountAllocations = { + // Keys are variant ID (without gid://shopify/ProductVariant/) and quantity + // Even though we had only 2 line items as input, it is expected that we have 4 entries here due to the combination of the + // "Buy X Get Y" discount and the fact that "5OFFONCE" is only applied once and not per item + // This "line item splitting" behaviour is consistent between the Checkout API and the Cart API + '50850334310456_1': { + expected: [ + { + allocatedAmount: { + amount: '5.0', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '5.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: '5OFFONCE', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + }, + { + // There is a small discrepancy between the Checkout API and the Cart API in this "line item splitting" scenario + // For both APIs, we have 4 discount allocations for the "ORDERFIXED50OFF" order-level discount (and we have 4 line items after the "splitting") + // For these 4 discount allocations (for the "ORDERFIXED50OFF" order-level discount): + // - the Checkout API returns **2** discount allocations on 2 of the line items and **0** discount allocations on the other 2 line items + // - the Cart API returns **1** discount allocation on each of the 4 line items + // + // With order-level discounts, the discount allocations sum up to the total discount amount, so either way the result is the same. + // Here, the "expected" discount allocations are directly from the Checkout APIs EXCEPT I have manually moved 2 of the order-level discount allocations + // so that each line item has 1 discount allocation (since ultimately we want this to represent what we expect JS Buy SDK v3 to return if everything + // is working as expected) + allocatedAmount: { + amount: '7.25', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + } + ] + }, + '50850334310456_4': { + expected: [ + { + allocatedAmount: { + amount: '29.74', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + }, + { + allocatedAmount: { + amount: '0.0', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'EACH', + targetType: 'LINE_ITEM', + value: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'XGETY50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + } + ] + }, + '50850336211000_3': { + expected: [ + { + allocatedAmount: { + amount: '7.8', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + }, + { + allocatedAmount: { + amount: '0.0', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'EACH', + targetType: 'LINE_ITEM', + value: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'XGETY50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + } + ] + }, + '50850336211000_7': { + expected: [ + { + allocatedAmount: { + amount: '5.21', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + }, + { + allocatedAmount: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'EACH', + targetType: 'LINE_ITEM', + value: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'XGETY50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + } + ] + } + }; + + updatedCheckout.lineItems.forEach((line) => { + // Remove the gid://shopify/ProductVariant/ prefix from the variant ID + const variantId = line.variant.id.split('/').pop(); + const quantity = line.quantity; + const actual = line.discountAllocations.sort((a, b) => a.discountApplication.code.localeCompare(b.discountApplication.code)); + const expected = actualAndExpectedDiscountAllocations[`${variantId}_${quantity}`].expected.sort((a, b) => a.discountApplication.code.localeCompare(b.discountApplication.code)); + + assert.strictEqual(actual.length, expected.length); + for (let i = 0; i < actual.length; i++) { + assertActualDiscountAllocationIsExpected(actual[i], expected[i]); + } + }); + }).timeout(10000); + + test('gracefully returns an empty array for discountApplications when adding a free shipping discount to a checkout with a shipping address but no line items', async() => { + const discountCode = 'FREESHIPPINGALLCOUNTRIES'; + + const checkout = await client.checkout.create({ + shippingAddress: { + country: 'United States', + province: 'New York' + } + }); + + const updatedCheckout = await client.checkout.addDiscount(checkout.id, discountCode); + + + assert.strictEqual(updatedCheckout.discountApplications.length, 0); + }); + + test('adds a free shipping discount to a checkout with a shipping address and a single line item', async() => { + const discountCode = 'FREESHIPPINGALLCOUNTRIES'; + + const checkout = await client.checkout.create({ + lineItems: [ + { + variantId: 'gid://shopify/ProductVariant/50850334310456', + quantity: 1 + } + ], + shippingAddress: { + country: 'United States', + province: 'New York' + } + }); + + const updatedCheckout = await client.checkout.addDiscount(checkout.id, discountCode); + + const expectedRootDiscountApplications = [ + { + __typename: 'DiscountCodeApplication', + targetSelection: 'ALL', + allocationMethod: 'EACH', + targetType: 'SHIPPING_LINE', + value: { + percentage: 100.0, + type: { + kind: 'UNION', + name: 'PricingValue' + } + }, + code: 'FREESHIPPINGALLCOUNTRIES', + applicable: true, + type: { + name: 'DiscountCodeApplication', + kind: 'OBJECT', + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false + }, + hasNextPage: false, + hasPreviousPage: false, + variableValues: { + checkoutId: + 'gid://shopify/Checkout/64315a82d62b5d89481c462a43f3a896?key=a27df729b11a037e5b844dbd41750b41', + discountCode + } + } + ]; + + const expectedRootDiscountApplication = expectedRootDiscountApplications[0]; + const actualRootDiscountApplication = updatedCheckout.discountApplications[0]; + + assertActualDiscountApplicationIsExpected( + actualRootDiscountApplication, + expectedRootDiscountApplication + ); + assert.strictEqual(updatedCheckout.lineItems[0].discountAllocations.length, 0); + }).timeout(5000); + + test('adds a free shipping discount to a checkout with a shipping address and multiple line items', async () => { + const discountCode = 'FREESHIPPINGALLCOUNTRIES'; + + const checkout = await client.checkout.create({ + lineItems: [ + { + variantId: 'gid://shopify/ProductVariant/50850334310456', + quantity: 2 + }, + { + variantId: 'gid://shopify/ProductVariant/50850336211000', + quantity: 3 + } + ], + shippingAddress: { + country: 'United States', + province: 'New York' + } + }); + + const updatedCheckout = await client.checkout.addDiscount( + checkout.id, + discountCode + ); + + const expectedRootDiscountApplications = [ + { + __typename: 'DiscountCodeApplication', + targetSelection: 'ALL', + allocationMethod: 'EACH', + targetType: 'SHIPPING_LINE', + value: { + percentage: 100, + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + code: discountCode, + applicable: true, + type: { + name: 'DiscountCodeApplication', + kind: 'OBJECT', + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false + }, + hasNextPage: false, + hasPreviousPage: false, + variableValues: { + checkoutId: + 'gid://shopify/Checkout/64315a82d62b5d89481c462a43f3a896?key=a27df729b11a037e5b844dbd41750b41', + discountCode + } + } + ]; + + assert.strictEqual( + updatedCheckout.discountApplications.length, + 1 + ); + + const expectedRootDiscountApplication = + expectedRootDiscountApplications[0]; + const actualRootDiscountApplication = + updatedCheckout.discountApplications[0]; + + assertActualDiscountApplicationIsExpected( + actualRootDiscountApplication, + expectedRootDiscountApplication + ); + + updatedCheckout.lineItems.forEach((lineItem) => { + assert.strictEqual(lineItem.discountAllocations.length, 0); + }); + }).timeout(5000); + + test('returns shipping discount and other discount information for a checkout with a shipping address and multiple line items', async() => { + const discountCode1 = 'ORDERFIXED50OFF'; + const discountCode2 = 'FREESHIPPINGALLCOUNTRIES'; + const discountCode3 = '5OFFONCE'; + const discountCode4 = 'XGETY50OFF'; + + + const checkout = await client.checkout.create({ + lineItems: [ + { + variantId: 'gid://shopify/ProductVariant/50850334310456', + quantity: 5 + }, + { + variantId: 'gid://shopify/ProductVariant/50850336211000', + quantity: 10 + } + ], + shippingAddress: { + country: 'United States', + province: 'New York' + } + }); + + // eslint-disable-next-line newline-after-var + let updatedCheckout = await client.checkout.addDiscount(checkout.id, discountCode1); + updatedCheckout = await client.checkout.addDiscount(updatedCheckout.id, discountCode2); + updatedCheckout = await client.checkout.addDiscount(updatedCheckout.id, discountCode3); + updatedCheckout = await client.checkout.addDiscount(updatedCheckout.id, discountCode4); + + const expectedRootDiscountApplications = [ + { + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + name: 'DiscountCodeApplication', + kind: 'OBJECT', + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false + }, + hasNextPage: false, + hasPreviousPage: false, + variableValues: { + checkoutId: + 'gid://shopify/Checkout/64315a82d62b5d89481c462a43f3a896?key=a27df729b11a037e5b844dbd41750b41', + discountCode: 'ORDERFIXED50OFF' + } + }, + { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '5.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: '5OFFONCE', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + { + targetSelection: 'ENTITLED', + allocationMethod: 'EACH', + targetType: 'LINE_ITEM', + value: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + code: 'XGETY50OFF', + applicable: true, + type: { + name: 'DiscountCodeApplication', + kind: 'OBJECT', + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false + }, + hasNextPage: false, + hasPreviousPage: false, + variableValues: { + checkoutId: + 'gid://shopify/Checkout/64315a82d62b5d89481c462a43f3a896?key=a27df729b11a037e5b844dbd41750b41', + discountCode: 'XGETY50OFF' + } + }, + { + targetSelection: 'ALL', + allocationMethod: 'EACH', + targetType: 'SHIPPING_LINE', + value: { + percentage: 100.0, + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + code: 'FREESHIPPINGALLCOUNTRIES', + applicable: true, + type: { + name: 'DiscountCodeApplication', + kind: 'OBJECT', + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false + }, + hasNextPage: false, + hasPreviousPage: false, + variableValues: { + checkoutId: + 'gid://shopify/Checkout/64315a82d62b5d89481c462a43f3a896?key=a27df729b11a037e5b844dbd41750b41', + discountCode: 'FREESHIPPINGALLCOUNTRIES' + } + } + ]; + + assert.strictEqual(updatedCheckout.discountApplications.length, expectedRootDiscountApplications.length); + // Just in case the relative order is different + updatedCheckout.discountApplications.sort((a, b) => a.code.localeCompare(b.code)); + expectedRootDiscountApplications.sort((a, b) => a.code.localeCompare(b.code)); + updatedCheckout.discountApplications.forEach((discountApplication, index) => { + assertActualDiscountApplicationIsExpected( + discountApplication, + expectedRootDiscountApplications[index] + ); + }); + + const actualAndExpectedDiscountAllocations = { + // Keys are variant ID (without gid://shopify/ProductVariant/) and quantity + // Even though we had only 2 line items as input, it is expected that we have 4 entries here due to the combination of the + // "Buy X Get Y" discount and the fact that "5OFFONCE" is only applied once and not per item + // This "line item splitting" behaviour is consistent between the Checkout API and the Cart API + '50850334310456_1': { + expected: [ + { + allocatedAmount: { + amount: '5.0', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '5.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: '5OFFONCE', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + }, + { + allocatedAmount: { + amount: '7.25', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + } + ] + }, + '50850334310456_4': { + expected: [ + { + // There is a small discrepancy between the Checkout API and the Cart API in this "line item splitting" scenario + // For both APIs, we have 4 discount allocations for the "ORDERFIXED50OFF" order-level discount (and we have 4 line items after the "splitting") + // For these 4 discount allocations (for the "ORDERFIXED50OFF" order-level discount): + // - the Checkout API returns **2** discount allocations on 2 of the line items and **0** discount allocations on the other 2 line items + // - the Cart API returns **1** discount allocation on each of the 4 line items + // + // With order-level discounts, the discount allocations sum up to the total discount amount, so either way the result is the same. + // Here, the "expected" discount allocations are directly from the Checkout APIs EXCEPT I have manually adjusted the order-level discount allocations + // so that each line item has 1 discount allocation (since ultimately we want this to represent what we expect JS Buy SDK v3 to return if everything + // is working as expected) + allocatedAmount: { + amount: '29.74', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + }, + { + allocatedAmount: { + amount: '0.0', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'EACH', + targetType: 'LINE_ITEM', + value: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'XGETY50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + } + ] + }, + '50850336211000_3': { + expected: [ + { + allocatedAmount: { + amount: '7.8', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + }, + { + allocatedAmount: { + amount: '0.0', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'EACH', + targetType: 'LINE_ITEM', + value: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'XGETY50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + } + ] + }, + '50850336211000_7': { + expected: [ + { + allocatedAmount: { + amount: '5.21', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ALL', + allocationMethod: 'ACROSS', + targetType: 'LINE_ITEM', + value: { + amount: '50.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'ORDERFIXED50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + }, + { + allocatedAmount: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'MoneyV2', + kind: 'OBJECT', + fieldBaseTypes: { + amount: 'Decimal', + currencyCode: 'CurrencyCode' + }, + implementsNode: false + } + }, + discountApplication: { + __typename: 'DiscountApplication', + targetSelection: 'ENTITLED', + allocationMethod: 'EACH', + targetType: 'LINE_ITEM', + value: { + amount: '350.0', + currencyCode: 'CAD', + type: { + name: 'PricingValue', + kind: 'UNION' + } + }, + hasNextPage: false, + hasPreviousPage: false, + code: 'XGETY50OFF', + applicable: true, + type: { + fieldBaseTypes: { + applicable: 'Boolean', + code: 'String' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountApplication' + } + }, + type: { + fieldBaseTypes: { + allocatedAmount: 'MoneyV2', + discountApplication: 'DiscountApplication' + }, + implementsNode: false, + kind: 'OBJECT', + name: 'DiscountAllocation' + } + } + ] + } + }; + + updatedCheckout.lineItems.forEach((line) => { + // Remove the gid://shopify/ProductVariant/ prefix from the variant ID + const variantId = line.variant.id.split('/').pop(); + const quantity = line.quantity; + const actual = line.discountAllocations.sort((a, b) => a.discountApplication.code.localeCompare(b.discountApplication.code)); + const expected = actualAndExpectedDiscountAllocations[`${variantId}_${quantity}`].expected.sort((a, b) => a.discountApplication.code.localeCompare(b.discountApplication.code)); + + assert.strictEqual(actual.length, expected.length); + for (let i = 0; i < actual.length; i++) { + assertActualDiscountAllocationIsExpected(actual[i], expected[i]); + } + }); + }).timeout(10000); + }); }); });