@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.
- package/CLAUDE.md +136 -0
- package/EXAMPLES.md +463 -0
- package/INTEGRATION.md +482 -0
- package/README.md +5 -1
- package/dist/common/AndSelector.d.ts +11 -0
- package/dist/common/AndSelector.d.ts.map +1 -0
- package/dist/common/AndSelector.js +30 -0
- package/dist/common/Campaign.d.ts +24 -0
- package/dist/common/Campaign.d.ts.map +1 -0
- package/dist/common/Campaign.js +41 -0
- package/dist/common/CampaignConfiguration.d.ts +10 -0
- package/dist/common/CampaignConfiguration.d.ts.map +1 -0
- package/dist/common/CampaignConfiguration.js +7 -0
- package/dist/common/CampaignFactory.d.ts +22 -0
- package/dist/common/CampaignFactory.d.ts.map +1 -0
- package/dist/common/CampaignFactory.js +98 -0
- package/dist/common/CartAmountQualifier.d.ts +22 -0
- package/dist/common/CartAmountQualifier.d.ts.map +1 -0
- package/dist/common/CartAmountQualifier.js +46 -0
- package/dist/common/CartHasItemQualifier.d.ts +21 -0
- package/dist/common/CartHasItemQualifier.d.ts.map +1 -0
- package/dist/common/CartHasItemQualifier.js +54 -0
- package/dist/common/CartQuantityQualifier.d.ts +22 -0
- package/dist/common/CartQuantityQualifier.d.ts.map +1 -0
- package/dist/common/CartQuantityQualifier.js +62 -0
- package/dist/common/ConfigSchema.d.ts +71 -0
- package/dist/common/ConfigSchema.d.ts.map +1 -0
- package/dist/common/ConfigSchema.js +69 -0
- package/dist/common/ConfigValidator.d.ts +29 -0
- package/dist/common/ConfigValidator.d.ts.map +1 -0
- package/dist/common/ConfigValidator.js +84 -0
- package/dist/common/CountryCodeQualifier.d.ts +15 -0
- package/dist/common/CountryCodeQualifier.d.ts.map +1 -0
- package/dist/common/CountryCodeQualifier.js +30 -0
- package/dist/common/CustomerEmailQualifier.d.ts +16 -0
- package/dist/common/CustomerEmailQualifier.d.ts.map +1 -0
- package/dist/common/CustomerEmailQualifier.js +52 -0
- package/dist/common/CustomerSubscriberQualifier.d.ts +18 -0
- package/dist/common/CustomerSubscriberQualifier.d.ts.map +1 -0
- package/dist/common/CustomerSubscriberQualifier.js +28 -0
- package/dist/common/CustomerTagQualifier.d.ts +16 -0
- package/dist/common/CustomerTagQualifier.d.ts.map +1 -0
- package/dist/common/CustomerTagQualifier.js +45 -0
- package/dist/common/DiscountCart.d.ts +23 -0
- package/dist/common/DiscountCart.d.ts.map +1 -0
- package/dist/common/DiscountCart.js +103 -0
- package/dist/common/DiscountInterface.d.ts +17 -0
- package/dist/common/DiscountInterface.d.ts.map +1 -0
- package/dist/common/DiscountInterface.js +2 -0
- package/dist/common/OrSelector.d.ts +11 -0
- package/dist/common/OrSelector.d.ts.map +1 -0
- package/dist/common/OrSelector.js +30 -0
- package/dist/common/PostCartAmountQualifier.d.ts +9 -0
- package/dist/common/PostCartAmountQualifier.d.ts.map +1 -0
- package/dist/common/PostCartAmountQualifier.js +17 -0
- package/dist/common/ProductHandleSelector.d.ts +8 -0
- package/dist/common/ProductHandleSelector.d.ts.map +1 -0
- package/dist/common/ProductHandleSelector.js +22 -0
- package/dist/common/ProductIdSelector.d.ts +8 -0
- package/dist/common/ProductIdSelector.d.ts.map +1 -0
- package/dist/common/ProductIdSelector.js +22 -0
- package/dist/common/ProductTagSelector.d.ts +10 -0
- package/dist/common/ProductTagSelector.d.ts.map +1 -0
- package/dist/common/ProductTagSelector.js +39 -0
- package/dist/common/ProductTypeSelector.d.ts +8 -0
- package/dist/common/ProductTypeSelector.d.ts.map +1 -0
- package/dist/common/ProductTypeSelector.js +25 -0
- package/dist/common/Qualifier.d.ts +30 -0
- package/dist/common/Qualifier.d.ts.map +1 -0
- package/dist/common/Qualifier.js +61 -0
- package/dist/common/SaleItemSelector.d.ts +11 -0
- package/dist/common/SaleItemSelector.d.ts.map +1 -0
- package/dist/common/SaleItemSelector.js +30 -0
- package/dist/common/Selector.d.ts +27 -0
- package/dist/common/Selector.d.ts.map +1 -0
- package/dist/common/Selector.js +51 -0
- package/dist/common/SubscriptionItemSelector.d.ts +7 -0
- package/dist/common/SubscriptionItemSelector.d.ts.map +1 -0
- package/dist/common/SubscriptionItemSelector.js +19 -0
- package/dist/form/CampaignForm.d.ts +1 -0
- package/dist/form/CampaignForm.d.ts.map +1 -0
- package/dist/form/CampaignForm.js +1 -0
- package/{src/index.ts → dist/index.d.ts} +23 -27
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +80 -0
- package/dist/lineItem/BuyXGetY.d.ts +18 -0
- package/dist/lineItem/BuyXGetY.d.ts.map +1 -0
- package/dist/lineItem/BuyXGetY.js +85 -0
- package/dist/lineItem/ConditionalDiscount.d.ts +14 -0
- package/dist/lineItem/ConditionalDiscount.d.ts.map +1 -0
- package/dist/lineItem/ConditionalDiscount.js +81 -0
- package/dist/lineItem/FixedItemDiscount.d.ts +9 -0
- package/dist/lineItem/FixedItemDiscount.d.ts.map +1 -0
- package/dist/lineItem/FixedItemDiscount.js +35 -0
- package/dist/lineItem/MultiTierDiscount.d.ts +47 -0
- package/dist/lineItem/MultiTierDiscount.d.ts.map +1 -0
- package/dist/lineItem/MultiTierDiscount.js +150 -0
- package/dist/lineItem/PercentageDiscount.d.ts +8 -0
- package/dist/lineItem/PercentageDiscount.d.ts.map +1 -0
- package/dist/lineItem/PercentageDiscount.js +32 -0
- package/dist/lineItem/api.d.ts +2026 -0
- package/dist/lineItem/api.d.ts.map +1 -0
- package/dist/lineItem/api.js +1157 -0
- package/dist/shipping/FixedDiscount.d.ts +8 -0
- package/dist/shipping/FixedDiscount.d.ts.map +1 -0
- package/dist/shipping/FixedDiscount.js +30 -0
- package/dist/shipping/RateNameSelector.d.ts +10 -0
- package/dist/shipping/RateNameSelector.d.ts.map +1 -0
- package/dist/shipping/RateNameSelector.js +45 -0
- package/dist/shipping/ShippingDiscount.d.ts +14 -0
- package/dist/shipping/ShippingDiscount.d.ts.map +1 -0
- package/dist/shipping/ShippingDiscount.js +48 -0
- package/dist/shipping/api.d.ts +2019 -0
- package/dist/shipping/api.d.ts.map +1 -0
- package/dist/shipping/api.js +1147 -0
- package/package.json +26 -7
- package/migrate.ts +0 -64
- package/src/common/AndSelector.ts +0 -42
- package/src/common/Campaign.ts +0 -70
- package/src/common/CampaignConfiguration.ts +0 -10
- package/src/common/CampaignFactory.ts +0 -233
- package/src/common/CartAmountQualifier.ts +0 -64
- package/src/common/CartHasItemQualifier.ts +0 -80
- package/src/common/CartQuantityQualifier.ts +0 -94
- package/src/common/CountryCodeQualifier.ts +0 -49
- package/src/common/CustomerEmailQualifier.ts +0 -67
- package/src/common/CustomerSubscriberQualifier.ts +0 -32
- package/src/common/CustomerTagQualifier.ts +0 -69
- package/src/common/DiscountCart.ts +0 -161
- package/src/common/DiscountInterface.ts +0 -26
- package/src/common/OrSelector.ts +0 -40
- package/src/common/PostCartAmountQualifier.ts +0 -21
- package/src/common/ProductHandleSelector.ts +0 -32
- package/src/common/ProductIdSelector.ts +0 -34
- package/src/common/ProductTagSelector.ts +0 -63
- package/src/common/ProductTypeSelector.ts +0 -40
- package/src/common/Qualifier.ts +0 -67
- package/src/common/SaleItemSelector.ts +0 -36
- package/src/common/Selector.ts +0 -66
- package/src/common/SubscriptionItemSelector.ts +0 -23
- package/src/lineItem/BuyXGetY.ts +0 -131
- package/src/lineItem/ConditionalDiscount.ts +0 -102
- package/src/lineItem/FixedItemDiscount.ts +0 -51
- package/src/lineItem/PercentageDiscount.ts +0 -44
- package/src/lineItem/api.ts +0 -2103
- package/src/shipping/FixedDiscount.ts +0 -37
- package/src/shipping/RateNameSelector.ts +0 -54
- package/src/shipping/ShippingDiscount.ts +0 -75
- package/src/shipping/api.ts +0 -2014
- package/tests/AndSelector.test.ts +0 -27
- package/tests/CartQuantityQualifier.test.ts +0 -381
- package/tests/CountryCodeQualifier.test.ts +0 -55
- package/tests/CustomerSubscriberQualifier.test.ts +0 -101
- package/tests/CustomerTagQualifier.test.ts +0 -71
- package/tests/DiscountCart.test.ts +0 -115
- package/tests/OrSelector.test.ts +0 -27
- package/tests/ProductIdSelector.test.ts +0 -83
- package/tests/ProductTagSelector.test.ts +0 -75
- package/tests/Qualifier.test.ts +0 -193
- package/tests/RateNameSelector.test.ts +0 -107
- package/tests/SaleItemSelector.test.ts +0 -113
- package/tests/Selector.test.ts +0 -83
- package/tests/ShippingDiscount.test.ts +0 -147
- 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
|
-
|
|
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"}
|