brainerce 1.20.2 → 1.23.11
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/README.md +4662 -4506
- package/dist/index.d.mts +1134 -57
- package/dist/index.d.ts +1134 -57
- package/dist/index.js +626 -32
- package/dist/index.mjs +624 -32
- package/package.json +76 -76
package/dist/index.mjs
CHANGED
|
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// src/version.ts
|
|
118
|
-
var SDK_VERSION = "1.
|
|
118
|
+
var SDK_VERSION = "1.21.0";
|
|
119
119
|
|
|
120
120
|
// src/client.ts
|
|
121
121
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -141,27 +141,87 @@ var BrainerceClient = class {
|
|
|
141
141
|
* This is needed because Stripe redirects lose in-memory state.
|
|
142
142
|
*/
|
|
143
143
|
this.ACTIVE_CHECKOUT_KEY = "brainerce_active_checkout";
|
|
144
|
+
// -------------------- Contact Forms (schema) --------------------
|
|
145
|
+
/**
|
|
146
|
+
* List active contact forms configured for the store.
|
|
147
|
+
*
|
|
148
|
+
* Storefront (public) mode only. Useful when your site has multiple
|
|
149
|
+
* forms (e.g. "main", "newsletter", "whatsapp_prechat") and you need
|
|
150
|
+
* to pick the right one at render time.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const forms = await brainerce.contactForms.list();
|
|
155
|
+
* // → [{ key: 'main', name: 'Main Contact', isDefault: true }, ...]
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
this.contactForms = {
|
|
159
|
+
list: async () => {
|
|
160
|
+
if (this.isVibeCodedMode()) {
|
|
161
|
+
return this.vibeCodedRequest("GET", "/contact-forms");
|
|
162
|
+
}
|
|
163
|
+
return this.storefrontRequest("GET", "/contact-forms");
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Fetch the full schema for a single contact form, including all
|
|
167
|
+
* visible fields + their localized labels/placeholders/help text.
|
|
168
|
+
*
|
|
169
|
+
* `locale` is the storefront locale at render time (e.g. `"he"`).
|
|
170
|
+
* Falls back to the store's default language when omitted.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* const form = await brainerce.contactForms.get('main', 'he');
|
|
175
|
+
* // Render form.fields; submit via brainerce.createInquiry({ formKey: 'main', fields })
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
get: async (formKey = "main", locale) => {
|
|
179
|
+
const query = locale ? { locale } : void 0;
|
|
180
|
+
if (this.isVibeCodedMode()) {
|
|
181
|
+
return this.vibeCodedRequest(
|
|
182
|
+
"GET",
|
|
183
|
+
`/contact-forms/${encodeURIComponent(formKey)}`,
|
|
184
|
+
void 0,
|
|
185
|
+
query
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return this.storefrontRequest(
|
|
189
|
+
"GET",
|
|
190
|
+
`/contact-forms/${encodeURIComponent(formKey)}`,
|
|
191
|
+
void 0,
|
|
192
|
+
query
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
144
196
|
// -------------------- Local Cart (Client-Side for Guests) --------------------
|
|
145
197
|
// These methods store cart data in localStorage - NO API calls!
|
|
146
198
|
// Use for guest users in vibe-coded sites
|
|
147
199
|
this.LOCAL_CART_KEY = "brainerce_cart";
|
|
148
|
-
|
|
149
|
-
|
|
200
|
+
const resolvedSalesChannelId = options.salesChannelId ?? options.connectionId;
|
|
201
|
+
if (!options.apiKey && !options.storeId && !resolvedSalesChannelId) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"BrainerceClient: either salesChannelId, apiKey, or storeId is required"
|
|
204
|
+
);
|
|
150
205
|
}
|
|
151
206
|
if (options.apiKey && !options.apiKey.startsWith("brainerce_")) {
|
|
152
207
|
console.warn('BrainerceClient: apiKey should start with "brainerce_"');
|
|
153
208
|
}
|
|
154
|
-
if (
|
|
155
|
-
console.warn('BrainerceClient:
|
|
209
|
+
if (resolvedSalesChannelId && !resolvedSalesChannelId.startsWith("vc_")) {
|
|
210
|
+
console.warn('BrainerceClient: salesChannelId should start with "vc_"');
|
|
211
|
+
}
|
|
212
|
+
if (!options.salesChannelId && options.connectionId) {
|
|
213
|
+
console.warn(
|
|
214
|
+
"BrainerceClient: `connectionId` is deprecated \u2014 use `salesChannelId` instead. `connectionId` will be removed in SDK 2.0."
|
|
215
|
+
);
|
|
156
216
|
}
|
|
157
217
|
if (options.apiKey && typeof window !== "undefined") {
|
|
158
218
|
console.warn(
|
|
159
|
-
"BrainerceClient: WARNING - API key detected in browser environment. This is a security risk! Use
|
|
219
|
+
"BrainerceClient: WARNING - API key detected in browser environment. This is a security risk! Use salesChannelId or storeId for frontend applications."
|
|
160
220
|
);
|
|
161
221
|
}
|
|
162
222
|
this.apiKey = options.apiKey;
|
|
163
223
|
this.storeId = options.storeId;
|
|
164
|
-
this.connectionId =
|
|
224
|
+
this.connectionId = resolvedSalesChannelId;
|
|
165
225
|
let resolvedBase = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
166
226
|
if (resolvedBase.startsWith("/") && typeof window !== "undefined" && window.location?.origin) {
|
|
167
227
|
resolvedBase = window.location.origin + resolvedBase;
|
|
@@ -248,11 +308,17 @@ var BrainerceClient = class {
|
|
|
248
308
|
}
|
|
249
309
|
// -------------------- Mode Detection --------------------
|
|
250
310
|
/**
|
|
251
|
-
* Check if client is in
|
|
311
|
+
* Check if client is in sales-channel mode (using salesChannelId / legacy connectionId).
|
|
252
312
|
*/
|
|
253
|
-
|
|
313
|
+
isSalesChannelMode() {
|
|
254
314
|
return !!this.connectionId && !this.apiKey;
|
|
255
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* @deprecated Use `isSalesChannelMode()` instead. Kept as a backwards-compatible alias.
|
|
318
|
+
*/
|
|
319
|
+
isVibeCodedMode() {
|
|
320
|
+
return this.isSalesChannelMode();
|
|
321
|
+
}
|
|
256
322
|
/**
|
|
257
323
|
* Check if client is in storefront mode (using storeId)
|
|
258
324
|
*/
|
|
@@ -564,6 +630,18 @@ var BrainerceClient = class {
|
|
|
564
630
|
const categories = Array.isArray(params?.categories) ? params.categories.join(",") : params?.categories;
|
|
565
631
|
const brands = Array.isArray(params?.brands) ? params.brands.join(",") : params?.brands;
|
|
566
632
|
const tags = Array.isArray(params?.tags) ? params.tags.join(",") : params?.tags;
|
|
633
|
+
let metafields;
|
|
634
|
+
if (params?.metafields && Object.keys(params.metafields).length > 0) {
|
|
635
|
+
const normalized = {};
|
|
636
|
+
for (const [key, raw] of Object.entries(params.metafields)) {
|
|
637
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
638
|
+
const strings = arr.map((v) => String(v)).filter(Boolean);
|
|
639
|
+
if (strings.length > 0) normalized[key] = strings;
|
|
640
|
+
}
|
|
641
|
+
if (Object.keys(normalized).length > 0) {
|
|
642
|
+
metafields = JSON.stringify(normalized);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
567
645
|
const queryParams = {
|
|
568
646
|
page: params?.page,
|
|
569
647
|
limit: params?.limit,
|
|
@@ -574,6 +652,7 @@ var BrainerceClient = class {
|
|
|
574
652
|
tags,
|
|
575
653
|
minPrice: params?.minPrice,
|
|
576
654
|
maxPrice: params?.maxPrice,
|
|
655
|
+
metafields,
|
|
577
656
|
sortBy: params?.sortBy,
|
|
578
657
|
sortOrder: params?.sortOrder,
|
|
579
658
|
// Admin-only params
|
|
@@ -1503,6 +1582,58 @@ var BrainerceClient = class {
|
|
|
1503
1582
|
async getCustomerByEmail(email) {
|
|
1504
1583
|
return this.request("GET", "/api/v1/customers/by-email", void 0, { email });
|
|
1505
1584
|
}
|
|
1585
|
+
/**
|
|
1586
|
+
* List a customer's saved payment methods (vaulted cards).
|
|
1587
|
+
*
|
|
1588
|
+
* Returns display-only metadata — last4, brand, expiry, default flag,
|
|
1589
|
+
* status. The underlying provider token is encrypted at rest and
|
|
1590
|
+
* NEVER returned through this API.
|
|
1591
|
+
*
|
|
1592
|
+
* Apps mode requires `customers:read` and `payments:read` scopes.
|
|
1593
|
+
*
|
|
1594
|
+
* @param storeId - The store this customer belongs to
|
|
1595
|
+
* @param customerId - The customer's ID
|
|
1596
|
+
* @returns Array of saved payment methods
|
|
1597
|
+
*
|
|
1598
|
+
* @example
|
|
1599
|
+
* ```typescript
|
|
1600
|
+
* const methods = await client.listSavedPaymentMethods(storeId, customerId);
|
|
1601
|
+
* methods.forEach((m) => {
|
|
1602
|
+
* console.log(`${m.brand} ending in ${m.last4} (expires ${m.expMonth}/${m.expYear})`);
|
|
1603
|
+
* });
|
|
1604
|
+
* ```
|
|
1605
|
+
*/
|
|
1606
|
+
async listSavedPaymentMethods(storeId, customerId) {
|
|
1607
|
+
return this.request(
|
|
1608
|
+
"GET",
|
|
1609
|
+
`/api/stores/${storeId}/customers/${customerId}/payment-methods`
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Remove a customer's saved payment method.
|
|
1614
|
+
*
|
|
1615
|
+
* Hard-deletes the row. The provider may still hold the underlying
|
|
1616
|
+
* token internally — we don't issue a delete-at-provider call because
|
|
1617
|
+
* not every provider supports it. From the platform's perspective the
|
|
1618
|
+
* token is gone; subsequent charges will fail.
|
|
1619
|
+
*
|
|
1620
|
+
* Apps mode requires `customers:write` scope.
|
|
1621
|
+
*
|
|
1622
|
+
* @param storeId - The store this customer belongs to
|
|
1623
|
+
* @param customerId - The customer's ID
|
|
1624
|
+
* @param paymentMethodId - The saved payment method ID to remove
|
|
1625
|
+
*
|
|
1626
|
+
* @example
|
|
1627
|
+
* ```typescript
|
|
1628
|
+
* await client.removeSavedPaymentMethod(storeId, customerId, methodId);
|
|
1629
|
+
* ```
|
|
1630
|
+
*/
|
|
1631
|
+
async removeSavedPaymentMethod(storeId, customerId, paymentMethodId) {
|
|
1632
|
+
return this.request(
|
|
1633
|
+
"DELETE",
|
|
1634
|
+
`/api/stores/${storeId}/customers/${customerId}/payment-methods/${paymentMethodId}`
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1506
1637
|
/**
|
|
1507
1638
|
* Login an existing customer (returns JWT token)
|
|
1508
1639
|
* Works in vibe-coded, storefront, and admin mode
|
|
@@ -2009,7 +2140,7 @@ var BrainerceClient = class {
|
|
|
2009
2140
|
* per IP on the server. Include an empty `honeypot` field on your form
|
|
2010
2141
|
* and do NOT send it — bots that auto-fill every input will be rejected.
|
|
2011
2142
|
*
|
|
2012
|
-
*
|
|
2143
|
+
* **Legacy shape (kept working forever):**
|
|
2013
2144
|
* ```typescript
|
|
2014
2145
|
* await brainerce.createInquiry({
|
|
2015
2146
|
* name: 'Jane Doe',
|
|
@@ -2019,6 +2150,16 @@ var BrainerceClient = class {
|
|
|
2019
2150
|
* phone: '+1-555-0100',
|
|
2020
2151
|
* });
|
|
2021
2152
|
* ```
|
|
2153
|
+
*
|
|
2154
|
+
* **Phase 2 (multi-form / custom fields):**
|
|
2155
|
+
* ```typescript
|
|
2156
|
+
* const form = await brainerce.contactForms.get('newsletter');
|
|
2157
|
+
* await brainerce.createInquiry({
|
|
2158
|
+
* formKey: 'newsletter',
|
|
2159
|
+
* fields: { email: 'jane@example.com', source: 'homepage' },
|
|
2160
|
+
* locale: 'he',
|
|
2161
|
+
* });
|
|
2162
|
+
* ```
|
|
2022
2163
|
*/
|
|
2023
2164
|
async createInquiry(input) {
|
|
2024
2165
|
if (this.isVibeCodedMode()) {
|
|
@@ -2345,6 +2486,82 @@ var BrainerceClient = class {
|
|
|
2345
2486
|
"cart"
|
|
2346
2487
|
);
|
|
2347
2488
|
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Recalculate cart totals against current product/variant prices and
|
|
2491
|
+
* discount rules. Idempotent — does NOT mutate the per-item snapshot
|
|
2492
|
+
* `unitPrice`. The returned cart's `hasPriceChanges` / `hasUnavailableItems`
|
|
2493
|
+
* flags reflect the live state. Call this on cart load if you want to
|
|
2494
|
+
* surface drift to the customer before they reach checkout.
|
|
2495
|
+
*
|
|
2496
|
+
* @example
|
|
2497
|
+
* ```typescript
|
|
2498
|
+
* const cart = await client.recalculateCart('cart_123');
|
|
2499
|
+
* if (cart.hasPriceChanges) {
|
|
2500
|
+
* // show "prices have changed" banner
|
|
2501
|
+
* }
|
|
2502
|
+
* ```
|
|
2503
|
+
*/
|
|
2504
|
+
async recalculateCart(cartId) {
|
|
2505
|
+
if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
|
|
2506
|
+
return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
|
|
2507
|
+
}
|
|
2508
|
+
if (this.isVibeCodedMode()) {
|
|
2509
|
+
return this.withGuards(
|
|
2510
|
+
this.vibeCodedRequest("POST", `/cart/${cartId}/recalculate`),
|
|
2511
|
+
"cart"
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
if (this.storeId && !this.apiKey) {
|
|
2515
|
+
return this.withGuards(
|
|
2516
|
+
this.storefrontRequest("POST", `/cart/${cartId}/recalculate`),
|
|
2517
|
+
"cart"
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
return this.withGuards(
|
|
2521
|
+
this.adminRequest("POST", `/api/v1/cart/${cartId}/recalculate`),
|
|
2522
|
+
"cart"
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Refresh per-item `unitPrice` snapshots to current live prices. Use this
|
|
2527
|
+
* after the customer accepts new prices in the drift-reconfirm flow. The
|
|
2528
|
+
* subsequent `createCheckout` call will then succeed (it would otherwise
|
|
2529
|
+
* throw `PRICE_DRIFT`).
|
|
2530
|
+
*
|
|
2531
|
+
* @example
|
|
2532
|
+
* ```typescript
|
|
2533
|
+
* try {
|
|
2534
|
+
* await client.createCheckout(cartId);
|
|
2535
|
+
* } catch (err) {
|
|
2536
|
+
* if (err.code === 'PRICE_DRIFT') {
|
|
2537
|
+
* // ask user to confirm new prices, then:
|
|
2538
|
+
* await client.refreshCartSnapshots(cartId);
|
|
2539
|
+
* await client.createCheckout(cartId);
|
|
2540
|
+
* }
|
|
2541
|
+
* }
|
|
2542
|
+
* ```
|
|
2543
|
+
*/
|
|
2544
|
+
async refreshCartSnapshots(cartId) {
|
|
2545
|
+
if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
|
|
2546
|
+
return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
|
|
2547
|
+
}
|
|
2548
|
+
if (this.isVibeCodedMode()) {
|
|
2549
|
+
return this.withGuards(
|
|
2550
|
+
this.vibeCodedRequest("POST", `/cart/${cartId}/refresh-snapshots`),
|
|
2551
|
+
"cart"
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
if (this.storeId && !this.apiKey) {
|
|
2555
|
+
return this.withGuards(
|
|
2556
|
+
this.storefrontRequest("POST", `/cart/${cartId}/refresh-snapshots`),
|
|
2557
|
+
"cart"
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
return this.withGuards(
|
|
2561
|
+
this.adminRequest("POST", `/api/v1/cart/${cartId}/refresh-snapshots`),
|
|
2562
|
+
"cart"
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2348
2565
|
/**
|
|
2349
2566
|
* Link a cart to the currently logged-in customer.
|
|
2350
2567
|
* Use this after customer logs in to associate their guest cart with their account.
|
|
@@ -2585,7 +2802,8 @@ var BrainerceClient = class {
|
|
|
2585
2802
|
* ```typescript
|
|
2586
2803
|
* const { bundles } = await client.getCartBundles('cart_123');
|
|
2587
2804
|
* bundles.forEach(b => {
|
|
2588
|
-
*
|
|
2805
|
+
* const names = b.offeredProducts.map(p => p.name).join(', ');
|
|
2806
|
+
* console.log(`Add ${names} and save! Was ${b.totalOriginalPrice}, now ${b.totalDiscountedPrice}`);
|
|
2589
2807
|
* });
|
|
2590
2808
|
* ```
|
|
2591
2809
|
*/
|
|
@@ -2648,24 +2866,31 @@ var BrainerceClient = class {
|
|
|
2648
2866
|
throw new BrainerceError("removeOrderBump() requires vibe-coded or storefront mode", 400);
|
|
2649
2867
|
}
|
|
2650
2868
|
/**
|
|
2651
|
-
*
|
|
2869
|
+
* Accept an N-product bundle offer: every offered product not yet in cart
|
|
2870
|
+
* is added with the bundle discount applied.
|
|
2652
2871
|
*
|
|
2653
2872
|
* @param cartId - Cart ID
|
|
2654
2873
|
* @param bundleOfferId - Bundle offer ID
|
|
2655
|
-
* @param
|
|
2874
|
+
* @param variantSelections - Optional map of `productId → variantId` for offered products with variants
|
|
2656
2875
|
* @returns Updated cart
|
|
2657
2876
|
*
|
|
2658
2877
|
* @example
|
|
2659
2878
|
* ```typescript
|
|
2660
2879
|
* const { bundles } = await client.getCartBundles('cart_123');
|
|
2661
|
-
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2880
|
+
* const bundle = bundles[0];
|
|
2881
|
+
* // Simple products only:
|
|
2882
|
+
* await client.addBundleToCart('cart_123', bundle.id);
|
|
2883
|
+
* // Some offered products have variants:
|
|
2884
|
+
* await client.addBundleToCart('cart_123', bundle.id, {
|
|
2885
|
+
* [variantProductId]: selectedVariantId,
|
|
2886
|
+
* });
|
|
2665
2887
|
* ```
|
|
2666
2888
|
*/
|
|
2667
|
-
async addBundleToCart(cartId, bundleOfferId,
|
|
2668
|
-
const body = {
|
|
2889
|
+
async addBundleToCart(cartId, bundleOfferId, variantSelections) {
|
|
2890
|
+
const body = {
|
|
2891
|
+
bundleOfferId,
|
|
2892
|
+
...variantSelections && Object.keys(variantSelections).length > 0 ? { variantSelections } : {}
|
|
2893
|
+
};
|
|
2669
2894
|
if (this.isVibeCodedMode()) {
|
|
2670
2895
|
return this.vibeCodedRequest("POST", `/cart/${cartId}/bundle`, body);
|
|
2671
2896
|
}
|
|
@@ -2781,6 +3006,9 @@ var BrainerceClient = class {
|
|
|
2781
3006
|
couponCode: null,
|
|
2782
3007
|
items: [],
|
|
2783
3008
|
itemCount: 0,
|
|
3009
|
+
hasPriceChanges: false,
|
|
3010
|
+
hasUnavailableItems: false,
|
|
3011
|
+
unavailableItemIds: [],
|
|
2784
3012
|
expiresAt: null,
|
|
2785
3013
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2786
3014
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -3017,23 +3245,21 @@ var BrainerceClient = class {
|
|
|
3017
3245
|
* ```
|
|
3018
3246
|
*/
|
|
3019
3247
|
async smartAddToCart(item) {
|
|
3248
|
+
const payload = {
|
|
3249
|
+
productId: item.productId,
|
|
3250
|
+
variantId: item.variantId,
|
|
3251
|
+
quantity: item.quantity,
|
|
3252
|
+
metadata: item.metadata,
|
|
3253
|
+
...item.selections && item.selections.length > 0 ? { selections: item.selections } : {},
|
|
3254
|
+
...item.nestedByModifierId ? { nestedByModifierId: item.nestedByModifierId } : {}
|
|
3255
|
+
};
|
|
3020
3256
|
if (this.isCustomerLoggedIn()) {
|
|
3021
3257
|
const cart = await this.getOrCreateCustomerCart();
|
|
3022
|
-
const updated = await this.addToCart(cart.id,
|
|
3023
|
-
productId: item.productId,
|
|
3024
|
-
variantId: item.variantId,
|
|
3025
|
-
quantity: item.quantity,
|
|
3026
|
-
metadata: item.metadata
|
|
3027
|
-
});
|
|
3258
|
+
const updated = await this.addToCart(cart.id, payload);
|
|
3028
3259
|
return updated;
|
|
3029
3260
|
} else {
|
|
3030
3261
|
const cart = await this.getOrCreateSessionCart();
|
|
3031
|
-
const updated = await this.addToCart(cart.id,
|
|
3032
|
-
productId: item.productId,
|
|
3033
|
-
variantId: item.variantId,
|
|
3034
|
-
quantity: item.quantity,
|
|
3035
|
-
metadata: item.metadata
|
|
3036
|
-
});
|
|
3262
|
+
const updated = await this.addToCart(cart.id, payload);
|
|
3037
3263
|
this.updateSessionCartItemCount(updated.items?.length ?? 0);
|
|
3038
3264
|
return updated;
|
|
3039
3265
|
}
|
|
@@ -4076,6 +4302,16 @@ var BrainerceClient = class {
|
|
|
4076
4302
|
variantId: item.variantId || null,
|
|
4077
4303
|
quantity: item.quantity,
|
|
4078
4304
|
unitPrice: item.price || "0",
|
|
4305
|
+
// Local carts never reach the server, so they have no live price to
|
|
4306
|
+
// compare against. Surface the snapshot as both values and flag
|
|
4307
|
+
// everything as "unchanged + available" — the migration to a server
|
|
4308
|
+
// cart on next load will run the real recalc.
|
|
4309
|
+
currentUnitPrice: item.price || "0",
|
|
4310
|
+
priceChanged: false,
|
|
4311
|
+
priceDelta: "0",
|
|
4312
|
+
priceDirection: "unchanged",
|
|
4313
|
+
isAvailable: true,
|
|
4314
|
+
unavailableReason: null,
|
|
4079
4315
|
discountAmount: "0",
|
|
4080
4316
|
promoDiscountAmount: "0",
|
|
4081
4317
|
promoSource: null,
|
|
@@ -4098,6 +4334,9 @@ var BrainerceClient = class {
|
|
|
4098
4334
|
updatedAt: item.addedAt
|
|
4099
4335
|
})),
|
|
4100
4336
|
itemCount: localCart.items.reduce((sum, i) => sum + i.quantity, 0),
|
|
4337
|
+
hasPriceChanges: false,
|
|
4338
|
+
hasUnavailableItems: false,
|
|
4339
|
+
unavailableItemIds: [],
|
|
4101
4340
|
expiresAt: null,
|
|
4102
4341
|
createdAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
4103
4342
|
updatedAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5444,6 +5683,204 @@ var BrainerceClient = class {
|
|
|
5444
5683
|
`/api/v1/attributes/${attributeId}/options/${optionId}`
|
|
5445
5684
|
);
|
|
5446
5685
|
}
|
|
5686
|
+
// -------------------- Modifier Groups (Admin) --------------------
|
|
5687
|
+
// These methods require Admin mode (apiKey).
|
|
5688
|
+
// Routes: /api/stores/:storeId/modifier-groups[/:id]
|
|
5689
|
+
// /api/stores/:storeId/modifier-groups/:groupId/modifiers[/:modifierId]
|
|
5690
|
+
// /api/stores/:storeId/products/:productId/modifier-groups[/:attachmentId]
|
|
5691
|
+
//
|
|
5692
|
+
// Server-side validation failures arrive as a structured 400 envelope:
|
|
5693
|
+
// { code: 'MODIFIER_VALIDATION_FAILED', errors: ModifierValidationError[] }
|
|
5694
|
+
// surfaced via BrainerceError.details.
|
|
5695
|
+
/**
|
|
5696
|
+
* List modifier groups in a store, paginated.
|
|
5697
|
+
* Requires Admin mode (apiKey).
|
|
5698
|
+
*
|
|
5699
|
+
* @example
|
|
5700
|
+
* ```typescript
|
|
5701
|
+
* const groups = await client.listModifierGroups('store_123', {
|
|
5702
|
+
* page: 1,
|
|
5703
|
+
* limit: 20,
|
|
5704
|
+
* search: 'pizza',
|
|
5705
|
+
* status: 'active',
|
|
5706
|
+
* });
|
|
5707
|
+
* ```
|
|
5708
|
+
*/
|
|
5709
|
+
async listModifierGroups(storeId, params) {
|
|
5710
|
+
return this.adminRequest(
|
|
5711
|
+
"GET",
|
|
5712
|
+
`/api/stores/${storeId}/modifier-groups`,
|
|
5713
|
+
void 0,
|
|
5714
|
+
params
|
|
5715
|
+
);
|
|
5716
|
+
}
|
|
5717
|
+
/**
|
|
5718
|
+
* Fetch a single modifier group (with its modifiers).
|
|
5719
|
+
* Requires Admin mode (apiKey).
|
|
5720
|
+
*
|
|
5721
|
+
* Admin fetches include `internalName`; storefront responses strip it
|
|
5722
|
+
* (PRD §5.2 invariant).
|
|
5723
|
+
*/
|
|
5724
|
+
async getModifierGroup(storeId, groupId) {
|
|
5725
|
+
return this.adminRequest(
|
|
5726
|
+
"GET",
|
|
5727
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}`
|
|
5728
|
+
);
|
|
5729
|
+
}
|
|
5730
|
+
/**
|
|
5731
|
+
* Create a modifier group. Modifiers themselves are created separately via
|
|
5732
|
+
* `createModifier(storeId, groupId, …)`.
|
|
5733
|
+
* Requires Admin mode (apiKey).
|
|
5734
|
+
*
|
|
5735
|
+
* @example
|
|
5736
|
+
* ```typescript
|
|
5737
|
+
* const group = await client.createModifierGroup('store_123', {
|
|
5738
|
+
* name: 'Toppings',
|
|
5739
|
+
* internalName: 'Pizza toppings',
|
|
5740
|
+
* selectionType: 'MULTIPLE',
|
|
5741
|
+
* minSelections: 0,
|
|
5742
|
+
* maxSelections: 8,
|
|
5743
|
+
* freeQuantity: 3,
|
|
5744
|
+
* freeAllocationPolicy: 'EXPENSIVE_FREE',
|
|
5745
|
+
* });
|
|
5746
|
+
* ```
|
|
5747
|
+
*/
|
|
5748
|
+
async createModifierGroup(storeId, data) {
|
|
5749
|
+
return this.adminRequest("POST", `/api/stores/${storeId}/modifier-groups`, data);
|
|
5750
|
+
}
|
|
5751
|
+
/**
|
|
5752
|
+
* Update a modifier group's metadata or selection rules. Pass `status: 'archived'`
|
|
5753
|
+
* to soft-delete (the group becomes hidden from product attachments).
|
|
5754
|
+
* Requires Admin mode (apiKey).
|
|
5755
|
+
*/
|
|
5756
|
+
async updateModifierGroup(storeId, groupId, data) {
|
|
5757
|
+
return this.adminRequest(
|
|
5758
|
+
"PATCH",
|
|
5759
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}`,
|
|
5760
|
+
data
|
|
5761
|
+
);
|
|
5762
|
+
}
|
|
5763
|
+
/**
|
|
5764
|
+
* Hard-delete a modifier group. Server rejects deletion when the group is
|
|
5765
|
+
* still attached to any product (detach the attachments first, or use
|
|
5766
|
+
* `updateModifierGroup(..., { status: 'archived' })` to keep the row).
|
|
5767
|
+
* Requires Admin mode (apiKey).
|
|
5768
|
+
*/
|
|
5769
|
+
async deleteModifierGroup(storeId, groupId) {
|
|
5770
|
+
await this.adminRequest("DELETE", `/api/stores/${storeId}/modifier-groups/${groupId}`);
|
|
5771
|
+
}
|
|
5772
|
+
/**
|
|
5773
|
+
* Create a modifier inside a group (e.g., "Olives" inside the "Toppings" group).
|
|
5774
|
+
* Requires Admin mode (apiKey).
|
|
5775
|
+
*
|
|
5776
|
+
* `priceDelta` is a decimal string. Negatives are accepted for downsell
|
|
5777
|
+
* modifiers (e.g., `"-2.00"` for "no bread"); the server still enforces
|
|
5778
|
+
* `unitPrice >= 0` at cart-line resolution and rejects negative deltas
|
|
5779
|
+
* combined with a `referencedProductId`.
|
|
5780
|
+
*
|
|
5781
|
+
* @example
|
|
5782
|
+
* ```typescript
|
|
5783
|
+
* const olives = await client.createModifier('store_123', 'mg_toppings', {
|
|
5784
|
+
* name: 'Olives',
|
|
5785
|
+
* priceDelta: '5.00',
|
|
5786
|
+
* isDefault: false,
|
|
5787
|
+
* available: true,
|
|
5788
|
+
* });
|
|
5789
|
+
* ```
|
|
5790
|
+
*/
|
|
5791
|
+
async createModifier(storeId, groupId, data) {
|
|
5792
|
+
return this.adminRequest(
|
|
5793
|
+
"POST",
|
|
5794
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}/modifiers`,
|
|
5795
|
+
data
|
|
5796
|
+
);
|
|
5797
|
+
}
|
|
5798
|
+
/**
|
|
5799
|
+
* Update a modifier. Pass `status: 'archived'` to soft-delete.
|
|
5800
|
+
* Requires Admin mode (apiKey).
|
|
5801
|
+
*/
|
|
5802
|
+
async updateModifier(storeId, groupId, modifierId, data) {
|
|
5803
|
+
return this.adminRequest(
|
|
5804
|
+
"PATCH",
|
|
5805
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}`,
|
|
5806
|
+
data
|
|
5807
|
+
);
|
|
5808
|
+
}
|
|
5809
|
+
/**
|
|
5810
|
+
* Hard-delete a modifier. Use `updateModifier(..., { status: 'archived' })`
|
|
5811
|
+
* if existing line items / order snapshots reference it.
|
|
5812
|
+
* Requires Admin mode (apiKey).
|
|
5813
|
+
*/
|
|
5814
|
+
async deleteModifier(storeId, groupId, modifierId) {
|
|
5815
|
+
await this.adminRequest(
|
|
5816
|
+
"DELETE",
|
|
5817
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}`
|
|
5818
|
+
);
|
|
5819
|
+
}
|
|
5820
|
+
/**
|
|
5821
|
+
* Flip the sold-out toggle on a modifier. Operational endpoint kept separate
|
|
5822
|
+
* from `updateModifier` because the daily ops bar is intentionally lower
|
|
5823
|
+
* (PRD §7.1 — STAFF role once the granular `TOGGLE_MODIFIER_AVAILABILITY`
|
|
5824
|
+
* permission ships).
|
|
5825
|
+
* Requires Admin mode (apiKey).
|
|
5826
|
+
*
|
|
5827
|
+
* @example
|
|
5828
|
+
* ```typescript
|
|
5829
|
+
* // Mark the egg modifier as out of stock during a busy lunch service
|
|
5830
|
+
* await client.toggleModifierAvailability('store_123', 'mg_toppings', 'm_egg', false);
|
|
5831
|
+
* ```
|
|
5832
|
+
*/
|
|
5833
|
+
async toggleModifierAvailability(storeId, groupId, modifierId, available) {
|
|
5834
|
+
return this.adminRequest(
|
|
5835
|
+
"POST",
|
|
5836
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}/availability-toggle`,
|
|
5837
|
+
{ available }
|
|
5838
|
+
);
|
|
5839
|
+
}
|
|
5840
|
+
/**
|
|
5841
|
+
* Attach a modifier group to a product. Three patterns (PRD §5.4):
|
|
5842
|
+
*
|
|
5843
|
+
* - `variantId` omitted → default attach (applies to all variants).
|
|
5844
|
+
* - `variantId` set + matching default attach already exists → variant override row.
|
|
5845
|
+
* - `variantId` set + no default attach → variant-only group.
|
|
5846
|
+
*
|
|
5847
|
+
* Override fields use `null` to mean inherit; any non-null value (including
|
|
5848
|
+
* `0` or `false`) wins. The disable-for-variant convention is `maxOverride: 0` —
|
|
5849
|
+
* the resolver returns the group with `effectiveMax=0` and the validator
|
|
5850
|
+
* silently skips it on cart add.
|
|
5851
|
+
*
|
|
5852
|
+
* Requires Admin mode (apiKey).
|
|
5853
|
+
*/
|
|
5854
|
+
async attachModifierGroup(storeId, productId, data) {
|
|
5855
|
+
return this.adminRequest(
|
|
5856
|
+
"POST",
|
|
5857
|
+
`/api/stores/${storeId}/products/${productId}/modifier-groups`,
|
|
5858
|
+
data
|
|
5859
|
+
);
|
|
5860
|
+
}
|
|
5861
|
+
/**
|
|
5862
|
+
* Update an existing attachment's overrides or position. The `modifierGroupId`
|
|
5863
|
+
* and `variantId` are immutable — to swap groups or move between default /
|
|
5864
|
+
* per-variant rows, detach and re-attach.
|
|
5865
|
+
* Requires Admin mode (apiKey).
|
|
5866
|
+
*/
|
|
5867
|
+
async updateAttachment(storeId, productId, attachmentId, data) {
|
|
5868
|
+
return this.adminRequest(
|
|
5869
|
+
"PATCH",
|
|
5870
|
+
`/api/stores/${storeId}/products/${productId}/modifier-groups/${attachmentId}`,
|
|
5871
|
+
data
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* Detach a modifier group from a product (or remove a per-variant override row).
|
|
5876
|
+
* Requires Admin mode (apiKey).
|
|
5877
|
+
*/
|
|
5878
|
+
async detachModifierGroup(storeId, productId, attachmentId) {
|
|
5879
|
+
await this.adminRequest(
|
|
5880
|
+
"DELETE",
|
|
5881
|
+
`/api/stores/${storeId}/products/${productId}/modifier-groups/${attachmentId}`
|
|
5882
|
+
);
|
|
5883
|
+
}
|
|
5447
5884
|
// -------------------- Shipping: Zones and Rates (Admin) --------------------
|
|
5448
5885
|
// These methods require Admin mode (apiKey)
|
|
5449
5886
|
/**
|
|
@@ -5623,7 +6060,10 @@ var BrainerceClient = class {
|
|
|
5623
6060
|
type: d.type,
|
|
5624
6061
|
required: d.required,
|
|
5625
6062
|
enumValues: d.enumValues || void 0,
|
|
5626
|
-
position: d.position
|
|
6063
|
+
position: d.position,
|
|
6064
|
+
isCustomerInput: d.isCustomerInput,
|
|
6065
|
+
appliesToAllProducts: d.appliesToAllProducts,
|
|
6066
|
+
filterable: d.filterable
|
|
5627
6067
|
}))
|
|
5628
6068
|
};
|
|
5629
6069
|
}
|
|
@@ -5679,6 +6119,107 @@ var BrainerceClient = class {
|
|
|
5679
6119
|
async deleteMetafieldDefinition(definitionId) {
|
|
5680
6120
|
await this.adminRequest("DELETE", `/api/v1/metafield-definitions/${definitionId}`);
|
|
5681
6121
|
}
|
|
6122
|
+
/**
|
|
6123
|
+
* Replace the platform publishing configuration on a metafield definition.
|
|
6124
|
+
* `publishedOn` is the source of truth for which sales channels the field
|
|
6125
|
+
* appears on; `platformMetadata` carries the per-platform mapping config
|
|
6126
|
+
* required for sync.
|
|
6127
|
+
*
|
|
6128
|
+
* Requires Admin mode (apiKey).
|
|
6129
|
+
*
|
|
6130
|
+
* @example
|
|
6131
|
+
* ```typescript
|
|
6132
|
+
* await client.setMetafieldPlatforms('def_123', {
|
|
6133
|
+
* publishedOn: ['SHOPIFY', 'WOOCOMMERCE'],
|
|
6134
|
+
* platformMetadata: {
|
|
6135
|
+
* SHOPIFY: { namespace: 'custom', key: 'warranty' },
|
|
6136
|
+
* WOOCOMMERCE: { key: '_warranty_info' },
|
|
6137
|
+
* },
|
|
6138
|
+
* });
|
|
6139
|
+
* ```
|
|
6140
|
+
*/
|
|
6141
|
+
async setMetafieldPlatforms(definitionId, data) {
|
|
6142
|
+
return this.adminRequest(
|
|
6143
|
+
"PUT",
|
|
6144
|
+
`/api/v1/metafield-definitions/${definitionId}/platforms`,
|
|
6145
|
+
data
|
|
6146
|
+
);
|
|
6147
|
+
}
|
|
6148
|
+
// -------------- Vibe-coded site publishing (Admin Mode) --------------
|
|
6149
|
+
// Publish/unpublish category/tag/brand/metafield definitions to specific
|
|
6150
|
+
// vibe-coded sites. Mirrors the per-channel control already available for
|
|
6151
|
+
// Products/Coupons. When no publishes exist for an entity, it remains
|
|
6152
|
+
// visible to all vibe-coded sites of the store (legacy default).
|
|
6153
|
+
/**
|
|
6154
|
+
* Publish a metafield definition to a vibe-coded site (admin mode).
|
|
6155
|
+
* @example
|
|
6156
|
+
* ```typescript
|
|
6157
|
+
* await client.publishMetafieldDefinitionToVibeCodedSite('def_123', 'conn_456');
|
|
6158
|
+
* ```
|
|
6159
|
+
*/
|
|
6160
|
+
async publishMetafieldDefinitionToVibeCodedSite(definitionId, vibeCodedConnectionId) {
|
|
6161
|
+
return this.adminRequest(
|
|
6162
|
+
"POST",
|
|
6163
|
+
`/api/v1/metafield-definitions/${definitionId}/publish-vibe-coded`,
|
|
6164
|
+
{ vibeCodedConnectionId }
|
|
6165
|
+
);
|
|
6166
|
+
}
|
|
6167
|
+
/** Unpublish a metafield definition from a vibe-coded site (admin mode). */
|
|
6168
|
+
async unpublishMetafieldDefinitionFromVibeCodedSite(definitionId, vibeCodedConnectionId) {
|
|
6169
|
+
return this.adminRequest(
|
|
6170
|
+
"POST",
|
|
6171
|
+
`/api/v1/metafield-definitions/${definitionId}/unpublish-vibe-coded`,
|
|
6172
|
+
{ vibeCodedConnectionId }
|
|
6173
|
+
);
|
|
6174
|
+
}
|
|
6175
|
+
/** Publish a category to a vibe-coded site (admin mode). */
|
|
6176
|
+
async publishCategoryToVibeCodedSite(categoryId, vibeCodedConnectionId) {
|
|
6177
|
+
return this.adminRequest(
|
|
6178
|
+
"POST",
|
|
6179
|
+
`/api/v1/categories/${categoryId}/publish-vibe-coded`,
|
|
6180
|
+
{ vibeCodedConnectionId }
|
|
6181
|
+
);
|
|
6182
|
+
}
|
|
6183
|
+
/** Unpublish a category from a vibe-coded site (admin mode). */
|
|
6184
|
+
async unpublishCategoryFromVibeCodedSite(categoryId, vibeCodedConnectionId) {
|
|
6185
|
+
return this.adminRequest(
|
|
6186
|
+
"POST",
|
|
6187
|
+
`/api/v1/categories/${categoryId}/unpublish-vibe-coded`,
|
|
6188
|
+
{ vibeCodedConnectionId }
|
|
6189
|
+
);
|
|
6190
|
+
}
|
|
6191
|
+
/** Publish a tag to a vibe-coded site (admin mode). */
|
|
6192
|
+
async publishTagToVibeCodedSite(tagId, vibeCodedConnectionId) {
|
|
6193
|
+
return this.adminRequest(
|
|
6194
|
+
"POST",
|
|
6195
|
+
`/api/v1/tags/${tagId}/publish-vibe-coded`,
|
|
6196
|
+
{ vibeCodedConnectionId }
|
|
6197
|
+
);
|
|
6198
|
+
}
|
|
6199
|
+
/** Unpublish a tag from a vibe-coded site (admin mode). */
|
|
6200
|
+
async unpublishTagFromVibeCodedSite(tagId, vibeCodedConnectionId) {
|
|
6201
|
+
return this.adminRequest(
|
|
6202
|
+
"POST",
|
|
6203
|
+
`/api/v1/tags/${tagId}/unpublish-vibe-coded`,
|
|
6204
|
+
{ vibeCodedConnectionId }
|
|
6205
|
+
);
|
|
6206
|
+
}
|
|
6207
|
+
/** Publish a brand to a vibe-coded site (admin mode). */
|
|
6208
|
+
async publishBrandToVibeCodedSite(brandId, vibeCodedConnectionId) {
|
|
6209
|
+
return this.adminRequest(
|
|
6210
|
+
"POST",
|
|
6211
|
+
`/api/v1/brands/${brandId}/publish-vibe-coded`,
|
|
6212
|
+
{ vibeCodedConnectionId }
|
|
6213
|
+
);
|
|
6214
|
+
}
|
|
6215
|
+
/** Unpublish a brand from a vibe-coded site (admin mode). */
|
|
6216
|
+
async unpublishBrandFromVibeCodedSite(brandId, vibeCodedConnectionId) {
|
|
6217
|
+
return this.adminRequest(
|
|
6218
|
+
"POST",
|
|
6219
|
+
`/api/v1/brands/${brandId}/unpublish-vibe-coded`,
|
|
6220
|
+
{ vibeCodedConnectionId }
|
|
6221
|
+
);
|
|
6222
|
+
}
|
|
5682
6223
|
/**
|
|
5683
6224
|
* Replace the list of products a (customer-input) metafield definition is
|
|
5684
6225
|
* attached to. Diff-scoped: only rows for this definition are touched, so
|
|
@@ -6248,6 +6789,55 @@ function createWebhookHandler(handlers) {
|
|
|
6248
6789
|
};
|
|
6249
6790
|
}
|
|
6250
6791
|
|
|
6792
|
+
// src/payment-url.ts
|
|
6793
|
+
var ALLOWED_PAYMENT_HOSTS = [
|
|
6794
|
+
// Stripe
|
|
6795
|
+
"checkout.stripe.com",
|
|
6796
|
+
"js.stripe.com",
|
|
6797
|
+
"hooks.stripe.com",
|
|
6798
|
+
// PayPal
|
|
6799
|
+
"www.paypal.com",
|
|
6800
|
+
"www.sandbox.paypal.com",
|
|
6801
|
+
// Cardcom
|
|
6802
|
+
"secure.cardcom.solutions",
|
|
6803
|
+
// Meshulam
|
|
6804
|
+
"meshulam.co.il",
|
|
6805
|
+
// Grow (Linktech)
|
|
6806
|
+
"grow.link",
|
|
6807
|
+
"grow.security",
|
|
6808
|
+
// CreditGuard
|
|
6809
|
+
"creditguard.co.il",
|
|
6810
|
+
// Brainerce-hosted payment embeds (backend payment-embed proxy at
|
|
6811
|
+
// `/api/payment/embed/...` that fronts provider apps' embed shells —
|
|
6812
|
+
// e.g. cardcom-payments OpenFields wrapper). The match also covers
|
|
6813
|
+
// subdomains like `api.brainerce.com`, `staging.brainerce.com`.
|
|
6814
|
+
"brainerce.com"
|
|
6815
|
+
];
|
|
6816
|
+
function isAllowedPaymentUrl(url, options) {
|
|
6817
|
+
if (!url || typeof url !== "string") return false;
|
|
6818
|
+
let parsed;
|
|
6819
|
+
try {
|
|
6820
|
+
parsed = new URL(url);
|
|
6821
|
+
} catch {
|
|
6822
|
+
return false;
|
|
6823
|
+
}
|
|
6824
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
6825
|
+
if (options?.allowLocalhost && parsed.protocol === "http:" && (hostname === "localhost" || hostname === "127.0.0.1")) {
|
|
6826
|
+
return true;
|
|
6827
|
+
}
|
|
6828
|
+
if (parsed.protocol !== "https:") return false;
|
|
6829
|
+
const allowed = options?.extraHosts ? [...ALLOWED_PAYMENT_HOSTS, ...options.extraHosts] : ALLOWED_PAYMENT_HOSTS;
|
|
6830
|
+
return allowed.some((host) => hostname === host || hostname.endsWith("." + host));
|
|
6831
|
+
}
|
|
6832
|
+
function safePaymentRedirect(url, options) {
|
|
6833
|
+
if (!isAllowedPaymentUrl(url, options)) {
|
|
6834
|
+
throw new Error("Payment redirect URL is not in the allowlist");
|
|
6835
|
+
}
|
|
6836
|
+
if (typeof window !== "undefined") {
|
|
6837
|
+
window.location.href = url;
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6840
|
+
|
|
6251
6841
|
// src/types.ts
|
|
6252
6842
|
function isHtmlDescription(product) {
|
|
6253
6843
|
if (product?.descriptionFormat === "html") return true;
|
|
@@ -6487,9 +7077,11 @@ export {
|
|
|
6487
7077
|
getStockStatus,
|
|
6488
7078
|
getVariantOptions,
|
|
6489
7079
|
getVariantPrice,
|
|
7080
|
+
isAllowedPaymentUrl,
|
|
6490
7081
|
isCouponApplicableToProduct,
|
|
6491
7082
|
isHtmlDescription,
|
|
6492
7083
|
isWebhookEventType,
|
|
6493
7084
|
parseWebhookEvent,
|
|
7085
|
+
safePaymentRedirect,
|
|
6494
7086
|
verifyWebhook
|
|
6495
7087
|
};
|