@@ -152,44 +152,84 @@ export function discountMapper({cartLineItems, cartDiscountAllocations, cartDisc
152152 } ;
153153 }
154154
155+ // For each discount allocation, move the code/title field to be inside the discountApplication.
156+ // This is because the code/title field is part of the discount allocation for a Cart, but part of
157+ // the discount application for a Checkout
158+ //
159+ // CART EXAMPLE: | CHECKOUT EXAMPLE:
160+ // "cart": { | "checkout": {
161+ // "discountAllocations": [ | "discountApplications": {
162+ // { | "nodes": [
163+ // "discountedAmount": { | {
164+ // "amount": "18.0", | "targetSelection": "ALL",
165+ // "currencyCode": "CAD" | "allocationMethod": "EACH",
166+ // }, | "targetType": "SHIPPING_LINE",
167+ // "discountApplication": { | "value": {
168+ // "targetType": "SHIPPING_LINE", | "percentage": 100.0
169+ // "allocationMethod": "EACH", | },
170+ // "targetSelection": "ALL", | "code": "FREESHIPPINGALLCOUNTRIES",
171+ // "value": { | "applicable": true
172+ // "percentage": 100.0 | }
173+ // } | ]
174+ // }, | },
175+ // "code": "FREESHIPPINGALLCOUNTRIES" | }
176+ // } |
177+ // ] |
178+ // "discountCodes": [ |
179+ // { |
180+ // "code": "FREESHIPPINGALLCOUNTRIES", |
181+ // "applicable": true |
182+ // } |
183+ // ], |
184+ // } |
155185 convertToCheckoutDiscountApplicationType ( cartLineItems , cartDiscountAllocations ) ;
156186
187+ // While both the Cart and Checkout API return discount allocations for line items and therefore appear similar, they are
188+ // substantially different in how they handle order-level discounts.
189+ //
190+ // The Checkout API ONLY returns discount allocations as a field on line items (for both product-level and order-level discounts).
191+ // Shipping discounts are only returned as part of `checkout.discountApplications` (and do NOT have any discount allocations).
192+ //
193+ // Unlike the Checkout API, the Cart API returns different types of discount allocations in 2 different places:
194+ // 1. Discount allocations as a field on line items (for product-level discounts)
195+ // 2. Discount allocations as a field on the Cart itself (for order-level discounts and shipping discounts)
196+ //
197+ // Therefore, to map the Cart API payload to the equivalent Checkout API payload, we need to go through all of the order-level discount
198+ // allocations on the *Cart*, and determine which line item the discount is allocated to. But first, we must go through the cart-level
199+ // discount allocations to split them into order-level and shipping-level discount allocations.
200+ // - ONLY the order-level discount allocations go onto line items.
201+ const [ shippingDiscountAllocations , orderLevelDiscountAllocations ] = cartDiscountAllocations . reduce ( ( acc , discountAllocation ) => {
202+ if ( discountAllocation . discountApplication . targetType === 'SHIPPING_LINE' ) {
203+ acc [ 0 ] . push ( discountAllocation ) ;
204+ } else {
205+ acc [ 1 ] . push ( discountAllocation ) ;
206+ }
207+
208+ return acc ;
209+ } , [ [ ] , [ ] ] ) ;
157210 const cartLinesWithAllDiscountAllocations =
158211 mergeCartOrderLevelDiscountAllocationsToCartLineDiscountAllocations ( {
159212 lineItems : cartLineItems ,
160213 orderLevelDiscountAllocationsForLines : findLineIdForEachOrderLevelDiscountAllocation (
161214 cartLineItems ,
162- cartDiscountAllocations
215+ orderLevelDiscountAllocations
163216 )
164217 } ) ;
165218
219+ // The Cart API and Checkout API have almost identical fields for discount applications, but the `value` field's behaviour (for fixed-amount discounts)
220+ // is different.
221+ //
222+ // 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
223+ // for that discount.
224+ // With the Cart API, the `value` field of a discount application is always equal to the `allocatedAmount` of the discount allocation that the discount
225+ // 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
226+ // discount, and sum up all of the allocated amounts to determine the TOTAL value of the discount.
166227 const discountIdToDiscountApplicationMap = generateDiscountApplications (
167228 cartLinesWithAllDiscountAllocations ,
229+ shippingDiscountAllocations ,
168230 cartDiscountCodes
169231 ) ;
170232
171- cartDiscountCodes . forEach ( ( { code, codeIsApplied} ) => {
172- if ( ! codeIsApplied ) { return ; }
173-
174- // Check if the code exists in the map (case-insensitive)
175- let found = false ;
176-
177- for ( const [ key ] of discountIdToDiscountApplicationMap ) {
178- if ( key . toLowerCase ( ) === code . toLowerCase ( ) ) {
179- found = true ;
180- break ;
181- }
182- }
183- if ( ! found ) {
184- throw new Error (
185- `Discount code ${ code } not found in discount application map.
186- Discount application map: ${ JSON . stringify (
187- discountIdToDiscountApplicationMap
188- ) } `
189- ) ;
190- }
191- } ) ;
192-
193233 return {
194234 discountApplications : Array . from (
195235 discountIdToDiscountApplicationMap . values ( )
@@ -222,7 +262,7 @@ function mergeCartOrderLevelDiscountAllocationsToCartLineDiscountAllocations({
222262 } ) ;
223263}
224264
225- function generateDiscountApplications ( cartLinesWithAllDiscountAllocations , discountCodes ) {
265+ function generateDiscountApplications ( cartLinesWithAllDiscountAllocations , shippingDiscountAllocations , discountCodes ) {
226266 const discountIdToDiscountApplicationMap = new Map ( ) ;
227267
228268 if ( ! cartLinesWithAllDiscountAllocations ) { return discountIdToDiscountApplicationMap ; }
@@ -231,86 +271,94 @@ function generateDiscountApplications(cartLinesWithAllDiscountAllocations, disco
231271 if ( ! discountAllocations ) { return ; }
232272
233273 discountAllocations . forEach ( ( discountAllocation ) => {
234- const discountApp = discountAllocation . discountApplication ;
235- const discountId = getDiscountAllocationId ( discountAllocation ) ;
274+ createCheckoutDiscountApplicationFromCartDiscountAllocation ( discountAllocation , discountIdToDiscountApplicationMap , discountCodes ) ;
275+ } ) ;
276+ } ) ;
277+
278+ shippingDiscountAllocations . forEach ( ( discountAllocation ) => {
279+ createCheckoutDiscountApplicationFromCartDiscountAllocation ( discountAllocation , discountIdToDiscountApplicationMap , discountCodes ) ;
280+ } ) ;
281+
282+ return discountIdToDiscountApplicationMap ;
283+ }
284+
285+ function createCheckoutDiscountApplicationFromCartDiscountAllocation ( discountAllocation , discountIdToDiscountApplicationMap , discountCodes ) {
286+ const discountApp = discountAllocation . discountApplication ;
287+ const discountId = getDiscountAllocationId ( discountAllocation ) ;
236288
237- if ( ! discountId ) {
289+ if ( ! discountId ) {
290+ throw new Error (
291+ `Discount allocation must have either code or title in discountApplication: ${ JSON . stringify (
292+ discountAllocation
293+ ) } `
294+ ) ;
295+ }
296+
297+ if ( discountIdToDiscountApplicationMap . has ( discountId . toLowerCase ( ) ) ) {
298+ const existingDiscountApplication =
299+ discountIdToDiscountApplicationMap . get ( discountId . toLowerCase ( ) ) ;
300+
301+ // if existingDiscountApplication.value is an amount rather than a percentage discount
302+ if ( existingDiscountApplication . value && 'amount' in existingDiscountApplication . value ) {
303+ existingDiscountApplication . value = {
304+ amount : ( Number ( existingDiscountApplication . value . amount ) + Number ( discountAllocation . discountedAmount . amount ) ) . toFixed ( 2 ) ,
305+ currencyCode : existingDiscountApplication . value . currencyCode ,
306+ type : existingDiscountApplication . value . type
307+ } ;
308+ }
309+ } else {
310+ let discountApplication = {
311+ __typename : 'DiscountApplication' ,
312+ targetSelection : discountApp . targetSelection ,
313+ allocationMethod : discountApp . allocationMethod ,
314+ targetType : discountApp . targetType ,
315+ value : discountApp . value ,
316+ hasNextPage : false ,
317+ hasPreviousPage : false
318+ } ;
319+
320+ if ( 'code' in discountAllocation . discountApplication ) {
321+ const discountCode = discountCodes . find (
322+ ( { code} ) => code . toLowerCase ( ) === discountId . toLowerCase ( )
323+ ) ;
324+
325+ if ( ! discountCode ) {
238326 throw new Error (
239- `Discount allocation must have either code or title in discountApplication : ${ JSON . stringify (
240- discountAllocation
327+ `Discount code ${ discountId } not found in cart discount codes. Discount codes : ${ JSON . stringify (
328+ discountCodes
241329 ) } `
242330 ) ;
243331 }
244-
245- if ( discountIdToDiscountApplicationMap . has ( discountId . toLowerCase ( ) ) ) {
246- const existingDiscountApplication =
247- discountIdToDiscountApplicationMap . get ( discountId . toLowerCase ( ) ) ;
248-
249- // if existingDiscountApplication.value is an amount rather than a percentage discount
250- if ( existingDiscountApplication . value && 'amount' in existingDiscountApplication . value ) {
251- existingDiscountApplication . value = {
252- amount : ( Number ( existingDiscountApplication . value . amount ) + Number ( discountAllocation . discountedAmount . amount ) ) . toFixed ( 2 ) ,
253- currencyCode : existingDiscountApplication . value . currencyCode ,
254- type : existingDiscountApplication . value . type
255- } ;
332+ discountApplication = Object . assign ( { } , discountApplication , {
333+ code : discountAllocation . discountApplication . code ,
334+ applicable : discountCode . applicable ,
335+ type : {
336+ fieldBaseTypes : {
337+ applicable : 'Boolean' ,
338+ code : 'String'
339+ } ,
340+ implementsNode : false ,
341+ kind : 'OBJECT' ,
342+ name : 'DiscountApplication'
256343 }
257- } else {
258- let discountApplication = {
259- __typename : 'DiscountApplication' ,
260- targetSelection : discountApp . targetSelection ,
261- allocationMethod : discountApp . allocationMethod ,
262- targetType : discountApp . targetType ,
263- value : discountApp . value ,
264- hasNextPage : false ,
265- hasPreviousPage : false
266- } ;
267-
268- if ( 'code' in discountAllocation . discountApplication ) {
269- const discountCode = discountCodes . find (
270- ( { code} ) => code . toLowerCase ( ) === discountId . toLowerCase ( )
271- ) ;
272-
273- if ( ! discountCode ) {
274- throw new Error (
275- `Discount code ${ discountId } not found in cart discount codes. Discount codes: ${ JSON . stringify (
276- discountCodes
277- ) } `
278- ) ;
279- }
280- discountApplication = Object . assign ( { } , discountApplication , {
281- code : discountAllocation . discountApplication . code ,
282- applicable : discountCode . applicable ,
283- type : {
284- fieldBaseTypes : {
285- applicable : 'Boolean' ,
286- code : 'String'
287- } ,
288- implementsNode : false ,
289- kind : 'OBJECT' ,
290- name : 'DiscountApplication'
291- }
292- } ) ;
293- } else {
294- discountApplication = Object . assign ( { } , discountApplication , {
295- title : discountAllocation . discountApplication . title ,
296- type : {
297- fieldBaseTypes : {
298- applicable : 'Boolean' ,
299- title : 'String'
300- } ,
301- implementsNode : false ,
302- kind : 'OBJECT' ,
303- name : 'DiscountApplication'
304- }
305- } ) ;
344+ } ) ;
345+ } else {
346+ discountApplication = Object . assign ( { } , discountApplication , {
347+ title : discountAllocation . discountApplication . title ,
348+ type : {
349+ fieldBaseTypes : {
350+ applicable : 'Boolean' ,
351+ title : 'String'
352+ } ,
353+ implementsNode : false ,
354+ kind : 'OBJECT' ,
355+ name : 'DiscountApplication'
306356 }
357+ } ) ;
358+ }
307359
308- discountIdToDiscountApplicationMap . set ( discountId . toLowerCase ( ) , discountApplication ) ;
309- }
310- } ) ;
311- } ) ;
312-
313- return discountIdToDiscountApplicationMap ;
360+ discountIdToDiscountApplicationMap . set ( discountId . toLowerCase ( ) , discountApplication ) ;
361+ }
314362}
315363
316364export function deepSortLines ( lineItems ) {
0 commit comments