brainerce 1.58.1 → 1.63.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/README.md +534 -125
- package/dist/index.d.mts +623 -43
- package/dist/index.d.ts +623 -43
- package/dist/index.js +359 -41
- package/dist/index.mjs +358 -41
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -78,6 +78,7 @@ __export(index_exports, {
|
|
|
78
78
|
jsonLdScriptProps: () => jsonLdScriptProps,
|
|
79
79
|
parseDateFieldValue: () => parseDateFieldValue,
|
|
80
80
|
parseWebhookEvent: () => parseWebhookEvent,
|
|
81
|
+
resolveRelativeBounds: () => resolveRelativeBounds,
|
|
81
82
|
resolveStoreLocalParts: () => resolveStoreLocalParts,
|
|
82
83
|
safePaymentRedirect: () => safePaymentRedirect,
|
|
83
84
|
stripHtml: () => stripHtml2,
|
|
@@ -203,7 +204,7 @@ function isDevGuardsEnabled() {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
// src/version.ts
|
|
206
|
-
var SDK_VERSION = "1.
|
|
207
|
+
var SDK_VERSION = "1.60.0";
|
|
207
208
|
|
|
208
209
|
// src/client.ts
|
|
209
210
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -360,6 +361,87 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
360
361
|
);
|
|
361
362
|
}
|
|
362
363
|
};
|
|
364
|
+
// -------------------- Marketing signup (newsletter) --------------------
|
|
365
|
+
/**
|
|
366
|
+
* Email marketing signup for a storefront — a newsletter popup, a footer
|
|
367
|
+
* capture bar, an exit-intent modal.
|
|
368
|
+
*
|
|
369
|
+
* **Confirmed opt-in, always.** `subscribe()` creates the contact and mails
|
|
370
|
+
* them a confirmation link. The address is NOT subscribed and CANNOT receive
|
|
371
|
+
* a campaign until the recipient clicks that link. This is not a setting:
|
|
372
|
+
* consent has to come from the mailbox, or anyone could subscribe anyone.
|
|
373
|
+
*
|
|
374
|
+
* So do not render "You're subscribed!" on success — render "Check your
|
|
375
|
+
* email to confirm." The one is a lie until the click lands.
|
|
376
|
+
*
|
|
377
|
+
* The response is identical for a brand-new address, one that is already
|
|
378
|
+
* subscribed, and one suppressed after a bounce, so the form can't be used to
|
|
379
|
+
* probe who shops here. Show the same message for every success.
|
|
380
|
+
*
|
|
381
|
+
* Storefront (public) and vibe-coded modes only. Rate-limited server-side to
|
|
382
|
+
* 3 requests / 60s per IP, plus one confirmation email per address per 24h.
|
|
383
|
+
*
|
|
384
|
+
* **Where the discount goes.** A "10% off your first order" popup needs a
|
|
385
|
+
* coupon from the dashboard — create one with the `customer_first_order`
|
|
386
|
+
* condition and show the code after a successful call. Subscribing does not
|
|
387
|
+
* mint a code on its own.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* // Newsletter popup — hidden honeypot input, Hebrew storefront
|
|
392
|
+
* await brainerce.marketing.subscribe({
|
|
393
|
+
* email: 'jane@example.com',
|
|
394
|
+
* locale: 'he',
|
|
395
|
+
* source: 'popup',
|
|
396
|
+
* honeypot: hiddenFieldValue,
|
|
397
|
+
* });
|
|
398
|
+
* // → show "בדקו את המייל שלכם כדי לאשר" — NOT "נרשמת בהצלחה"
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
this.marketing = {
|
|
402
|
+
subscribe: async (input) => {
|
|
403
|
+
if (this.isVibeCodedMode()) {
|
|
404
|
+
return this.vibeCodedRequest(
|
|
405
|
+
"POST",
|
|
406
|
+
"/marketing/subscribe",
|
|
407
|
+
input
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return this.storefrontRequest(
|
|
411
|
+
"POST",
|
|
412
|
+
"/marketing/subscribe",
|
|
413
|
+
input
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
// -------------------- Stock alerts --------------------
|
|
418
|
+
/**
|
|
419
|
+
* "Email me when this is back."
|
|
420
|
+
*
|
|
421
|
+
* ⛔ Not a newsletter signup, and must not be worded as one. It grants no
|
|
422
|
+
* marketing consent, creates no customer account, and the person is never
|
|
423
|
+
* mailed anything else as a result — exactly one message, about this item,
|
|
424
|
+
* with a link that stops it. Someone who unsubscribed from marketing can
|
|
425
|
+
* still use this, so do not gate it on consent.
|
|
426
|
+
*
|
|
427
|
+
* Show the affordance only on an item that is out of stock AND cannot be
|
|
428
|
+
* backordered. Every other case is silently ignored server-side — the
|
|
429
|
+
* response is uniform on purpose, so it cannot be used to read stock levels
|
|
430
|
+
* or test who is a customer — which means a button on an in-stock item looks
|
|
431
|
+
* like it worked and does nothing.
|
|
432
|
+
*
|
|
433
|
+
* Pass `variantId` on any product with variants. Without it the alert waits
|
|
434
|
+
* on the product as a whole, and a shopper who wanted the medium hears when
|
|
435
|
+
* the small comes back.
|
|
436
|
+
*/
|
|
437
|
+
this.stockAlerts = {
|
|
438
|
+
subscribe: async (input) => {
|
|
439
|
+
if (this.isVibeCodedMode()) {
|
|
440
|
+
return this.vibeCodedRequest("POST", "/stock-alerts", input);
|
|
441
|
+
}
|
|
442
|
+
return this.storefrontRequest("POST", "/stock-alerts", input);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
363
445
|
// -------------------- Content (typed merchant content) --------------------
|
|
364
446
|
/**
|
|
365
447
|
* Typed merchant content store: FAQ, Footer, Header, Announcement,
|
|
@@ -3134,6 +3216,11 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3134
3216
|
* Register a new customer with password (creates account)
|
|
3135
3217
|
* Works in vibe-coded, storefront, and admin mode
|
|
3136
3218
|
*
|
|
3219
|
+
* `birthMonth`/`birthDay` are optional and must be sent together. When
|
|
3220
|
+
* `getStoreInfo().requireBirthday` is true the merchant made the birthday
|
|
3221
|
+
* mandatory on that sales channel, and a call without both fields is
|
|
3222
|
+
* rejected with HTTP 400.
|
|
3223
|
+
*
|
|
3137
3224
|
* @example
|
|
3138
3225
|
* ```typescript
|
|
3139
3226
|
* const auth = await client.registerCustomer({
|
|
@@ -3141,6 +3228,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3141
3228
|
* password: 'securepassword123',
|
|
3142
3229
|
* firstName: 'Jane',
|
|
3143
3230
|
* lastName: 'Doe',
|
|
3231
|
+
* birthMonth: 4, // optional, unless the channel requires a birthday
|
|
3232
|
+
* birthDay: 17,
|
|
3144
3233
|
* });
|
|
3145
3234
|
* ```
|
|
3146
3235
|
*/
|
|
@@ -4205,7 +4294,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4205
4294
|
* try {
|
|
4206
4295
|
* await client.createCheckout(cartId);
|
|
4207
4296
|
* } catch (err) {
|
|
4208
|
-
*
|
|
4297
|
+
* // BrainerceError.details is the whole response body — the code lives
|
|
4298
|
+
* // there, NOT on the error object itself.
|
|
4299
|
+
* if ((err as BrainerceError).details?.code === 'PRICE_DRIFT') {
|
|
4209
4300
|
* // ask user to confirm new prices, then:
|
|
4210
4301
|
* await client.refreshCartSnapshots(cartId);
|
|
4211
4302
|
* await client.createCheckout(cartId);
|
|
@@ -4417,16 +4508,27 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4417
4508
|
* List visible reviews for a product (storefront / sales-channel modes).
|
|
4418
4509
|
* Reviews that the merchant has hidden are excluded.
|
|
4419
4510
|
*
|
|
4511
|
+
* Each review carries `images` — the photos its author attached, already
|
|
4512
|
+
* filtered to the ones shoppers are allowed to see. Always an array.
|
|
4513
|
+
*
|
|
4514
|
+
* Ordering defaults to `photos_first`: reviews carrying photos lead, newest-first
|
|
4515
|
+
* within each group. Pass `sort: 'newest'` for plain chronological order. On a
|
|
4516
|
+
* store with no review photos the two are identical.
|
|
4517
|
+
*
|
|
4420
4518
|
* @example
|
|
4421
4519
|
* ```typescript
|
|
4422
4520
|
* const { data, meta } = await client.listProductReviews('prod_123', { page: 1, limit: 20 });
|
|
4423
|
-
* data.forEach(r =>
|
|
4521
|
+
* data.forEach(r => {
|
|
4522
|
+
* console.log(r.rating, r.body, r.verifiedPurchase);
|
|
4523
|
+
* r.images.forEach(img => console.log(img.thumbnailUrl ?? img.url));
|
|
4524
|
+
* });
|
|
4424
4525
|
* ```
|
|
4425
4526
|
*/
|
|
4426
4527
|
async listProductReviews(productId, params) {
|
|
4427
4528
|
const queryParams = {};
|
|
4428
4529
|
if (params?.page) queryParams.page = params.page;
|
|
4429
4530
|
if (params?.limit) queryParams.limit = params.limit;
|
|
4531
|
+
if (params?.sort) queryParams.sort = params.sort;
|
|
4430
4532
|
if (this.isVibeCodedMode()) {
|
|
4431
4533
|
return this.vibeCodedRequest(
|
|
4432
4534
|
"GET",
|
|
@@ -4584,6 +4686,37 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4584
4686
|
storeId ? { storeId } : void 0
|
|
4585
4687
|
);
|
|
4586
4688
|
}
|
|
4689
|
+
/**
|
|
4690
|
+
* Admin: hide ONE photo on a review, leaving the review and its other photos
|
|
4691
|
+
* visible. Requires an API key with `reviews:write`.
|
|
4692
|
+
*/
|
|
4693
|
+
async hideProductReviewImage(imageId, storeId) {
|
|
4694
|
+
if (!this.apiKey) {
|
|
4695
|
+
throw new BrainerceError("hideProductReviewImage() requires admin (API key) mode", 400);
|
|
4696
|
+
}
|
|
4697
|
+
return this.adminRequest(
|
|
4698
|
+
"PATCH",
|
|
4699
|
+
`/api/v1/review-images/${encodePathSegment(imageId)}/hide`,
|
|
4700
|
+
void 0,
|
|
4701
|
+
storeId ? { storeId } : void 0
|
|
4702
|
+
);
|
|
4703
|
+
}
|
|
4704
|
+
/**
|
|
4705
|
+
* Admin: show one review photo. This is also the approve action — a photo that
|
|
4706
|
+
* has never been approved carries no `approvedAt`, and showing it stamps one, so
|
|
4707
|
+
* stores using review-photo approval need no separate verb.
|
|
4708
|
+
*/
|
|
4709
|
+
async showProductReviewImage(imageId, storeId) {
|
|
4710
|
+
if (!this.apiKey) {
|
|
4711
|
+
throw new BrainerceError("showProductReviewImage() requires admin (API key) mode", 400);
|
|
4712
|
+
}
|
|
4713
|
+
return this.adminRequest(
|
|
4714
|
+
"PATCH",
|
|
4715
|
+
`/api/v1/review-images/${encodePathSegment(imageId)}/show`,
|
|
4716
|
+
void 0,
|
|
4717
|
+
storeId ? { storeId } : void 0
|
|
4718
|
+
);
|
|
4719
|
+
}
|
|
4587
4720
|
/** Admin: unhide a previously hidden review. */
|
|
4588
4721
|
async showProductReview(reviewId, storeId) {
|
|
4589
4722
|
if (!this.apiKey) {
|
|
@@ -6043,6 +6176,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
6043
6176
|
* Get applicable custom field definitions for a checkout.
|
|
6044
6177
|
* Returns fields filtered by visibility conditions (delivery type, products in cart).
|
|
6045
6178
|
* Use these to render dynamic input fields in the checkout flow.
|
|
6179
|
+
*
|
|
6180
|
+
* **Vibe-coded or storefront mode only.** There is no checkout custom-field
|
|
6181
|
+
* route on the API-key `/v1` surface; in admin mode this throws.
|
|
6046
6182
|
*/
|
|
6047
6183
|
async getCheckoutCustomFields(checkoutId) {
|
|
6048
6184
|
if (this.isVibeCodedMode()) {
|
|
@@ -6057,15 +6193,18 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
6057
6193
|
`/checkout/${encodePathSegment(checkoutId)}/custom-fields`
|
|
6058
6194
|
);
|
|
6059
6195
|
}
|
|
6060
|
-
|
|
6061
|
-
"
|
|
6062
|
-
|
|
6196
|
+
throw new BrainerceError(
|
|
6197
|
+
"getCheckoutCustomFields is only available in vibe-coded or storefront mode",
|
|
6198
|
+
400
|
|
6063
6199
|
);
|
|
6064
6200
|
}
|
|
6065
6201
|
/**
|
|
6066
6202
|
* Set checkout custom field values and recalculate surcharges.
|
|
6067
6203
|
* The checkout total is automatically updated to include surcharges.
|
|
6068
6204
|
*
|
|
6205
|
+
* **Vibe-coded or storefront mode only.** There is no checkout custom-field
|
|
6206
|
+
* route on the API-key `/v1` surface; in admin mode this throws.
|
|
6207
|
+
*
|
|
6069
6208
|
* @example
|
|
6070
6209
|
* ```typescript
|
|
6071
6210
|
* const checkout = await client.setCheckoutCustomFields(checkoutId, {
|
|
@@ -6093,10 +6232,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
6093
6232
|
data
|
|
6094
6233
|
);
|
|
6095
6234
|
}
|
|
6096
|
-
|
|
6097
|
-
"
|
|
6098
|
-
|
|
6099
|
-
data
|
|
6235
|
+
throw new BrainerceError(
|
|
6236
|
+
"setCheckoutCustomFields is only available in vibe-coded or storefront mode",
|
|
6237
|
+
400
|
|
6100
6238
|
);
|
|
6101
6239
|
}
|
|
6102
6240
|
/**
|
|
@@ -7234,11 +7372,20 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
7234
7372
|
);
|
|
7235
7373
|
}
|
|
7236
7374
|
/**
|
|
7237
|
-
* Update the current customer's profile (requires customerToken)
|
|
7238
|
-
* Only available in storefront mode
|
|
7375
|
+
* Update the current customer's profile (requires customerToken).
|
|
7376
|
+
* Only available in storefront and vibe-coded mode.
|
|
7239
7377
|
*
|
|
7240
|
-
* `birthMonth`/`birthDay` (1-12 / 1-31
|
|
7241
|
-
*
|
|
7378
|
+
* `birthMonth`/`birthDay` (1-12 / 1-31) power the loyalty birthday gift.
|
|
7379
|
+
* Month and day only, never a year, for privacy. Send both or neither, and
|
|
7380
|
+
* the day has to exist in the month: anything else is rejected with HTTP 400.
|
|
7381
|
+
* Send `null` for both to REMOVE a stored birthday. Leaving the keys out is a
|
|
7382
|
+
* different request and keeps the stored value, so a profile form whose
|
|
7383
|
+
* fields the shopper cleared has to send nulls, not omit them.
|
|
7384
|
+
*
|
|
7385
|
+
* The saved values now come back on the returned `CustomerProfile`, on
|
|
7386
|
+
* `getMyProfile()` and on every customer read type, so a profile form can
|
|
7387
|
+
* re-render the birthday it just saved. Before this, no API returned the two
|
|
7388
|
+
* fields at all and the form always came back blank.
|
|
7242
7389
|
*/
|
|
7243
7390
|
async updateMyProfile(data) {
|
|
7244
7391
|
if (!this.customerToken && !this.proxyMode) {
|
|
@@ -8362,8 +8509,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
8362
8509
|
// /api/stores/:storeId/products/:productId/modifier-groups[/:attachmentId]
|
|
8363
8510
|
//
|
|
8364
8511
|
// Server-side validation failures arrive as a structured 400 envelope:
|
|
8365
|
-
// { code: 'MODIFIER_VALIDATION_FAILED', errors:
|
|
8366
|
-
//
|
|
8512
|
+
// { code: 'MODIFIER_VALIDATION_FAILED', message, details: { errors: [...] } }
|
|
8513
|
+
// BrainerceError.details holds the WHOLE body, so the issue list is
|
|
8514
|
+
// `err.details.details.errors` — see ModifierValidationFailedError.
|
|
8367
8515
|
/**
|
|
8368
8516
|
* List modifier groups in a store, paginated.
|
|
8369
8517
|
* Requires Admin mode (apiKey).
|
|
@@ -9414,28 +9562,99 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9414
9562
|
}
|
|
9415
9563
|
throw new Error("uploadCustomizationFile requires storefront or vibe-coded mode");
|
|
9416
9564
|
}
|
|
9565
|
+
/**
|
|
9566
|
+
* Upload one photo to attach to a product review.
|
|
9567
|
+
*
|
|
9568
|
+
* Available in storefront and vibe-coded modes, and — unlike
|
|
9569
|
+
* `uploadCustomizationFile` — it REQUIRES a logged-in customer who has actually
|
|
9570
|
+
* bought the product. Call `setCustomerToken(...)` first. That is the same bar as
|
|
9571
|
+
* writing the review itself, checked here so an ineligible shopper is told before
|
|
9572
|
+
* they wait for the upload rather than after.
|
|
9573
|
+
*
|
|
9574
|
+
* Returns a storage `key`. Collect the keys and pass them as `imageKeys` when you
|
|
9575
|
+
* submit or update the review — the `url` is for a local preview only, and sending
|
|
9576
|
+
* it back instead of the key will be rejected.
|
|
9577
|
+
*
|
|
9578
|
+
* Server rules:
|
|
9579
|
+
* - `image/jpeg|png|webp|gif` only, cross-checked against the file's real bytes.
|
|
9580
|
+
* - Max 5 MB and 40 megapixels per file.
|
|
9581
|
+
* - Throttled to 10 uploads / minute → HTTP 429.
|
|
9582
|
+
* - 403 when the store has review photos turned off, or the customer has not
|
|
9583
|
+
* bought the product; 400 once the review is already at its photo cap.
|
|
9584
|
+
* - EXIF is stripped (so GPS coordinates never reach the storefront) while the
|
|
9585
|
+
* orientation tag is applied first, so phone photos stay upright.
|
|
9586
|
+
* - A photo uploaded but never attached to a submitted review is reclaimed after
|
|
9587
|
+
* 7 days.
|
|
9588
|
+
*
|
|
9589
|
+
* Read `photos` from `getMyProductReview()` for the store's live limits rather
|
|
9590
|
+
* than hard-coding them.
|
|
9591
|
+
*
|
|
9592
|
+
* @example
|
|
9593
|
+
* ```ts
|
|
9594
|
+
* const { photos } = await client.getMyProductReview(productId);
|
|
9595
|
+
* if (photos.enabled) {
|
|
9596
|
+
* const uploads = await Promise.all(
|
|
9597
|
+
* [...fileInput.files].slice(0, photos.maxPerReview)
|
|
9598
|
+
* .map(f => client.uploadReviewPhoto(productId, f))
|
|
9599
|
+
* );
|
|
9600
|
+
* await client.submitProductReview(productId, {
|
|
9601
|
+
* rating: 5,
|
|
9602
|
+
* body: 'Arrived beautifully wrapped.',
|
|
9603
|
+
* imageKeys: uploads.map(u => u.key),
|
|
9604
|
+
* });
|
|
9605
|
+
* }
|
|
9606
|
+
* ```
|
|
9607
|
+
*/
|
|
9608
|
+
async uploadReviewPhoto(productId, file) {
|
|
9609
|
+
const formData = new FormData();
|
|
9610
|
+
formData.append("file", file);
|
|
9611
|
+
if (this.isVibeCodedMode()) {
|
|
9612
|
+
return this.vibeCodedRequest(
|
|
9613
|
+
"POST",
|
|
9614
|
+
`/products/${encodePathSegment(productId)}/review-photo`,
|
|
9615
|
+
formData
|
|
9616
|
+
);
|
|
9617
|
+
}
|
|
9618
|
+
if (this.storeId && !this.apiKey) {
|
|
9619
|
+
return this.storefrontRequest(
|
|
9620
|
+
"POST",
|
|
9621
|
+
`/products/${encodePathSegment(productId)}/review-photo`,
|
|
9622
|
+
formData
|
|
9623
|
+
);
|
|
9624
|
+
}
|
|
9625
|
+
throw new Error("uploadReviewPhoto requires storefront or vibe-coded mode");
|
|
9626
|
+
}
|
|
9417
9627
|
// -------------------- Team Management (Admin) - DEPRECATED --------------------
|
|
9418
|
-
//
|
|
9628
|
+
// Account-level team methods. These are the ONLY team endpoints reachable with
|
|
9629
|
+
// a `brainerce_*` API key. The store-level equivalents (`getStoreTeam`,
|
|
9630
|
+
// `inviteStoreMember`, `updateStoreMember`, ...) sit behind
|
|
9631
|
+
// `DashboardOnlyGuard`, which rejects api_key principals by design — so they
|
|
9632
|
+
// return 403 from the SDK, not 404, and "fixing" their path would not help.
|
|
9633
|
+
// Do not migrate SDK code onto them.
|
|
9419
9634
|
/**
|
|
9420
|
-
* @deprecated
|
|
9635
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `getStoreTeam`
|
|
9636
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9421
9637
|
*/
|
|
9422
9638
|
async getTeamMembers() {
|
|
9423
9639
|
return this.adminRequest("GET", "/api/v1/team/members");
|
|
9424
9640
|
}
|
|
9425
9641
|
/**
|
|
9426
|
-
* @deprecated
|
|
9642
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `getStoreTeam`
|
|
9643
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9427
9644
|
*/
|
|
9428
9645
|
async getTeamInvitations() {
|
|
9429
9646
|
return this.adminRequest("GET", "/api/v1/team/invitations");
|
|
9430
9647
|
}
|
|
9431
9648
|
/**
|
|
9432
|
-
* @deprecated
|
|
9649
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `inviteStoreMember`
|
|
9650
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9433
9651
|
*/
|
|
9434
9652
|
async inviteTeamMember(data) {
|
|
9435
9653
|
return this.adminRequest("POST", "/api/v1/team/invitations", data);
|
|
9436
9654
|
}
|
|
9437
9655
|
/**
|
|
9438
|
-
* @deprecated
|
|
9656
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `resendStoreInvitation`
|
|
9657
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9439
9658
|
*/
|
|
9440
9659
|
async resendTeamInvitation(invitationId) {
|
|
9441
9660
|
return this.adminRequest(
|
|
@@ -9444,7 +9663,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9444
9663
|
);
|
|
9445
9664
|
}
|
|
9446
9665
|
/**
|
|
9447
|
-
* @deprecated
|
|
9666
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `revokeStoreInvitation`
|
|
9667
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9448
9668
|
*/
|
|
9449
9669
|
async revokeTeamInvitation(invitationId) {
|
|
9450
9670
|
await this.adminRequest(
|
|
@@ -9453,7 +9673,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9453
9673
|
);
|
|
9454
9674
|
}
|
|
9455
9675
|
/**
|
|
9456
|
-
* @deprecated
|
|
9676
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `updateStoreMember`
|
|
9677
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9457
9678
|
*/
|
|
9458
9679
|
async updateTeamMemberRole(memberId, data) {
|
|
9459
9680
|
return this.adminRequest(
|
|
@@ -9463,7 +9684,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9463
9684
|
);
|
|
9464
9685
|
}
|
|
9465
9686
|
/**
|
|
9466
|
-
* @deprecated
|
|
9687
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `removeStoreMember`
|
|
9688
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9467
9689
|
*/
|
|
9468
9690
|
async removeTeamMember(memberId) {
|
|
9469
9691
|
await this.adminRequest("DELETE", `/api/v1/team/members/${encodePathSegment(memberId)}`);
|
|
@@ -10089,7 +10311,7 @@ var DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\
|
|
|
10089
10311
|
var MIN_OFFSET_MINUTES = -12 * 60;
|
|
10090
10312
|
var MAX_OFFSET_MINUTES = 14 * 60;
|
|
10091
10313
|
var VALID_OFFSET_MINUTE_PARTS = [0, 30, 45];
|
|
10092
|
-
function validateDateAvailabilityConfig(config, fieldType) {
|
|
10314
|
+
function validateDateAvailabilityConfig(config, fieldType, surface = "checkout") {
|
|
10093
10315
|
const errors = [];
|
|
10094
10316
|
if (!config) return errors;
|
|
10095
10317
|
if (config.minDate !== void 0 && !DATE_RE.test(config.minDate)) {
|
|
@@ -10101,6 +10323,29 @@ function validateDateAvailabilityConfig(config, fieldType) {
|
|
|
10101
10323
|
if (config.minDate && config.maxDate && DATE_RE.test(config.minDate) && DATE_RE.test(config.maxDate) && config.minDate > config.maxDate) {
|
|
10102
10324
|
errors.push("minDate must be on or before maxDate");
|
|
10103
10325
|
}
|
|
10326
|
+
if (config.leadTimeMinutes !== void 0) {
|
|
10327
|
+
if (!Number.isInteger(config.leadTimeMinutes) || config.leadTimeMinutes < 0) {
|
|
10328
|
+
errors.push("leadTimeMinutes must be a non-negative integer");
|
|
10329
|
+
}
|
|
10330
|
+
}
|
|
10331
|
+
if (config.maxDaysAhead !== void 0) {
|
|
10332
|
+
if (!Number.isInteger(config.maxDaysAhead) || config.maxDaysAhead < 1) {
|
|
10333
|
+
errors.push("maxDaysAhead must be a positive integer");
|
|
10334
|
+
}
|
|
10335
|
+
}
|
|
10336
|
+
if (config.cutoffTime !== void 0 && !TIME_RE.test(config.cutoffTime)) {
|
|
10337
|
+
errors.push("cutoffTime must be in HH:mm format");
|
|
10338
|
+
}
|
|
10339
|
+
if (surface !== "checkout") {
|
|
10340
|
+
const relativeKeys = ["leadTimeMinutes", "cutoffTime", "maxDaysAhead"].filter(
|
|
10341
|
+
(k) => config[k] !== void 0
|
|
10342
|
+
);
|
|
10343
|
+
if (relativeKeys.length > 0) {
|
|
10344
|
+
errors.push(
|
|
10345
|
+
`${relativeKeys.join("/")} only apply to checkout fields, not ${surface} fields`
|
|
10346
|
+
);
|
|
10347
|
+
}
|
|
10348
|
+
}
|
|
10104
10349
|
if (config.blockedWeekdays) {
|
|
10105
10350
|
for (const d of config.blockedWeekdays) {
|
|
10106
10351
|
if (!Number.isInteger(d) || d < 0 || d > 6) {
|
|
@@ -10118,22 +10363,37 @@ function validateDateAvailabilityConfig(config, fieldType) {
|
|
|
10118
10363
|
}
|
|
10119
10364
|
}
|
|
10120
10365
|
if (config.businessHours) {
|
|
10121
|
-
const
|
|
10366
|
+
const windowsByWeekday = /* @__PURE__ */ new Map();
|
|
10122
10367
|
for (const w of config.businessHours) {
|
|
10123
10368
|
if (!Number.isInteger(w.weekday) || w.weekday < 0 || w.weekday > 6) {
|
|
10124
10369
|
errors.push(`businessHours.weekday must be an integer 0-6, got ${JSON.stringify(w.weekday)}`);
|
|
10125
10370
|
continue;
|
|
10126
10371
|
}
|
|
10127
|
-
if (seenWeekdays.has(w.weekday)) {
|
|
10128
|
-
errors.push(`businessHours has more than one window for weekday ${w.weekday}`);
|
|
10129
|
-
}
|
|
10130
|
-
seenWeekdays.add(w.weekday);
|
|
10131
10372
|
const openValid = TIME_RE.test(w.open);
|
|
10132
10373
|
const closeValid = TIME_RE.test(w.close);
|
|
10133
10374
|
if (!openValid) errors.push(`businessHours.open must be HH:mm, got "${w.open}"`);
|
|
10134
10375
|
if (!closeValid) errors.push(`businessHours.close must be HH:mm, got "${w.close}"`);
|
|
10135
|
-
if (openValid
|
|
10376
|
+
if (!openValid || !closeValid) continue;
|
|
10377
|
+
if (w.open >= w.close) {
|
|
10136
10378
|
errors.push(`businessHours for weekday ${w.weekday}: open must be before close`);
|
|
10379
|
+
continue;
|
|
10380
|
+
}
|
|
10381
|
+
const forDay = windowsByWeekday.get(w.weekday);
|
|
10382
|
+
if (forDay) forDay.push(w);
|
|
10383
|
+
else windowsByWeekday.set(w.weekday, [w]);
|
|
10384
|
+
}
|
|
10385
|
+
const weekdaysWithWindows = Array.from(windowsByWeekday.keys()).sort((a, b) => a - b);
|
|
10386
|
+
for (const weekday of weekdaysWithWindows) {
|
|
10387
|
+
const sorted = [...windowsByWeekday.get(weekday) ?? []].sort(
|
|
10388
|
+
(a, b) => a.open < b.open ? -1 : a.open > b.open ? 1 : 0
|
|
10389
|
+
);
|
|
10390
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
10391
|
+
if (sorted[i].open < sorted[i - 1].close) {
|
|
10392
|
+
errors.push(
|
|
10393
|
+
`businessHours for weekday ${weekday}: ${sorted[i - 1].open}-${sorted[i - 1].close} overlaps ${sorted[i].open}-${sorted[i].close}`
|
|
10394
|
+
);
|
|
10395
|
+
break;
|
|
10396
|
+
}
|
|
10137
10397
|
}
|
|
10138
10398
|
}
|
|
10139
10399
|
}
|
|
@@ -10189,6 +10449,32 @@ function resolveStoreLocalParts(instant, timezone) {
|
|
|
10189
10449
|
weekday: WEEKDAY_TO_INDEX[get("weekday")] ?? instant.getUTCDay()
|
|
10190
10450
|
};
|
|
10191
10451
|
}
|
|
10452
|
+
function resolveRelativeBounds(config, clock) {
|
|
10453
|
+
if (!config || !clock) return {};
|
|
10454
|
+
const leadMinutes = typeof config.leadTimeMinutes === "number" && Number.isFinite(config.leadTimeMinutes) ? Math.max(0, Math.floor(config.leadTimeMinutes)) : 0;
|
|
10455
|
+
const cutoff = typeof config.cutoffTime === "string" && TIME_RE.test(config.cutoffTime) ? config.cutoffTime : null;
|
|
10456
|
+
const daysAhead = typeof config.maxDaysAhead === "number" && Number.isFinite(config.maxDaysAhead) && config.maxDaysAhead >= 1 ? Math.floor(config.maxDaysAhead) : null;
|
|
10457
|
+
if (leadMinutes === 0 && !cutoff && daysAhead === null) return {};
|
|
10458
|
+
const now = clock.now ?? /* @__PURE__ */ new Date();
|
|
10459
|
+
const nowLocal = resolveStoreLocalParts(now, clock.timezone);
|
|
10460
|
+
const bounds = {};
|
|
10461
|
+
if (leadMinutes > 0 || cutoff) {
|
|
10462
|
+
const earliestInstant = new Date(now.getTime() + leadMinutes * 6e4);
|
|
10463
|
+
let earliestDate = resolveStoreLocalParts(earliestInstant, clock.timezone).dateYYYYMMDD;
|
|
10464
|
+
if (cutoff && nowLocal.hhmm >= cutoff) earliestDate = addCalendarDays(earliestDate, 1);
|
|
10465
|
+
bounds.earliestDate = earliestDate;
|
|
10466
|
+
if (leadMinutes > 0) bounds.earliestInstant = earliestInstant;
|
|
10467
|
+
}
|
|
10468
|
+
if (daysAhead !== null) {
|
|
10469
|
+
bounds.latestDate = addCalendarDays(nowLocal.dateYYYYMMDD, daysAhead);
|
|
10470
|
+
}
|
|
10471
|
+
return bounds;
|
|
10472
|
+
}
|
|
10473
|
+
function addCalendarDays(dateYYYYMMDD, days) {
|
|
10474
|
+
const [year, month, day] = dateYYYYMMDD.split("-").map(Number);
|
|
10475
|
+
const shifted = new Date(Date.UTC(year, month - 1, day + days));
|
|
10476
|
+
return `${shifted.getUTCFullYear()}-${pad2(shifted.getUTCMonth() + 1)}-${pad2(shifted.getUTCDate())}`;
|
|
10477
|
+
}
|
|
10192
10478
|
function parseDateFieldValue(raw, fieldType, timezone) {
|
|
10193
10479
|
const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
|
|
10194
10480
|
const expected = fieldType === "DATE" ? "expected a calendar date in YYYY-MM-DD format" : "expected an ISO-8601 date/time such as 2026-08-13T13:00:00+03:00";
|
|
@@ -10284,19 +10570,30 @@ function timezoneOffsetMs(utcMillis, timezone) {
|
|
|
10284
10570
|
const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
|
|
10285
10571
|
return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
|
|
10286
10572
|
}
|
|
10287
|
-
function isCalendarDateAllowed(dateYYYYMMDD, config) {
|
|
10573
|
+
function isCalendarDateAllowed(dateYYYYMMDD, config, clock) {
|
|
10288
10574
|
if (!config) return true;
|
|
10289
10575
|
if (config.minDate && dateYYYYMMDD < config.minDate) return false;
|
|
10290
10576
|
if (config.maxDate && dateYYYYMMDD > config.maxDate) return false;
|
|
10577
|
+
if (relativeBoundFailure(dateYYYYMMDD, config, clock)) return false;
|
|
10291
10578
|
if (config.blockedDates?.includes(dateYYYYMMDD)) return false;
|
|
10292
10579
|
if (config.blockedWeekdays?.length) {
|
|
10293
10580
|
if (config.blockedWeekdays.includes(weekdayOfDateString(dateYYYYMMDD))) return false;
|
|
10294
10581
|
}
|
|
10295
10582
|
return true;
|
|
10296
10583
|
}
|
|
10297
|
-
function
|
|
10584
|
+
function relativeBoundFailure(dateYYYYMMDD, config, clock) {
|
|
10585
|
+
const bounds = resolveRelativeBounds(config, clock);
|
|
10586
|
+
if (bounds.earliestDate && dateYYYYMMDD < bounds.earliestDate) {
|
|
10587
|
+
return `the earliest available date is ${bounds.earliestDate}`;
|
|
10588
|
+
}
|
|
10589
|
+
if (bounds.latestDate && dateYYYYMMDD > bounds.latestDate) {
|
|
10590
|
+
return `the latest available date is ${bounds.latestDate}`;
|
|
10591
|
+
}
|
|
10592
|
+
return null;
|
|
10593
|
+
}
|
|
10594
|
+
function computeAvailableSlots(config, dateYYYYMMDD, clock) {
|
|
10298
10595
|
if (!config) return [];
|
|
10299
|
-
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
10596
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) return [];
|
|
10300
10597
|
if (!config.businessHours?.length || !config.slotDurationMinutes) return [];
|
|
10301
10598
|
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
10302
10599
|
const windows = config.businessHours.filter((w) => w.weekday === weekday);
|
|
@@ -10309,27 +10606,47 @@ function computeAvailableSlots(config, dateYYYYMMDD) {
|
|
|
10309
10606
|
slots.push(minutesToHHMM(t));
|
|
10310
10607
|
}
|
|
10311
10608
|
}
|
|
10312
|
-
|
|
10609
|
+
const ordered = Array.from(new Set(slots)).sort();
|
|
10610
|
+
const { earliestInstant } = resolveRelativeBounds(config, clock);
|
|
10611
|
+
if (!earliestInstant || !clock) return ordered;
|
|
10612
|
+
return ordered.filter((hhmm) => {
|
|
10613
|
+
const [hour, minute] = hhmm.split(":").map(Number);
|
|
10614
|
+
const start = instantFromStoreLocal(dateYYYYMMDD, hour, minute, 0, 0, clock.timezone);
|
|
10615
|
+
return start.getTime() >= earliestInstant.getTime();
|
|
10616
|
+
});
|
|
10313
10617
|
}
|
|
10314
|
-
function getBusinessHoursForDate(config, dateYYYYMMDD) {
|
|
10618
|
+
function getBusinessHoursForDate(config, dateYYYYMMDD, clock) {
|
|
10315
10619
|
if (!config?.businessHours?.length) return [];
|
|
10316
|
-
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
10620
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) return [];
|
|
10317
10621
|
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
10318
10622
|
return config.businessHours.filter((w) => w.weekday === weekday);
|
|
10319
10623
|
}
|
|
10320
|
-
function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
10624
|
+
function isDateValueAllowed(instant, config, fieldType, timezone, now) {
|
|
10321
10625
|
if (!config) return { allowed: true };
|
|
10626
|
+
const clock = { timezone, now };
|
|
10322
10627
|
if (fieldType === "DATE") {
|
|
10323
10628
|
const dateYYYYMMDD = `${instant.getUTCFullYear()}-${pad2(instant.getUTCMonth() + 1)}-${pad2(instant.getUTCDate())}`;
|
|
10324
|
-
|
|
10629
|
+
const relative2 = relativeBoundFailure(dateYYYYMMDD, config, clock);
|
|
10630
|
+
if (relative2) return { allowed: false, reason: relative2 };
|
|
10631
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) {
|
|
10325
10632
|
return { allowed: false, reason: "date is outside the allowed range" };
|
|
10326
10633
|
}
|
|
10327
10634
|
return { allowed: true };
|
|
10328
10635
|
}
|
|
10329
10636
|
const local = resolveStoreLocalParts(instant, timezone);
|
|
10330
|
-
|
|
10637
|
+
const relative = relativeBoundFailure(local.dateYYYYMMDD, config, clock);
|
|
10638
|
+
if (relative) return { allowed: false, reason: relative };
|
|
10639
|
+
if (!isCalendarDateAllowed(local.dateYYYYMMDD, config, clock)) {
|
|
10331
10640
|
return { allowed: false, reason: "date is outside the allowed range" };
|
|
10332
10641
|
}
|
|
10642
|
+
const { earliestInstant } = resolveRelativeBounds(config, clock);
|
|
10643
|
+
if (earliestInstant && instant.getTime() < earliestInstant.getTime()) {
|
|
10644
|
+
const earliestLocal = resolveStoreLocalParts(earliestInstant, timezone);
|
|
10645
|
+
return {
|
|
10646
|
+
allowed: false,
|
|
10647
|
+
reason: `the earliest available time is ${earliestLocal.dateYYYYMMDD} ${earliestLocal.hhmm}`
|
|
10648
|
+
};
|
|
10649
|
+
}
|
|
10333
10650
|
if (!config.businessHours?.length) {
|
|
10334
10651
|
return { allowed: true };
|
|
10335
10652
|
}
|
|
@@ -10338,7 +10655,7 @@ function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
|
10338
10655
|
return { allowed: false, reason: "no business hours are configured for this day" };
|
|
10339
10656
|
}
|
|
10340
10657
|
if (config.slotDurationMinutes) {
|
|
10341
|
-
const slots = computeAvailableSlots(config, local.dateYYYYMMDD);
|
|
10658
|
+
const slots = computeAvailableSlots(config, local.dateYYYYMMDD, clock);
|
|
10342
10659
|
if (!slots.includes(local.hhmm)) {
|
|
10343
10660
|
return { allowed: false, reason: "time does not match an available slot" };
|
|
10344
10661
|
}
|
|
@@ -10969,6 +11286,7 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
10969
11286
|
jsonLdScriptProps,
|
|
10970
11287
|
parseDateFieldValue,
|
|
10971
11288
|
parseWebhookEvent,
|
|
11289
|
+
resolveRelativeBounds,
|
|
10972
11290
|
resolveStoreLocalParts,
|
|
10973
11291
|
safePaymentRedirect,
|
|
10974
11292
|
stripHtml,
|