@zoxllc/shopify-checkout-extensions 0.0.8 → 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.
Files changed (164) hide show
  1. package/CLAUDE.md +136 -0
  2. package/EXAMPLES.md +463 -0
  3. package/INTEGRATION.md +482 -0
  4. package/README.md +5 -1
  5. package/dist/common/AndSelector.d.ts +11 -0
  6. package/dist/common/AndSelector.d.ts.map +1 -0
  7. package/dist/common/AndSelector.js +30 -0
  8. package/dist/common/Campaign.d.ts +24 -0
  9. package/dist/common/Campaign.d.ts.map +1 -0
  10. package/dist/common/Campaign.js +41 -0
  11. package/dist/common/CampaignConfiguration.d.ts +10 -0
  12. package/dist/common/CampaignConfiguration.d.ts.map +1 -0
  13. package/dist/common/CampaignConfiguration.js +7 -0
  14. package/dist/common/CampaignFactory.d.ts +22 -0
  15. package/dist/common/CampaignFactory.d.ts.map +1 -0
  16. package/dist/common/CampaignFactory.js +98 -0
  17. package/dist/common/CartAmountQualifier.d.ts +22 -0
  18. package/dist/common/CartAmountQualifier.d.ts.map +1 -0
  19. package/dist/common/CartAmountQualifier.js +46 -0
  20. package/dist/common/CartHasItemQualifier.d.ts +21 -0
  21. package/dist/common/CartHasItemQualifier.d.ts.map +1 -0
  22. package/dist/common/CartHasItemQualifier.js +54 -0
  23. package/dist/common/CartQuantityQualifier.d.ts +22 -0
  24. package/dist/common/CartQuantityQualifier.d.ts.map +1 -0
  25. package/dist/common/CartQuantityQualifier.js +62 -0
  26. package/dist/common/ConfigSchema.d.ts +71 -0
  27. package/dist/common/ConfigSchema.d.ts.map +1 -0
  28. package/dist/common/ConfigSchema.js +69 -0
  29. package/dist/common/ConfigValidator.d.ts +29 -0
  30. package/dist/common/ConfigValidator.d.ts.map +1 -0
  31. package/dist/common/ConfigValidator.js +84 -0
  32. package/dist/common/CountryCodeQualifier.d.ts +15 -0
  33. package/dist/common/CountryCodeQualifier.d.ts.map +1 -0
  34. package/dist/common/CountryCodeQualifier.js +30 -0
  35. package/dist/common/CustomerEmailQualifier.d.ts +16 -0
  36. package/dist/common/CustomerEmailQualifier.d.ts.map +1 -0
  37. package/dist/common/CustomerEmailQualifier.js +52 -0
  38. package/dist/common/CustomerSubscriberQualifier.d.ts +18 -0
  39. package/dist/common/CustomerSubscriberQualifier.d.ts.map +1 -0
  40. package/dist/common/CustomerSubscriberQualifier.js +28 -0
  41. package/dist/common/CustomerTagQualifier.d.ts +16 -0
  42. package/dist/common/CustomerTagQualifier.d.ts.map +1 -0
  43. package/dist/common/CustomerTagQualifier.js +45 -0
  44. package/dist/common/DiscountCart.d.ts +23 -0
  45. package/dist/common/DiscountCart.d.ts.map +1 -0
  46. package/dist/common/DiscountCart.js +103 -0
  47. package/dist/common/DiscountInterface.d.ts +17 -0
  48. package/dist/common/DiscountInterface.d.ts.map +1 -0
  49. package/dist/common/DiscountInterface.js +2 -0
  50. package/dist/common/OrSelector.d.ts +11 -0
  51. package/dist/common/OrSelector.d.ts.map +1 -0
  52. package/dist/common/OrSelector.js +30 -0
  53. package/dist/common/PostCartAmountQualifier.d.ts +9 -0
  54. package/dist/common/PostCartAmountQualifier.d.ts.map +1 -0
  55. package/dist/common/PostCartAmountQualifier.js +17 -0
  56. package/dist/common/ProductHandleSelector.d.ts +8 -0
  57. package/dist/common/ProductHandleSelector.d.ts.map +1 -0
  58. package/dist/common/ProductHandleSelector.js +22 -0
  59. package/dist/common/ProductIdSelector.d.ts +8 -0
  60. package/dist/common/ProductIdSelector.d.ts.map +1 -0
  61. package/dist/common/ProductIdSelector.js +22 -0
  62. package/dist/common/ProductTagSelector.d.ts +10 -0
  63. package/dist/common/ProductTagSelector.d.ts.map +1 -0
  64. package/dist/common/ProductTagSelector.js +39 -0
  65. package/dist/common/ProductTypeSelector.d.ts +8 -0
  66. package/dist/common/ProductTypeSelector.d.ts.map +1 -0
  67. package/dist/common/ProductTypeSelector.js +25 -0
  68. package/dist/common/Qualifier.d.ts +30 -0
  69. package/dist/common/Qualifier.d.ts.map +1 -0
  70. package/dist/common/Qualifier.js +61 -0
  71. package/dist/common/SaleItemSelector.d.ts +11 -0
  72. package/dist/common/SaleItemSelector.d.ts.map +1 -0
  73. package/dist/common/SaleItemSelector.js +30 -0
  74. package/dist/common/Selector.d.ts +27 -0
  75. package/dist/common/Selector.d.ts.map +1 -0
  76. package/dist/common/Selector.js +51 -0
  77. package/dist/common/SubscriptionItemSelector.d.ts +7 -0
  78. package/dist/common/SubscriptionItemSelector.d.ts.map +1 -0
  79. package/dist/common/SubscriptionItemSelector.js +19 -0
  80. package/dist/form/CampaignForm.d.ts +1 -0
  81. package/dist/form/CampaignForm.d.ts.map +1 -0
  82. package/dist/form/CampaignForm.js +1 -0
  83. package/{src/index.ts → dist/index.d.ts} +23 -27
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +80 -0
  86. package/dist/lineItem/BuyXGetY.d.ts +18 -0
  87. package/dist/lineItem/BuyXGetY.d.ts.map +1 -0
  88. package/dist/lineItem/BuyXGetY.js +85 -0
  89. package/dist/lineItem/ConditionalDiscount.d.ts +14 -0
  90. package/dist/lineItem/ConditionalDiscount.d.ts.map +1 -0
  91. package/dist/lineItem/ConditionalDiscount.js +81 -0
  92. package/dist/lineItem/FixedItemDiscount.d.ts +9 -0
  93. package/dist/lineItem/FixedItemDiscount.d.ts.map +1 -0
  94. package/dist/lineItem/FixedItemDiscount.js +35 -0
  95. package/dist/lineItem/MultiTierDiscount.d.ts +47 -0
  96. package/dist/lineItem/MultiTierDiscount.d.ts.map +1 -0
  97. package/dist/lineItem/MultiTierDiscount.js +150 -0
  98. package/dist/lineItem/PercentageDiscount.d.ts +8 -0
  99. package/dist/lineItem/PercentageDiscount.d.ts.map +1 -0
  100. package/dist/lineItem/PercentageDiscount.js +32 -0
  101. package/dist/lineItem/api.d.ts +2026 -0
  102. package/dist/lineItem/api.d.ts.map +1 -0
  103. package/dist/lineItem/api.js +1157 -0
  104. package/dist/shipping/FixedDiscount.d.ts +8 -0
  105. package/dist/shipping/FixedDiscount.d.ts.map +1 -0
  106. package/dist/shipping/FixedDiscount.js +30 -0
  107. package/dist/shipping/RateNameSelector.d.ts +10 -0
  108. package/dist/shipping/RateNameSelector.d.ts.map +1 -0
  109. package/dist/shipping/RateNameSelector.js +45 -0
  110. package/dist/shipping/ShippingDiscount.d.ts +14 -0
  111. package/dist/shipping/ShippingDiscount.d.ts.map +1 -0
  112. package/dist/shipping/ShippingDiscount.js +48 -0
  113. package/dist/shipping/api.d.ts +2019 -0
  114. package/dist/shipping/api.d.ts.map +1 -0
  115. package/dist/shipping/api.js +1147 -0
  116. package/package.json +26 -7
  117. package/migrate.ts +0 -64
  118. package/src/common/AndSelector.ts +0 -42
  119. package/src/common/Campaign.ts +0 -70
  120. package/src/common/CampaignConfiguration.ts +0 -10
  121. package/src/common/CampaignFactory.ts +0 -233
  122. package/src/common/CartAmountQualifier.ts +0 -64
  123. package/src/common/CartHasItemQualifier.ts +0 -80
  124. package/src/common/CartQuantityQualifier.ts +0 -94
  125. package/src/common/CountryCodeQualifier.ts +0 -49
  126. package/src/common/CustomerEmailQualifier.ts +0 -67
  127. package/src/common/CustomerSubscriberQualifier.ts +0 -32
  128. package/src/common/CustomerTagQualifier.ts +0 -69
  129. package/src/common/DiscountCart.ts +0 -161
  130. package/src/common/DiscountInterface.ts +0 -26
  131. package/src/common/OrSelector.ts +0 -40
  132. package/src/common/PostCartAmountQualifier.ts +0 -21
  133. package/src/common/ProductHandleSelector.ts +0 -32
  134. package/src/common/ProductIdSelector.ts +0 -34
  135. package/src/common/ProductTagSelector.ts +0 -63
  136. package/src/common/ProductTypeSelector.ts +0 -40
  137. package/src/common/Qualifier.ts +0 -67
  138. package/src/common/SaleItemSelector.ts +0 -36
  139. package/src/common/Selector.ts +0 -66
  140. package/src/common/SubscriptionItemSelector.ts +0 -23
  141. package/src/lineItem/BuyXGetY.ts +0 -131
  142. package/src/lineItem/ConditionalDiscount.ts +0 -102
  143. package/src/lineItem/FixedItemDiscount.ts +0 -51
  144. package/src/lineItem/PercentageDiscount.ts +0 -44
  145. package/src/lineItem/api.ts +0 -2103
  146. package/src/shipping/FixedDiscount.ts +0 -37
  147. package/src/shipping/RateNameSelector.ts +0 -54
  148. package/src/shipping/ShippingDiscount.ts +0 -75
  149. package/src/shipping/api.ts +0 -2014
  150. package/tests/AndSelector.test.ts +0 -27
  151. package/tests/CartQuantityQualifier.test.ts +0 -381
  152. package/tests/CountryCodeQualifier.test.ts +0 -55
  153. package/tests/CustomerSubscriberQualifier.test.ts +0 -101
  154. package/tests/CustomerTagQualifier.test.ts +0 -71
  155. package/tests/DiscountCart.test.ts +0 -115
  156. package/tests/OrSelector.test.ts +0 -27
  157. package/tests/ProductIdSelector.test.ts +0 -83
  158. package/tests/ProductTagSelector.test.ts +0 -75
  159. package/tests/Qualifier.test.ts +0 -193
  160. package/tests/RateNameSelector.test.ts +0 -107
  161. package/tests/SaleItemSelector.test.ts +0 -113
  162. package/tests/Selector.test.ts +0 -83
  163. package/tests/ShippingDiscount.test.ts +0 -147
  164. package/tsconfig.json +0 -25
package/INTEGRATION.md ADDED
@@ -0,0 +1,482 @@
1
+ # Integration Guide
2
+
3
+ This guide shows how to integrate this library into your Shopify app ecosystem.
4
+
5
+ ## Architecture Overview
6
+
7
+ ```
8
+ ┌──────────────────────────────────────────────────────────────┐
9
+ │ Your Shopify App │
10
+ │ ┌────────────────────────────────────────────────────────┐ │
11
+ │ │ Admin UI (React/etc) │ │
12
+ │ │ • Campaign builder forms │ │
13
+ │ │ • Preview discount results │ │
14
+ │ │ • Manage campaigns (CRUD) │ │
15
+ │ └────────────────────────────────────────────────────────┘ │
16
+ │ │ │
17
+ │ ↓ │
18
+ │ ┌────────────────────────────────────────────────────────┐ │
19
+ │ │ Backend API (Node/Rails/etc) │ │
20
+ │ │ • REST/GraphQL API │ │
21
+ │ │ • Database: stores campaign configs as JSON │ │
22
+ │ │ • Shopify API: writes configs to metafields │ │
23
+ │ └────────────────────────────────────────────────────────┘ │
24
+ └──────────────────────────────────────────────────────────────┘
25
+
26
+ ↓ (stores config)
27
+ ┌──────────────────────────────────────────────────────────────┐
28
+ │ Shopify Admin (Discounts) │
29
+ │ • Discount metafield: { value: JSON.stringify(config) } │
30
+ └──────────────────────────────────────────────────────────────┘
31
+
32
+ ↓ (reads config)
33
+ ┌──────────────────────────────────────────────────────────────┐
34
+ │ Shopify Function (Discount Extension) │
35
+ │ • Imports this library │
36
+ │ • Reads metafield from input.discountNode.metafield.value │
37
+ │ • Creates campaign via CampaignFactory │
38
+ │ • Runs discount logic │
39
+ │ • Returns discounts to Shopify │
40
+ └──────────────────────────────────────────────────────────────┘
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Step 1: Database Schema
46
+
47
+ In your Shopify app database:
48
+
49
+ ```sql
50
+ CREATE TABLE campaign_configs (
51
+ id SERIAL PRIMARY KEY,
52
+ shop_id VARCHAR(255) NOT NULL,
53
+ shopify_discount_id VARCHAR(255), -- The Shopify discount ID
54
+ campaign_config JSONB NOT NULL, -- The campaign configuration
55
+ priority INTEGER DEFAULT 0,
56
+ active BOOLEAN DEFAULT true,
57
+ created_at TIMESTAMP DEFAULT NOW(),
58
+ updated_at TIMESTAMP DEFAULT NOW(),
59
+ UNIQUE(shop_id, shopify_discount_id)
60
+ );
61
+
62
+ CREATE INDEX idx_campaign_configs_shop ON campaign_configs(shop_id);
63
+ CREATE INDEX idx_campaign_configs_active ON campaign_configs(active);
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Step 2: Your Shopify App UI
69
+
70
+ ### Install the library
71
+
72
+ ```bash
73
+ npm install @zoxllc/shopify-checkout-extensions
74
+ ```
75
+
76
+ ### Validate configurations before saving
77
+
78
+ ```typescript
79
+ import { ConfigValidator } from '@zoxllc/shopify-checkout-extensions';
80
+
81
+ // In your API route
82
+ app.post('/api/campaigns', async (req, res) => {
83
+ const { config } = req.body;
84
+
85
+ // Validate the configuration
86
+ const validation = ConfigValidator.validate(config);
87
+ if (!validation.valid) {
88
+ return res.status(400).json({
89
+ error: 'Invalid configuration',
90
+ details: validation.errors,
91
+ });
92
+ }
93
+
94
+ // Save to database
95
+ const campaign = await db.campaignConfigs.create({
96
+ shop_id: req.session.shop,
97
+ campaign_config: config,
98
+ active: true,
99
+ });
100
+
101
+ // Create Shopify discount and store config in metafield
102
+ const shopifyDiscount = await createShopifyDiscount(campaign);
103
+
104
+ res.json({ success: true, campaign });
105
+ });
106
+ ```
107
+
108
+ ### Create Shopify Discount with Metafield
109
+
110
+ ```typescript
111
+ import { shopifyApi } from '@shopify/shopify-api';
112
+
113
+ async function createShopifyDiscount(campaign: CampaignConfig) {
114
+ const client = new shopifyApi.clients.Graphql({ session });
115
+
116
+ const mutation = `
117
+ mutation CreateDiscount($discount: DiscountAutomaticAppInput!) {
118
+ discountAutomaticAppCreate(automaticAppDiscount: $discount) {
119
+ automaticAppDiscount {
120
+ discountId
121
+ title
122
+ }
123
+ userErrors {
124
+ field
125
+ message
126
+ }
127
+ }
128
+ }
129
+ `;
130
+
131
+ const response = await client.query({
132
+ data: {
133
+ query: mutation,
134
+ variables: {
135
+ discount: {
136
+ title: campaign.campaign_config.label,
137
+ functionId: 'YOUR_FUNCTION_ID', // From Shopify Partners dashboard
138
+ startsAt: new Date().toISOString(),
139
+ metafields: [
140
+ {
141
+ namespace: 'campaign',
142
+ key: 'config',
143
+ type: 'json',
144
+ value: JSON.stringify(campaign.campaign_config),
145
+ },
146
+ ],
147
+ },
148
+ },
149
+ },
150
+ });
151
+
152
+ return response.body.data.discountAutomaticAppCreate.automaticAppDiscount;
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Step 3: Shopify Function Integration
159
+
160
+ ### In your Shopify Function project
161
+
162
+ ```bash
163
+ npm install @zoxllc/shopify-checkout-extensions
164
+ ```
165
+
166
+ ### Use the library in your function
167
+
168
+ ```typescript
169
+ // src/index.ts
170
+ import { CampaignFactory, DiscountCart } from '@zoxllc/shopify-checkout-extensions';
171
+ import type { RunInput, FunctionRunResult } from './generated/api';
172
+
173
+ export function run(input: RunInput): FunctionRunResult {
174
+ // Read configuration from metafield
175
+ const configValue = input.discountNode.metafield?.value;
176
+
177
+ if (!configValue) {
178
+ console.error('No configuration found in metafield');
179
+ return { discounts: [] };
180
+ }
181
+
182
+ try {
183
+ // Parse configuration
184
+ const config = JSON.parse(configValue);
185
+
186
+ // Create campaign from configuration
187
+ const campaign = CampaignFactory.createFromConfig(config);
188
+
189
+ // Wrap cart in DiscountCart
190
+ const discountCart = new DiscountCart(input.cart);
191
+
192
+ // Run the campaign
193
+ campaign.runWithHooks(discountCart);
194
+
195
+ // Return discounts
196
+ return {
197
+ discounts: discountCart.discounts,
198
+ };
199
+ } catch (error) {
200
+ console.error('Error running campaign:', error);
201
+ return { discounts: [] };
202
+ }
203
+ }
204
+ ```
205
+
206
+ ### Function GraphQL Schema
207
+
208
+ Make sure your function's `input.graphql` includes the metafield:
209
+
210
+ ```graphql
211
+ query RunInput {
212
+ discountNode {
213
+ metafield(namespace: "campaign", key: "config") {
214
+ value
215
+ }
216
+ }
217
+ cart {
218
+ # ... rest of cart fields
219
+ }
220
+ }
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Step 4: Building the UI (Campaign Builder)
226
+
227
+ ### Example React Component
228
+
229
+ ```typescript
230
+ import React, { useState } from 'react';
231
+ import { ConfigValidator } from '@zoxllc/shopify-checkout-extensions';
232
+
233
+ function CampaignBuilder() {
234
+ const [config, setConfig] = useState({
235
+ __type: 'ConditionalDiscount',
236
+ label: '',
237
+ active: true,
238
+ inputs: [],
239
+ });
240
+
241
+ const handleSave = async () => {
242
+ // Validate
243
+ const validation = ConfigValidator.validate(config);
244
+ if (!validation.valid) {
245
+ alert('Invalid configuration: ' + JSON.stringify(validation.errors));
246
+ return;
247
+ }
248
+
249
+ // Save to your API
250
+ const response = await fetch('/api/campaigns', {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({ config }),
254
+ });
255
+
256
+ if (response.ok) {
257
+ alert('Campaign saved successfully!');
258
+ }
259
+ };
260
+
261
+ return (
262
+ <div>
263
+ <h1>Create Campaign</h1>
264
+ <input
265
+ value={config.label}
266
+ onChange={(e) => setConfig({ ...config, label: e.target.value })}
267
+ placeholder="Campaign Label"
268
+ />
269
+ {/* Add more form fields for building the config */}
270
+ <button onClick={handleSave}>Save Campaign</button>
271
+ </div>
272
+ );
273
+ }
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Step 5: Preview Functionality
279
+
280
+ You can test campaigns client-side before saving:
281
+
282
+ ```typescript
283
+ import {
284
+ CampaignFactory,
285
+ DiscountCart,
286
+ } from '@zoxllc/shopify-checkout-extensions';
287
+
288
+ function previewCampaign(config, sampleCart) {
289
+ try {
290
+ // Create campaign
291
+ const campaign = CampaignFactory.createFromConfig(config);
292
+
293
+ // Create discount cart from sample
294
+ const discountCart = new DiscountCart(sampleCart);
295
+
296
+ // Run campaign
297
+ campaign.runWithHooks(discountCart);
298
+
299
+ // Return preview results
300
+ return {
301
+ success: true,
302
+ discounts: discountCart.discounts,
303
+ appliedDiscountTotal: discountCart.appliedDiscountTotal,
304
+ };
305
+ } catch (error) {
306
+ return {
307
+ success: false,
308
+ error: error.message,
309
+ };
310
+ }
311
+ }
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Complete Flow Example
317
+
318
+ ### 1. User creates campaign in your app
319
+
320
+ ```typescript
321
+ // User fills out form
322
+ const campaignConfig = {
323
+ __type: 'ConditionalDiscount',
324
+ label: 'VIP 20% Off',
325
+ active: true,
326
+ inputs: [
327
+ ':all',
328
+ [
329
+ {
330
+ __type: 'CustomerTagQualifier',
331
+ inputs: [':does', ':match', ['VIP']],
332
+ },
333
+ ],
334
+ {
335
+ __type: 'PercentageDiscount',
336
+ inputs: [20, 'VIP Discount'],
337
+ },
338
+ null,
339
+ 0,
340
+ ],
341
+ };
342
+
343
+ // Validate
344
+ const validation = ConfigValidator.validate(campaignConfig);
345
+ if (!validation.valid) {
346
+ throw new Error('Invalid config');
347
+ }
348
+
349
+ // Save to DB
350
+ await saveCampaign(campaignConfig);
351
+
352
+ // Create Shopify discount with metafield
353
+ await createShopifyDiscount(campaignConfig);
354
+ ```
355
+
356
+ ### 2. Shopify calls your function at checkout
357
+
358
+ ```typescript
359
+ // Shopify Function runs
360
+ export function run(input: RunInput): FunctionRunResult {
361
+ const config = JSON.parse(input.discountNode.metafield.value);
362
+ const campaign = CampaignFactory.createFromConfig(config);
363
+ const cart = new DiscountCart(input.cart);
364
+
365
+ campaign.runWithHooks(cart);
366
+
367
+ return { discounts: cart.discounts };
368
+ }
369
+ ```
370
+
371
+ ### 3. Discount applied to customer's cart
372
+
373
+ Shopify receives the discount and applies it to the cart automatically.
374
+
375
+ ---
376
+
377
+ ## Best Practices
378
+
379
+ ### 1. **Version Your Configurations**
380
+
381
+ Store a version field with your configs for future migrations:
382
+
383
+ ```typescript
384
+ {
385
+ version: '1.0',
386
+ config: { __type: 'ConditionalDiscount', ... }
387
+ }
388
+ ```
389
+
390
+ ### 2. **Test Before Activating**
391
+
392
+ Always preview campaigns with sample data before making them active:
393
+
394
+ ```typescript
395
+ const testCarts = [
396
+ { /* VIP customer cart */ },
397
+ { /* Regular customer cart */ },
398
+ { /* Large cart */ },
399
+ ];
400
+
401
+ testCarts.forEach(cart => {
402
+ const result = previewCampaign(config, cart);
403
+ console.log('Preview:', result);
404
+ });
405
+ ```
406
+
407
+ ### 3. **Error Handling**
408
+
409
+ Wrap campaign execution in try-catch to prevent crashes:
410
+
411
+ ```typescript
412
+ export function run(input: RunInput): FunctionRunResult {
413
+ try {
414
+ const config = JSON.parse(input.discountNode.metafield?.value || '{}');
415
+ const campaign = CampaignFactory.createFromConfig(config);
416
+ const cart = new DiscountCart(input.cart);
417
+
418
+ campaign.runWithHooks(cart);
419
+
420
+ return { discounts: cart.discounts };
421
+ } catch (error) {
422
+ console.error('Campaign error:', error);
423
+ // Return empty discounts on error to avoid breaking checkout
424
+ return { discounts: [] };
425
+ }
426
+ }
427
+ ```
428
+
429
+ ### 4. **Logging & Monitoring**
430
+
431
+ Add logging to track campaign performance:
432
+
433
+ ```typescript
434
+ const startTime = Date.now();
435
+ campaign.runWithHooks(cart);
436
+ const duration = Date.now() - startTime;
437
+
438
+ console.log({
439
+ campaignLabel: config.label,
440
+ duration,
441
+ discountsApplied: cart.discounts.length,
442
+ totalDiscountAmount: cart.appliedDiscountTotal,
443
+ });
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Troubleshooting
449
+
450
+ ### Configuration not working?
451
+
452
+ 1. Check validation: `ConfigValidator.validate(config)`
453
+ 2. Verify metafield is being set in Shopify
454
+ 3. Check Shopify Function logs
455
+ 4. Test with EXAMPLES.md configurations
456
+
457
+ ### Discounts not appearing?
458
+
459
+ 1. Verify the discount is active in Shopify Admin
460
+ 2. Check that qualifiers are matching (add logging)
461
+ 3. Ensure selectors are matching items (add logging)
462
+ 4. Verify discount values are valid (not negative, percentage not > 100)
463
+
464
+ ### TypeScript errors?
465
+
466
+ Make sure you're using the exported types:
467
+
468
+ ```typescript
469
+ import type { CampaignInputConfig } from '@zoxllc/shopify-checkout-extensions';
470
+ ```
471
+
472
+ ---
473
+
474
+ ## Next Steps
475
+
476
+ 1. Build your campaign builder UI
477
+ 2. Implement database storage
478
+ 3. Create Shopify discount via Admin API
479
+ 4. Deploy Shopify Function
480
+ 5. Test with real checkout flows
481
+
482
+ See EXAMPLES.md for more configuration examples!
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
1
  # ZOX Checkout Extensions lib
2
2
 
3
- This package aims to provide a common library for dealing with checkout extensions that deal specifically with product discounts or cart validation checks
3
+ An in-house common library that can be used with Shopify's checkout extensions.
4
+
5
+ ## Purpose
6
+
7
+ The intention of this library is to be imported within a Shopify app that deals with checkout extensions. This helps us keep commonly used utility functionality so that it can be shared across various extensions. For example, the same library may be imported into a `Product Discount Function` and a `Cart Validation Function` to share common logic.
@@ -0,0 +1,11 @@
1
+ import type { DiscountCart } from './DiscountCart';
2
+ import type { Qualifier } from './Qualifier';
3
+ import type { Selector } from './Selector';
4
+ import type { CartLine } from '../lineItem/api';
5
+ export declare class AndSelector {
6
+ is_a: 'AndSelector';
7
+ conditions: (Qualifier | Selector)[];
8
+ constructor(conditions: (Qualifier | Selector)[]);
9
+ match(subject: CartLine | DiscountCart, selector?: Selector): boolean;
10
+ }
11
+ //# sourceMappingURL=AndSelector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AndSelector.d.ts","sourceRoot":"","sources":["../../src/common/AndSelector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD,qBAAa,WAAW;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,CAAC,SAAS,GAAG,QAAQ,CAAC,EAAE,CAAC;gBAEzB,UAAU,EAAE,CAAC,SAAS,GAAG,QAAQ,CAAC,EAAE;IAKhD,KAAK,CACH,OAAO,EAAE,QAAQ,GAAG,YAAY,EAChC,QAAQ,CAAC,EAAE,QAAQ;CAyBtB"}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AndSelector = void 0;
4
+ class AndSelector {
5
+ is_a;
6
+ conditions;
7
+ constructor(conditions) {
8
+ this.is_a = 'AndSelector';
9
+ this.conditions = conditions;
10
+ }
11
+ match(subject, selector) {
12
+ try {
13
+ const conditionsResult = this.conditions
14
+ .map((condition) => {
15
+ if (selector) {
16
+ return condition.match(subject, selector);
17
+ }
18
+ else {
19
+ return condition.match(subject);
20
+ }
21
+ })
22
+ .filter((result) => result === true);
23
+ return (conditionsResult.length == this.conditions.length);
24
+ }
25
+ catch (e) {
26
+ return false;
27
+ }
28
+ }
29
+ }
30
+ exports.AndSelector = AndSelector;
@@ -0,0 +1,24 @@
1
+ import type { AndSelector } from './AndSelector';
2
+ import type { DiscountCart } from './DiscountCart';
3
+ import type { OrSelector } from './OrSelector';
4
+ import type { Selector } from './Selector';
5
+ import { type Qualifier, QualifierBehavior } from './Qualifier';
6
+ interface CampaignInterface {
7
+ runWithHooks(cart: DiscountCart): DiscountCart;
8
+ beforeRun?(cart: DiscountCart): DiscountCart;
9
+ run?(cart: DiscountCart): DiscountCart;
10
+ afterRun(cart: DiscountCart): DiscountCart;
11
+ }
12
+ export declare class Campaign implements CampaignInterface {
13
+ behavior: QualifierBehavior;
14
+ qualifiers?: (AndSelector | OrSelector | Qualifier)[] | null;
15
+ lineItemSelector?: Selector;
16
+ message?: string;
17
+ constructor(behavior: QualifierBehavior, qualifiers?: (AndSelector | OrSelector | Qualifier)[] | null);
18
+ qualifies(discountCart: DiscountCart): boolean;
19
+ runWithHooks(cart: DiscountCart): DiscountCart;
20
+ run(cart: DiscountCart): DiscountCart;
21
+ afterRun(cart: DiscountCart): DiscountCart;
22
+ }
23
+ export {};
24
+ //# sourceMappingURL=Campaign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Campaign.d.ts","sourceRoot":"","sources":["../../src/common/Campaign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EACL,KAAK,SAAS,EACd,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAErB,UAAU,iBAAiB;IACzB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAAC;IAC/C,SAAS,CAAC,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAAC;IAC7C,GAAG,CAAC,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAAC;CAC5C;AAED,qBAAa,QAAS,YAAW,iBAAiB;IAChD,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,UAAU,CAAC,EACP,CAAC,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC,EAAE,GACxC,IAAI,CAAC;IACT,gBAAgB,CAAC,EAAE,QAAQ,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;gBAGf,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,CAAC,EACP,CAAC,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC,EAAE,GACxC,IAAI;IAMV,SAAS,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO;IAuB9C,YAAY,CAAC,IAAI,EAAE,YAAY;IAK/B,GAAG,CAAC,IAAI,EAAE,YAAY;IAItB,QAAQ,CAAC,IAAI,EAAE,YAAY;CAG5B"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Campaign = void 0;
4
+ const Qualifier_1 = require("./Qualifier");
5
+ class Campaign {
6
+ behavior;
7
+ qualifiers;
8
+ lineItemSelector;
9
+ message;
10
+ constructor(behavior, qualifiers) {
11
+ this.behavior = behavior;
12
+ this.qualifiers = qualifiers;
13
+ }
14
+ qualifies(discountCart) {
15
+ if (this.qualifiers == undefined)
16
+ return true;
17
+ const qualifierResults = this.qualifiers.map((qualifier) => qualifier.match(discountCart, this.lineItemSelector));
18
+ if (this.behavior == Qualifier_1.QualifierBehavior.ALL) {
19
+ return (qualifierResults.filter((i) => i === false)
20
+ .length == 0);
21
+ }
22
+ else if (this.behavior == Qualifier_1.QualifierBehavior.ANY) {
23
+ return (qualifierResults.filter((i) => i === true).length >
24
+ 0);
25
+ }
26
+ else {
27
+ return true;
28
+ }
29
+ }
30
+ runWithHooks(cart) {
31
+ this.run(cart);
32
+ return this.afterRun(cart);
33
+ }
34
+ run(cart) {
35
+ return cart;
36
+ }
37
+ afterRun(cart) {
38
+ return cart;
39
+ }
40
+ }
41
+ exports.Campaign = Campaign;
@@ -0,0 +1,10 @@
1
+ export declare enum CampaignType {
2
+ ConditionalDiscount = "ConditionalDiscount"
3
+ }
4
+ export type CampaignConfiguration = {
5
+ priority: number;
6
+ campaignType: CampaignType;
7
+ name: string;
8
+ arguments: string;
9
+ };
10
+ //# sourceMappingURL=CampaignConfiguration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CampaignConfiguration.d.ts","sourceRoot":"","sources":["../../src/common/CampaignConfiguration.ts"],"names":[],"mappings":"AAAA,oBAAY,YAAY;IACtB,mBAAmB,wBAAwB;CAC5C;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CampaignType = void 0;
4
+ var CampaignType;
5
+ (function (CampaignType) {
6
+ CampaignType["ConditionalDiscount"] = "ConditionalDiscount";
7
+ })(CampaignType || (exports.CampaignType = CampaignType = {}));
@@ -0,0 +1,22 @@
1
+ import { OrSelector } from './OrSelector';
2
+ import type { Campaign } from './Campaign';
3
+ import type { DiscountInterface } from './DiscountInterface';
4
+ import type { Qualifier } from './Qualifier';
5
+ import type { Selector } from './Selector';
6
+ import { AndSelector } from './AndSelector';
7
+ export type InputConfig = {
8
+ __type: string;
9
+ inputs: (boolean | number | string | InputConfig | boolean[] | number[] | string[] | InputConfig[])[];
10
+ };
11
+ export type CampaignInputConfig = InputConfig & {
12
+ __type: 'BuyXGetY' | 'ConditionalDiscount' | 'ShippingDiscount' | 'MultiTierDiscount';
13
+ label: string;
14
+ active: boolean;
15
+ description?: string;
16
+ };
17
+ export declare class CampaignFactory {
18
+ static createFromConfig(config: CampaignInputConfig): Campaign;
19
+ private static parseInputConfig;
20
+ static createInputsObject(config: InputConfig): Campaign | Qualifier | AndSelector | OrSelector | Selector | DiscountInterface | undefined;
21
+ }
22
+ //# sourceMappingURL=CampaignFactory.d.ts.map
@@ -0,0 +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"}