@zoxllc/shopify-checkout-extensions 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/EXAMPLES.md CHANGED
@@ -360,9 +360,9 @@ export function run(input) {
360
360
 
361
361
  ---
362
362
 
363
- ## Example 11: Multi-Tier Gift with Purchase
363
+ ## Example 11: Multi-Tier Gift with Purchase (Cumulative)
364
364
 
365
- **Use Case:** Offer tiered gifts based on cart spend threshold (e.g., spend $75 get Gift A, $100 get Gift B, $250 get Gift C). Only the highest qualifying tier is applied.
365
+ **Use Case:** Offer cumulative tiered gifts based on cart spend threshold. All qualifying tiers are applied (stackTiers: true).
366
366
 
367
367
  ```json
368
368
  {
@@ -405,23 +405,94 @@ export function run(input) {
405
405
  "label": "Free Cosmic Drift Daily"
406
406
  }
407
407
  ],
408
- ["meta-exclude-gift"]
408
+ ["meta-exclude-gift"],
409
+ true
409
410
  ]
410
411
  }
411
412
  ```
412
413
 
413
- **How It Works:**
414
+ **How It Works (stackTiers: true):**
414
415
  - Customer adds items to cart totaling $150
415
416
  - Cart subtotal is calculated (excluding items tagged "meta-exclude-gift")
416
- - Tiers are evaluated from highest to lowest
417
- - $250 tier doesn't qualify ($150 < $250)
418
- - $100 tier DOES qualify ($150 >= $100)
419
- - Product "6640591011912" (Goldie Mystery ZOX) gets 100% discount
420
- - Lower tiers ($75) are NOT applied
417
+ - All qualifying tiers are found:
418
+ - $75 tier qualifies ($150 >= $75)
419
+ - $100 tier qualifies ($150 >= $100)
420
+ - $250 tier doesn't qualify ($150 < $250)
421
+ - Products get 100% discount:
422
+ - "6638547533896" (Colorwheel F&F) - from $75 tier
423
+ - "6640591011912" (Goldie Mystery ZOX) - from $100 tier
424
+ - Customer receives BOTH gifts (cumulative/stacking)
425
+
426
+ ---
427
+
428
+ ## Example 12: Multi-Tier Bundle Upgrade (Highest Only)
429
+
430
+ **Use Case:** Offer tiered bundle upgrades where higher spend gets a better bundle product. Only the highest qualifying tier is applied (stackTiers: false).
431
+
432
+ ```json
433
+ {
434
+ "__type": "MultiTierDiscount",
435
+ "label": "Spring Bundle Upgrade",
436
+ "active": true,
437
+ "description": "Spend more, get a better bundle!",
438
+ "inputs": [
439
+ ":all",
440
+ [],
441
+ [
442
+ {
443
+ "threshold": 75,
444
+ "discount": {
445
+ "__type": "PercentageDiscount",
446
+ "inputs": [100, "Free Starter Bundle"]
447
+ },
448
+ "productIds": ["1111111111111"],
449
+ "itemsToDiscount": 1,
450
+ "label": "Free Starter Bundle"
451
+ },
452
+ {
453
+ "threshold": 150,
454
+ "discount": {
455
+ "__type": "PercentageDiscount",
456
+ "inputs": [100, "Free Premium Bundle"]
457
+ },
458
+ "productIds": ["2222222222222"],
459
+ "itemsToDiscount": 1,
460
+ "label": "Free Premium Bundle"
461
+ },
462
+ {
463
+ "threshold": 300,
464
+ "discount": {
465
+ "__type": "PercentageDiscount",
466
+ "inputs": [100, "Free Deluxe Bundle"]
467
+ },
468
+ "productIds": ["3333333333333"],
469
+ "itemsToDiscount": 1,
470
+ "label": "Free Deluxe Bundle"
471
+ }
472
+ ],
473
+ ["meta-exclude-gift"],
474
+ false
475
+ ]
476
+ }
477
+ ```
478
+
479
+ **How It Works (stackTiers: false):**
480
+ - Customer adds items to cart totaling $200
481
+ - Cart subtotal is calculated (excluding items tagged "meta-exclude-gift")
482
+ - Highest qualifying tier is selected:
483
+ - $75 tier qualifies ($200 >= $75)
484
+ - $150 tier qualifies ($200 >= $150) ✓ HIGHEST
485
+ - $300 tier doesn't qualify ($200 < $300)
486
+ - Only the Premium Bundle gets 100% discount:
487
+ - "2222222222222" (Premium Bundle) - from $150 tier
488
+ - Customer receives ONLY the Premium Bundle (not the Starter Bundle)
489
+
490
+ ---
421
491
 
422
492
  **Key Features:**
423
493
  - Automatically excludes gift items from threshold calculation
424
- - Only applies the highest qualifying tier (no stacking)
494
+ - **stackTiers: true** - Applies ALL qualifying tiers (cumulative)
495
+ - **stackTiers: false** - Applies ONLY the highest qualifying tier
425
496
  - Supports product IDs or product tags per tier
426
497
  - Perfect for seasonal promotions with start/end dates
427
498
 
@@ -436,6 +507,9 @@ export function run(input) {
436
507
  - `itemsToDiscount` - Maximum items to discount (optional, defaults to all)
437
508
  - `label` - Display name for the tier (optional)
438
509
  4. **Exclude Tags** - Array of product tags to exclude from subtotal calculation (default: `["meta-exclude-gift"]`)
510
+ 5. **Stack Tiers** - Boolean (default: `true`)
511
+ - `true` - Apply all qualifying tiers (cumulative GWP)
512
+ - `false` - Apply only highest tier (bundle upgrades)
439
513
 
440
514
  ---
441
515
 
@@ -1 +1 @@
1
- {"version":3,"file":"CampaignFactory.d.ts","sourceRoot":"","sources":["../../src/common/CampaignFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAEV,SAAS,EAIV,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAiC5C,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,WAAW,GACX,OAAO,EAAE,GACT,MAAM,EAAE,GACR,MAAM,EAAE,GACR,WAAW,EAAE,CAChB,EAAE,CAAC;CACL,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG;IAC9C,MAAM,EAAE,UAAU,GAAG,qBAAqB,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;IACtF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,qBAAa,eAAe;IAC1B,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GACI,QAAQ;IAG/D,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAuB/B,MAAM,CAAC,kBAAkB,CACvB,MAAM,EAAE,WAAW,GAEjB,QAAQ,GACR,SAAS,GACT,WAAW,GACX,UAAU,GACV,QAAQ,GACR,iBAAiB,GACjB,SAAS;CAyHd"}
1
+ {"version":3,"file":"CampaignFactory.d.ts","sourceRoot":"","sources":["../../src/common/CampaignFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAEV,SAAS,EAIV,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAiC5C,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,WAAW,GACX,OAAO,EAAE,GACT,MAAM,EAAE,GACR,MAAM,EAAE,GACR,WAAW,EAAE,CAChB,EAAE,CAAC;CACL,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG;IAC9C,MAAM,EAAE,UAAU,GAAG,qBAAqB,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;IACtF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,qBAAa,eAAe;IAC1B,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GACI,QAAQ;IAG/D,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAuB/B,MAAM,CAAC,kBAAkB,CACvB,MAAM,EAAE,WAAW,GAEjB,QAAQ,GACR,SAAS,GACT,WAAW,GACX,UAAU,GACV,QAAQ,GACR,iBAAiB,GACjB,SAAS;CA0Hd"}
@@ -55,7 +55,7 @@ class CampaignFactory {
55
55
  case 'ShippingDiscount':
56
56
  return new ShippingDiscount_1.ShippingDiscount(args[0], args[1], args[2], args[3]);
57
57
  case 'MultiTierDiscount':
58
- return new MultiTierDiscount_1.MultiTierDiscount(args[0], args[1], args[2], args[3]);
58
+ return new MultiTierDiscount_1.MultiTierDiscount(args[0], args[1], args[2], args[3], args[4]);
59
59
  case 'AndSelector':
60
60
  return new AndSelector_1.AndSelector([...args]);
61
61
  case 'OrSelector':
@@ -16,28 +16,37 @@ export interface Tier {
16
16
  * MultiTierDiscount - Applies tiered discounts based on cart subtotal
17
17
  *
18
18
  * This campaign evaluates the cart total (excluding gifts) and applies
19
- * the discount from the highest qualifying tier.
19
+ * discounts based on the stackTiers configuration:
20
20
  *
21
- * Example use case: Tiered gift with purchase
22
- * - Spend $75: Get free product A
23
- * - Spend $100: Get free product B
24
- * - Spend $250: Get free product C
21
+ * - stackTiers: true (default) - Applies ALL qualifying tiers (cumulative)
22
+ * Example: Spend $150 Get products from both $75 and $100 tiers
25
23
  *
26
- * Only the highest qualifying tier is applied (e.g., if cart is $150,
27
- * only the $100 tier applies, not both $75 and $100).
24
+ * - stackTiers: false - Applies ONLY the highest qualifying tier
25
+ * Example: Spend $150 Get product from $100 tier only
26
+ *
27
+ * Subtotal Calculation:
28
+ * - Automatically excludes items tagged with excludeGiftTags
29
+ * - Automatically excludes tier reward products (by ID or tag)
30
+ * This prevents gaming the system by manually adding gift products
31
+ *
32
+ * Use cases:
33
+ * - Cumulative GWP: Spend more, get more gifts (stackTiers: true)
34
+ * - Bundle upgrades: Spend more, get better bundle (stackTiers: false)
28
35
  */
29
36
  export declare class MultiTierDiscount extends Campaign {
30
37
  tiers: Tier[];
31
38
  excludeGiftTags: string[];
32
- constructor(behavior: QualifierBehavior, qualifiers: [Qualifier | AndSelector | OrSelector], tiers: Tier[], excludeGiftTags?: string[]);
39
+ stackTiers: boolean;
40
+ constructor(behavior: QualifierBehavior, qualifiers: [Qualifier | AndSelector | OrSelector], tiers: Tier[], excludeGiftTags?: string[], stackTiers?: boolean);
33
41
  /**
34
42
  * Calculate cart subtotal excluding gift items
35
43
  */
36
44
  private calculateCartSubtotal;
37
45
  /**
38
- * Find the highest qualifying tier based on cart subtotal
46
+ * Find all qualifying tiers based on cart subtotal
47
+ * Returns tiers in descending order by threshold (highest first)
39
48
  */
40
- private findQualifyingTier;
49
+ private findQualifyingTiers;
41
50
  /**
42
51
  * Filter line items that match the tier's product criteria
43
52
  */
@@ -1 +1 @@
1
- {"version":3,"file":"MultiTierDiscount.d.ts","sourceRoot":"","sources":["../../src/lineItem/MultiTierDiscount.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EACV,iBAAiB,EACjB,SAAS,EACV,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,WAAW,IAAI;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,iBAAkB,SAAQ,QAAQ;IAC7C,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,eAAe,EAAE,MAAM,EAAE,CAAC;gBAGxB,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,CAAC,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC,EAClD,KAAK,EAAE,IAAI,EAAE,EACb,eAAe,GAAE,MAAM,EAA0B;IASnD;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA8B7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAS1B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA+B9B,GAAG,CAAC,YAAY,EAAE,YAAY,GAAG,YAAY;CA2E9C"}
1
+ {"version":3,"file":"MultiTierDiscount.d.ts","sourceRoot":"","sources":["../../src/lineItem/MultiTierDiscount.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EACV,iBAAiB,EACjB,SAAS,EACV,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,WAAW,IAAI;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,iBAAkB,SAAQ,QAAQ;IAC7C,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,EAAE,OAAO,CAAC;gBAGlB,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,CAAC,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC,EAClD,KAAK,EAAE,IAAI,EAAE,EACb,eAAe,GAAE,MAAM,EAA0B,EACjD,UAAU,GAAE,OAAc;IAU5B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAyD7B;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA+B9B,GAAG,CAAC,YAAY,EAAE,YAAY,GAAG,YAAY;CAqF9C"}
@@ -6,24 +6,33 @@ const Campaign_1 = require("../common/Campaign");
6
6
  * MultiTierDiscount - Applies tiered discounts based on cart subtotal
7
7
  *
8
8
  * This campaign evaluates the cart total (excluding gifts) and applies
9
- * the discount from the highest qualifying tier.
9
+ * discounts based on the stackTiers configuration:
10
10
  *
11
- * Example use case: Tiered gift with purchase
12
- * - Spend $75: Get free product A
13
- * - Spend $100: Get free product B
14
- * - Spend $250: Get free product C
11
+ * - stackTiers: true (default) - Applies ALL qualifying tiers (cumulative)
12
+ * Example: Spend $150 Get products from both $75 and $100 tiers
15
13
  *
16
- * Only the highest qualifying tier is applied (e.g., if cart is $150,
17
- * only the $100 tier applies, not both $75 and $100).
14
+ * - stackTiers: false - Applies ONLY the highest qualifying tier
15
+ * Example: Spend $150 Get product from $100 tier only
16
+ *
17
+ * Subtotal Calculation:
18
+ * - Automatically excludes items tagged with excludeGiftTags
19
+ * - Automatically excludes tier reward products (by ID or tag)
20
+ * This prevents gaming the system by manually adding gift products
21
+ *
22
+ * Use cases:
23
+ * - Cumulative GWP: Spend more, get more gifts (stackTiers: true)
24
+ * - Bundle upgrades: Spend more, get better bundle (stackTiers: false)
18
25
  */
19
26
  class MultiTierDiscount extends Campaign_1.Campaign {
20
27
  tiers;
21
28
  excludeGiftTags;
22
- constructor(behavior, qualifiers, tiers, excludeGiftTags = ['meta-exclude-gift']) {
29
+ stackTiers;
30
+ constructor(behavior, qualifiers, tiers, excludeGiftTags = ['meta-exclude-gift'], stackTiers = true) {
23
31
  super(behavior, qualifiers);
24
32
  // Sort tiers by threshold descending (highest first)
25
33
  this.tiers = [...tiers].sort((a, b) => b.threshold - a.threshold);
26
34
  this.excludeGiftTags = excludeGiftTags;
35
+ this.stackTiers = stackTiers;
27
36
  }
28
37
  /**
29
38
  * Calculate cart subtotal excluding gift items
@@ -38,6 +47,24 @@ class MultiTierDiscount extends Campaign_1.Campaign {
38
47
  const variant = line.merchandise;
39
48
  // Check if product has any of the exclude tags
40
49
  isExcludedGift = this.excludeGiftTags.some(tag => variant.product.hasTags.filter((t) => t.hasTag && t.tag === tag).length > 0);
50
+ // Also exclude if this product is a tier reward product
51
+ // This prevents gaming the system by manually adding gift products
52
+ if (!isExcludedGift) {
53
+ const productId = variant.product.id.split('/').pop();
54
+ isExcludedGift = this.tiers.some(tier => {
55
+ // Check if product ID matches any tier's gift products
56
+ if (tier.productIds && tier.productIds.length > 0) {
57
+ if (tier.productIds.includes(productId || '')) {
58
+ return true;
59
+ }
60
+ }
61
+ // Check if product tags match any tier's gift product tags
62
+ if (tier.productTags && tier.productTags.length > 0) {
63
+ return tier.productTags.some((tag) => variant.product.hasTags.filter((t) => t.hasTag && t.tag === tag).length > 0);
64
+ }
65
+ return false;
66
+ });
67
+ }
41
68
  }
42
69
  if (isExcludedGift) {
43
70
  continue;
@@ -49,15 +76,11 @@ class MultiTierDiscount extends Campaign_1.Campaign {
49
76
  return subtotal;
50
77
  }
51
78
  /**
52
- * Find the highest qualifying tier based on cart subtotal
79
+ * Find all qualifying tiers based on cart subtotal
80
+ * Returns tiers in descending order by threshold (highest first)
53
81
  */
54
- findQualifyingTier(cartSubtotal) {
55
- for (const tier of this.tiers) {
56
- if (cartSubtotal >= tier.threshold) {
57
- return tier;
58
- }
59
- }
60
- return null;
82
+ findQualifyingTiers(cartSubtotal) {
83
+ return this.tiers.filter(tier => cartSubtotal >= tier.threshold);
61
84
  }
62
85
  /**
63
86
  * Filter line items that match the tier's product criteria
@@ -89,61 +112,70 @@ class MultiTierDiscount extends Campaign_1.Campaign {
89
112
  }
90
113
  // Calculate cart subtotal (excluding gifts)
91
114
  const cartSubtotal = this.calculateCartSubtotal(discountCart);
92
- // Find the highest qualifying tier
93
- const qualifyingTier = this.findQualifyingTier(cartSubtotal);
94
- if (!qualifyingTier) {
115
+ // Find qualifying tiers based on stackTiers configuration
116
+ const allQualifyingTiers = this.findQualifyingTiers(cartSubtotal);
117
+ if (allQualifyingTiers.length === 0) {
95
118
  // Cart doesn't meet minimum threshold
96
119
  return discountCart;
97
120
  }
98
- // Get line items that match this tier's product criteria
99
- const applicableLineItems = this.getApplicableLineItems(discountCart, qualifyingTier);
100
- if (applicableLineItems.length === 0) {
101
- // No matching products found
102
- return discountCart;
103
- }
104
- // Sort by price (ascending) to discount cheapest items first
105
- applicableLineItems.sort((a, b) => {
106
- const priceA = parseFloat(String(a.cost.amountPerQuantity.amount));
107
- const priceB = parseFloat(String(b.cost.amountPerQuantity.amount));
108
- return priceA - priceB;
109
- });
110
- const itemsToApplyDiscounts = [];
111
- let remainingDiscounts = qualifyingTier.itemsToDiscount;
112
- // Apply discount to matching items up to the limit
113
- applicableLineItems.forEach((lineItem) => {
114
- if (remainingDiscounts === 0)
115
- return;
116
- if (remainingDiscounts !== undefined) {
117
- // Apply to as many items as possible up to the limit
118
- if (lineItem.quantity >= remainingDiscounts) {
119
- itemsToApplyDiscounts.push({
120
- lineItem,
121
- maxDiscounts: remainingDiscounts,
122
- });
123
- remainingDiscounts = 0;
121
+ // Select tiers to apply based on stackTiers configuration
122
+ // Note: allQualifyingTiers is sorted by threshold descending (from constructor)
123
+ // so allQualifyingTiers[0] is the highest qualifying tier
124
+ const tiersToApply = this.stackTiers
125
+ ? allQualifyingTiers // Apply all qualifying tiers (cumulative)
126
+ : [allQualifyingTiers[0]]; // Apply only highest tier
127
+ // Apply each selected tier
128
+ for (const tier of tiersToApply) {
129
+ // Get line items that match this tier's product criteria
130
+ const applicableLineItems = this.getApplicableLineItems(discountCart, tier);
131
+ if (applicableLineItems.length === 0) {
132
+ // No matching products found for this tier
133
+ continue;
134
+ }
135
+ // Sort by price (ascending) to discount cheapest items first
136
+ applicableLineItems.sort((a, b) => {
137
+ const priceA = parseFloat(String(a.cost.amountPerQuantity.amount));
138
+ const priceB = parseFloat(String(b.cost.amountPerQuantity.amount));
139
+ return priceA - priceB;
140
+ });
141
+ const itemsToApplyDiscounts = [];
142
+ let remainingDiscounts = tier.itemsToDiscount;
143
+ // Apply discount to matching items up to the limit
144
+ applicableLineItems.forEach((lineItem) => {
145
+ if (remainingDiscounts === 0)
146
+ return;
147
+ if (remainingDiscounts !== undefined) {
148
+ // Apply to as many items as possible up to the limit
149
+ if (lineItem.quantity >= remainingDiscounts) {
150
+ itemsToApplyDiscounts.push({
151
+ lineItem,
152
+ maxDiscounts: remainingDiscounts,
153
+ });
154
+ remainingDiscounts = 0;
155
+ }
156
+ else {
157
+ // Apply to all items in this line
158
+ itemsToApplyDiscounts.push({
159
+ lineItem,
160
+ maxDiscounts: lineItem.quantity,
161
+ });
162
+ remainingDiscounts -= lineItem.quantity;
163
+ }
124
164
  }
125
165
  else {
126
- // Apply to all items in this line
166
+ // No limit, apply to all matching items
127
167
  itemsToApplyDiscounts.push({
128
168
  lineItem,
129
169
  maxDiscounts: lineItem.quantity,
130
170
  });
131
- remainingDiscounts -= lineItem.quantity;
132
171
  }
133
- }
134
- else {
135
- // No limit, apply to all matching items
136
- itemsToApplyDiscounts.push({
137
- lineItem,
138
- maxDiscounts: lineItem.quantity,
139
- });
140
- }
141
- });
142
- // Apply the tier's discount to all selected items
143
- itemsToApplyDiscounts.forEach((item) => {
144
- const discount = qualifyingTier.discount;
145
- discount.apply([item]);
146
- });
172
+ });
173
+ // Apply the tier's discount to all selected items
174
+ itemsToApplyDiscounts.forEach((item) => {
175
+ const discount = tier.discount;
176
+ discount.apply([item]);
177
+ });
178
+ }
147
179
  return discountCart;
148
180
  }
149
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoxllc/shopify-checkout-extensions",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Configuration-driven runtime engine for Shopify checkout extensions - campaigns, discounts, qualifiers, and selectors",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",