@zoxllc/shopify-checkout-extensions 0.1.0 → 0.2.0

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,6 +360,85 @@ export function run(input) {
360
360
 
361
361
  ---
362
362
 
363
+ ## Example 11: Multi-Tier Gift with Purchase
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.
366
+
367
+ ```json
368
+ {
369
+ "__type": "MultiTierDiscount",
370
+ "label": "Winter 2025 Tiered GWP",
371
+ "active": true,
372
+ "description": "Spend more, get better gifts!",
373
+ "inputs": [
374
+ ":all",
375
+ [],
376
+ [
377
+ {
378
+ "threshold": 75,
379
+ "discount": {
380
+ "__type": "PercentageDiscount",
381
+ "inputs": [100, "Free Colorwheel F&F"]
382
+ },
383
+ "productIds": ["6638547533896"],
384
+ "itemsToDiscount": 1,
385
+ "label": "Free Colorwheel F&F"
386
+ },
387
+ {
388
+ "threshold": 100,
389
+ "discount": {
390
+ "__type": "PercentageDiscount",
391
+ "inputs": [100, "Free Goldie Mystery ZOX"]
392
+ },
393
+ "productIds": ["6640591011912"],
394
+ "itemsToDiscount": 1,
395
+ "label": "Free Goldie Mystery ZOX"
396
+ },
397
+ {
398
+ "threshold": 250,
399
+ "discount": {
400
+ "__type": "PercentageDiscount",
401
+ "inputs": [100, "Free Cosmic Drift Daily"]
402
+ },
403
+ "productIds": ["6980288053320"],
404
+ "itemsToDiscount": 1,
405
+ "label": "Free Cosmic Drift Daily"
406
+ }
407
+ ],
408
+ ["meta-exclude-gift"]
409
+ ]
410
+ }
411
+ ```
412
+
413
+ **How It Works:**
414
+ - Customer adds items to cart totaling $150
415
+ - 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
421
+
422
+ **Key Features:**
423
+ - Automatically excludes gift items from threshold calculation
424
+ - Only applies the highest qualifying tier (no stacking)
425
+ - Supports product IDs or product tags per tier
426
+ - Perfect for seasonal promotions with start/end dates
427
+
428
+ ### MultiTierDiscount Inputs Array:
429
+ 1. **QualifierBehavior** - `:all` or `:any`
430
+ 2. **Qualifiers Array** - Array of cart-level qualifiers (optional, can be empty `[]`)
431
+ 3. **Tiers Array** - Array of tier objects, each containing:
432
+ - `threshold` - Minimum cart amount to qualify
433
+ - `discount` - Discount configuration (typically 100% for free gifts)
434
+ - `productIds` - Array of product IDs to discount (optional)
435
+ - `productTags` - Array of product tags to discount (optional)
436
+ - `itemsToDiscount` - Maximum items to discount (optional, defaults to all)
437
+ - `label` - Display name for the tier (optional)
438
+ 4. **Exclude Tags** - Array of product tags to exclude from subtotal calculation (default: `["meta-exclude-gift"]`)
439
+
440
+ ---
441
+
363
442
  ## Tips for Your UI App
364
443
 
365
444
  1. **Store configurations in your database** with fields:
@@ -9,7 +9,7 @@ export type InputConfig = {
9
9
  inputs: (boolean | number | string | InputConfig | boolean[] | number[] | string[] | InputConfig[])[];
10
10
  };
11
11
  export type CampaignInputConfig = InputConfig & {
12
- __type: 'BuyXGetY' | 'ConditionalDiscount' | 'ShippingDiscount';
12
+ __type: 'BuyXGetY' | 'ConditionalDiscount' | 'ShippingDiscount' | 'MultiTierDiscount';
13
13
  label: string;
14
14
  active: boolean;
15
15
  description?: string;
@@ -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;AAgC5C,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,CAAC;IAChE,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;CAkHd"}
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"}
@@ -17,6 +17,7 @@ const ConditionalDiscount_1 = require("../lineItem/ConditionalDiscount");
17
17
  const FixedItemDiscount_1 = require("../lineItem/FixedItemDiscount");
18
18
  const PercentageDiscount_1 = require("../lineItem/PercentageDiscount");
19
19
  const BuyXGetY_1 = require("../lineItem/BuyXGetY");
20
+ const MultiTierDiscount_1 = require("../lineItem/MultiTierDiscount");
20
21
  const SubscriptionItemSelector_1 = require("./SubscriptionItemSelector");
21
22
  const CountryCodeQualifier_1 = require("./CountryCodeQualifier");
22
23
  const RateNameSelector_1 = require("../shipping/RateNameSelector");
@@ -53,6 +54,8 @@ class CampaignFactory {
53
54
  return new BuyXGetY_1.BuyXGetY(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
54
55
  case 'ShippingDiscount':
55
56
  return new ShippingDiscount_1.ShippingDiscount(args[0], args[1], args[2], args[3]);
57
+ case 'MultiTierDiscount':
58
+ return new MultiTierDiscount_1.MultiTierDiscount(args[0], args[1], args[2], args[3]);
56
59
  case 'AndSelector':
57
60
  return new AndSelector_1.AndSelector([...args]);
58
61
  case 'OrSelector':
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { ConditionalDiscount } from './lineItem/ConditionalDiscount';
2
2
  export { BuyXGetY } from './lineItem/BuyXGetY';
3
3
  export { ShippingDiscount } from './shipping/ShippingDiscount';
4
+ export { MultiTierDiscount, type Tier } from './lineItem/MultiTierDiscount';
4
5
  export { Campaign } from './common/Campaign';
5
6
  export { FixedItemDiscount } from './lineItem/FixedItemDiscount';
6
7
  export { PercentageDiscount } from './lineItem/PercentageDiscount';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAGrE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EACL,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,EACtB,KAAK,wBAAwB,GAC9B,MAAM,uBAAuB,CAAC;AAG/B,YAAY,EACV,YAAY,EACZ,qBAAqB,GACtB,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,KAAK,IAAI,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAGrE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EACL,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,EACtB,KAAK,wBAAwB,GAC9B,MAAM,uBAAuB,CAAC;AAG/B,YAAY,EACV,YAAY,EACZ,qBAAqB,GACtB,MAAM,gCAAgC,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StringComparisonSchema = exports.NumericalComparisonSchema = exports.QualifierMatchTypeSchema = exports.MatchTypeSchema = exports.QualifierBehaviorSchema = exports.CampaignConfigSchema = exports.ConfigValidator = exports.CampaignFactory = exports.DiscountCart = exports.CountryCodeQualifier = exports.PostCartAmountQualifier = exports.CustomerTagQualifier = exports.CustomerSubscriberQualifier = exports.CustomerEmailQualifier = exports.CartQuantityQualifier = exports.CartHasItemQualifier = exports.CartAmountQualifier = exports.StringComparisonType = exports.NumericalComparisonType = exports.QualifierMatchType = exports.QualifierBehavior = exports.Qualifier = exports.RateNameSelector = exports.SubscriptionItemSelector = exports.SaleItemSelector = exports.ProductTypeSelector = exports.ProductTagSelector = exports.ProductIdSelector = exports.ProductHandleSelector = exports.Selector = exports.OrSelector = exports.AndSelector = exports.FixedDiscount = exports.PercentageDiscount = exports.FixedItemDiscount = exports.Campaign = exports.ShippingDiscount = exports.BuyXGetY = exports.ConditionalDiscount = void 0;
3
+ exports.StringComparisonSchema = exports.NumericalComparisonSchema = exports.QualifierMatchTypeSchema = exports.MatchTypeSchema = exports.QualifierBehaviorSchema = exports.CampaignConfigSchema = exports.ConfigValidator = exports.CampaignFactory = exports.DiscountCart = exports.CountryCodeQualifier = exports.PostCartAmountQualifier = exports.CustomerTagQualifier = exports.CustomerSubscriberQualifier = exports.CustomerEmailQualifier = exports.CartQuantityQualifier = exports.CartHasItemQualifier = exports.CartAmountQualifier = exports.StringComparisonType = exports.NumericalComparisonType = exports.QualifierMatchType = exports.QualifierBehavior = exports.Qualifier = exports.RateNameSelector = exports.SubscriptionItemSelector = exports.SaleItemSelector = exports.ProductTypeSelector = exports.ProductTagSelector = exports.ProductIdSelector = exports.ProductHandleSelector = exports.Selector = exports.OrSelector = exports.AndSelector = exports.FixedDiscount = exports.PercentageDiscount = exports.FixedItemDiscount = exports.Campaign = exports.MultiTierDiscount = exports.ShippingDiscount = exports.BuyXGetY = exports.ConditionalDiscount = void 0;
4
4
  // Campaign Types
5
5
  var ConditionalDiscount_1 = require("./lineItem/ConditionalDiscount");
6
6
  Object.defineProperty(exports, "ConditionalDiscount", { enumerable: true, get: function () { return ConditionalDiscount_1.ConditionalDiscount; } });
@@ -8,6 +8,8 @@ var BuyXGetY_1 = require("./lineItem/BuyXGetY");
8
8
  Object.defineProperty(exports, "BuyXGetY", { enumerable: true, get: function () { return BuyXGetY_1.BuyXGetY; } });
9
9
  var ShippingDiscount_1 = require("./shipping/ShippingDiscount");
10
10
  Object.defineProperty(exports, "ShippingDiscount", { enumerable: true, get: function () { return ShippingDiscount_1.ShippingDiscount; } });
11
+ var MultiTierDiscount_1 = require("./lineItem/MultiTierDiscount");
12
+ Object.defineProperty(exports, "MultiTierDiscount", { enumerable: true, get: function () { return MultiTierDiscount_1.MultiTierDiscount; } });
11
13
  var Campaign_1 = require("./common/Campaign");
12
14
  Object.defineProperty(exports, "Campaign", { enumerable: true, get: function () { return Campaign_1.Campaign; } });
13
15
  // Discount Types
@@ -0,0 +1,47 @@
1
+ import { Campaign } from '../common/Campaign';
2
+ import type { DiscountCart } from '../common/DiscountCart';
3
+ import type { DiscountInterface } from '../common/DiscountInterface';
4
+ import type { QualifierBehavior, Qualifier } from '../common/Qualifier';
5
+ import type { AndSelector } from '../common/AndSelector';
6
+ import type { OrSelector } from '../common/OrSelector';
7
+ export interface Tier {
8
+ threshold: number;
9
+ discount: DiscountInterface;
10
+ productIds?: string[];
11
+ productTags?: string[];
12
+ itemsToDiscount?: number;
13
+ label?: string;
14
+ }
15
+ /**
16
+ * MultiTierDiscount - Applies tiered discounts based on cart subtotal
17
+ *
18
+ * This campaign evaluates the cart total (excluding gifts) and applies
19
+ * the discount from the highest qualifying tier.
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
25
+ *
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).
28
+ */
29
+ export declare class MultiTierDiscount extends Campaign {
30
+ tiers: Tier[];
31
+ excludeGiftTags: string[];
32
+ constructor(behavior: QualifierBehavior, qualifiers: [Qualifier | AndSelector | OrSelector], tiers: Tier[], excludeGiftTags?: string[]);
33
+ /**
34
+ * Calculate cart subtotal excluding gift items
35
+ */
36
+ private calculateCartSubtotal;
37
+ /**
38
+ * Find the highest qualifying tier based on cart subtotal
39
+ */
40
+ private findQualifyingTier;
41
+ /**
42
+ * Filter line items that match the tier's product criteria
43
+ */
44
+ private getApplicableLineItems;
45
+ run(discountCart: DiscountCart): DiscountCart;
46
+ }
47
+ //# sourceMappingURL=MultiTierDiscount.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MultiTierDiscount = void 0;
4
+ const Campaign_1 = require("../common/Campaign");
5
+ /**
6
+ * MultiTierDiscount - Applies tiered discounts based on cart subtotal
7
+ *
8
+ * This campaign evaluates the cart total (excluding gifts) and applies
9
+ * the discount from the highest qualifying tier.
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
15
+ *
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).
18
+ */
19
+ class MultiTierDiscount extends Campaign_1.Campaign {
20
+ tiers;
21
+ excludeGiftTags;
22
+ constructor(behavior, qualifiers, tiers, excludeGiftTags = ['meta-exclude-gift']) {
23
+ super(behavior, qualifiers);
24
+ // Sort tiers by threshold descending (highest first)
25
+ this.tiers = [...tiers].sort((a, b) => b.threshold - a.threshold);
26
+ this.excludeGiftTags = excludeGiftTags;
27
+ }
28
+ /**
29
+ * Calculate cart subtotal excluding gift items
30
+ */
31
+ calculateCartSubtotal(discountCart) {
32
+ let subtotal = 0;
33
+ for (const line of discountCart.cart.lines) {
34
+ // Check if this item should be excluded from subtotal calculation
35
+ let isExcludedGift = false;
36
+ // Type guard: Only ProductVariant has product property
37
+ if (line.merchandise.__typename === 'ProductVariant') {
38
+ const variant = line.merchandise;
39
+ // Check if product has any of the exclude tags
40
+ isExcludedGift = this.excludeGiftTags.some(tag => variant.product.hasTags.filter((t) => t.hasTag && t.tag === tag).length > 0);
41
+ }
42
+ if (isExcludedGift) {
43
+ continue;
44
+ }
45
+ // Add line total to subtotal
46
+ const lineTotal = parseFloat(String(line.cost.totalAmount.amount));
47
+ subtotal += lineTotal;
48
+ }
49
+ return subtotal;
50
+ }
51
+ /**
52
+ * Find the highest qualifying tier based on cart subtotal
53
+ */
54
+ findQualifyingTier(cartSubtotal) {
55
+ for (const tier of this.tiers) {
56
+ if (cartSubtotal >= tier.threshold) {
57
+ return tier;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ /**
63
+ * Filter line items that match the tier's product criteria
64
+ */
65
+ getApplicableLineItems(discountCart, tier) {
66
+ return discountCart.cart.lines.filter((line) => {
67
+ // Type guard: Only ProductVariant has product property
68
+ if (line.merchandise.__typename !== 'ProductVariant') {
69
+ return false;
70
+ }
71
+ // If tier specifies product IDs, match against those
72
+ if (tier.productIds && tier.productIds.length > 0) {
73
+ const productId = line.merchandise.product.id.split('/').pop();
74
+ return tier.productIds.includes(productId || '');
75
+ }
76
+ // If tier specifies product tags, match against those
77
+ if (tier.productTags && tier.productTags.length > 0) {
78
+ const variant = line.merchandise;
79
+ return tier.productTags.some((tag) => variant.product.hasTags.filter((t) => t.hasTag && t.tag === tag).length > 0);
80
+ }
81
+ // If no product criteria specified, apply to all items
82
+ return true;
83
+ });
84
+ }
85
+ run(discountCart) {
86
+ // First check if cart qualifies for any discount
87
+ if (!this.qualifies(discountCart)) {
88
+ return discountCart;
89
+ }
90
+ // Calculate cart subtotal (excluding gifts)
91
+ const cartSubtotal = this.calculateCartSubtotal(discountCart);
92
+ // Find the highest qualifying tier
93
+ const qualifyingTier = this.findQualifyingTier(cartSubtotal);
94
+ if (!qualifyingTier) {
95
+ // Cart doesn't meet minimum threshold
96
+ return discountCart;
97
+ }
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;
124
+ }
125
+ else {
126
+ // Apply to all items in this line
127
+ itemsToApplyDiscounts.push({
128
+ lineItem,
129
+ maxDiscounts: lineItem.quantity,
130
+ });
131
+ remainingDiscounts -= lineItem.quantity;
132
+ }
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
+ });
147
+ return discountCart;
148
+ }
149
+ }
150
+ exports.MultiTierDiscount = MultiTierDiscount;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoxllc/shopify-checkout-extensions",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",