brainerce 1.59.0 → 2.0.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 +560 -59
- package/dist/index.d.mts +711 -196
- package/dist/index.d.ts +711 -196
- package/dist/index.js +531 -268
- package/dist/index.mjs +530 -268
- 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 = "
|
|
207
|
+
var SDK_VERSION = "2.0.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,
|
|
@@ -568,7 +650,7 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
568
650
|
*/
|
|
569
651
|
this.blog = /* @__PURE__ */ (() => {
|
|
570
652
|
const publicBase = "/blog/posts";
|
|
571
|
-
const adminBase = "/blog/posts";
|
|
653
|
+
const adminBase = "/api/blog/posts";
|
|
572
654
|
const requireAdmin = (action) => {
|
|
573
655
|
if (this.isVibeCodedMode() || this.storeId && !this.apiKey) {
|
|
574
656
|
throw new BrainerceError(
|
|
@@ -2402,115 +2484,97 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2402
2484
|
return this.request("PATCH", `/api/v1/orders/${encodePathSegment(orderId)}`, data);
|
|
2403
2485
|
}
|
|
2404
2486
|
/**
|
|
2405
|
-
* Update order status
|
|
2487
|
+
* Update order status.
|
|
2488
|
+
*
|
|
2489
|
+
* **Not callable — use {@link updateOrder} instead.** Status changes do work
|
|
2490
|
+
* over the API key, just by a different route.
|
|
2491
|
+
*
|
|
2492
|
+
* @deprecated Call `updateOrder(orderId, { status })`.
|
|
2406
2493
|
*
|
|
2407
2494
|
* @example
|
|
2408
2495
|
* ```typescript
|
|
2409
|
-
* const order = await client.
|
|
2496
|
+
* const order = await client.updateOrder('order_123', { status: 'SHIPPED' });
|
|
2410
2497
|
* ```
|
|
2411
2498
|
*/
|
|
2412
2499
|
async updateOrderStatus(orderId, status) {
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2500
|
+
void orderId;
|
|
2501
|
+
void status;
|
|
2502
|
+
throw new BrainerceError(
|
|
2503
|
+
"updateOrderStatus is not a route on the API-key /v1 surface. Status changes go through updateOrder instead: updateOrder(orderId, { status }) PATCHes /api/v1/orders/:id and ends up in the same service call.",
|
|
2504
|
+
400
|
|
2505
|
+
);
|
|
2416
2506
|
}
|
|
2417
2507
|
/**
|
|
2418
|
-
* Update order payment method
|
|
2419
|
-
* Note: Only WooCommerce supports syncing payment method changes back to platform
|
|
2508
|
+
* Update order payment method.
|
|
2420
2509
|
*
|
|
2421
|
-
*
|
|
2422
|
-
*
|
|
2423
|
-
*
|
|
2424
|
-
* ```
|
|
2510
|
+
* **Not callable.** The API-key `/v1` surface has no payment-method route,
|
|
2511
|
+
* so this throws in every mode. Change the payment method from the
|
|
2512
|
+
* dashboard until the route ships.
|
|
2425
2513
|
*/
|
|
2426
2514
|
async updatePaymentMethod(orderId, paymentMethod) {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
}
|
|
2515
|
+
void orderId;
|
|
2516
|
+
void paymentMethod;
|
|
2517
|
+
throw new BrainerceError(
|
|
2518
|
+
"updatePaymentMethod is not a route on the API-key /v1 surface. There is no orders/:id/payment-method endpoint to call; change the payment method from the Brainerce dashboard.",
|
|
2519
|
+
400
|
|
2433
2520
|
);
|
|
2434
2521
|
}
|
|
2435
2522
|
/**
|
|
2436
|
-
* Update order notes
|
|
2523
|
+
* Update order notes.
|
|
2437
2524
|
*
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
2441
|
-
* ```
|
|
2525
|
+
* **Not callable.** The API-key `/v1` surface has no order-notes route, so
|
|
2526
|
+
* this throws in every mode. Edit notes from the dashboard until the route
|
|
2527
|
+
* ships.
|
|
2442
2528
|
*/
|
|
2443
2529
|
async updateOrderNotes(orderId, notes) {
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2530
|
+
void orderId;
|
|
2531
|
+
void notes;
|
|
2532
|
+
throw new BrainerceError(
|
|
2533
|
+
"updateOrderNotes is not a route on the API-key /v1 surface. There is no orders/:id/notes endpoint to call; edit the note from the Brainerce dashboard.",
|
|
2534
|
+
400
|
|
2535
|
+
);
|
|
2447
2536
|
}
|
|
2448
2537
|
/**
|
|
2449
|
-
* Get refunds for an order
|
|
2450
|
-
* Returns refunds from the source platform (Shopify/WooCommerce only)
|
|
2538
|
+
* Get refunds for an order.
|
|
2451
2539
|
*
|
|
2452
|
-
*
|
|
2453
|
-
*
|
|
2454
|
-
*
|
|
2455
|
-
* console.log('Total refunds:', refunds.length);
|
|
2456
|
-
* ```
|
|
2540
|
+
* **Not callable.** The API-key `/v1` surface has no refunds route, so this
|
|
2541
|
+
* throws in every mode. Read refunds from the dashboard until the route
|
|
2542
|
+
* ships.
|
|
2457
2543
|
*/
|
|
2458
2544
|
async getOrderRefunds(orderId) {
|
|
2459
|
-
|
|
2545
|
+
void orderId;
|
|
2546
|
+
throw new BrainerceError(
|
|
2547
|
+
"getOrderRefunds is not a route on the API-key /v1 surface. There is no orders/:id/refunds endpoint to call; view refunds in the Brainerce dashboard.",
|
|
2548
|
+
400
|
|
2549
|
+
);
|
|
2460
2550
|
}
|
|
2461
2551
|
/**
|
|
2462
|
-
* Create a refund for an order
|
|
2463
|
-
* Creates refund on the source platform (Shopify/WooCommerce only)
|
|
2552
|
+
* Create a refund for an order.
|
|
2464
2553
|
*
|
|
2465
|
-
*
|
|
2466
|
-
*
|
|
2467
|
-
* // Full refund
|
|
2468
|
-
* const refund = await client.createRefund('order_123', {
|
|
2469
|
-
* type: 'full',
|
|
2470
|
-
* restockInventory: true,
|
|
2471
|
-
* notifyCustomer: true,
|
|
2472
|
-
* reason: 'Customer request',
|
|
2473
|
-
* });
|
|
2474
|
-
*
|
|
2475
|
-
* // Partial refund
|
|
2476
|
-
* const partialRefund = await client.createRefund('order_123', {
|
|
2477
|
-
* type: 'partial',
|
|
2478
|
-
* items: [
|
|
2479
|
-
* { lineItemId: 'item_456', quantity: 1 },
|
|
2480
|
-
* ],
|
|
2481
|
-
* restockInventory: true,
|
|
2482
|
-
* });
|
|
2483
|
-
* ```
|
|
2554
|
+
* **Not callable.** The API-key `/v1` surface has no refunds route, so this
|
|
2555
|
+
* throws in every mode. Refund from the dashboard until the route ships.
|
|
2484
2556
|
*/
|
|
2485
2557
|
async createRefund(orderId, data) {
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2558
|
+
void orderId;
|
|
2559
|
+
void data;
|
|
2560
|
+
throw new BrainerceError(
|
|
2561
|
+
"createRefund is not a route on the API-key /v1 surface. There is no orders/:id/refunds endpoint to call, so no refund was issued; refund the order from the Brainerce dashboard.",
|
|
2562
|
+
400
|
|
2490
2563
|
);
|
|
2491
2564
|
}
|
|
2492
2565
|
/**
|
|
2493
|
-
* Update order shipping address
|
|
2494
|
-
* Syncs to source platform (Shopify/WooCommerce only)
|
|
2566
|
+
* Update order shipping address.
|
|
2495
2567
|
*
|
|
2496
|
-
*
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
* firstName: 'John',
|
|
2500
|
-
* lastName: 'Doe',
|
|
2501
|
-
* line1: '456 New Address',
|
|
2502
|
-
* city: 'Los Angeles',
|
|
2503
|
-
* state: 'CA',
|
|
2504
|
-
* country: 'US',
|
|
2505
|
-
* postalCode: '90001',
|
|
2506
|
-
* });
|
|
2507
|
-
* ```
|
|
2568
|
+
* **Not callable.** The API-key `/v1` surface has no order-shipping route,
|
|
2569
|
+
* so this throws in every mode. Correct the address from the dashboard
|
|
2570
|
+
* until the route ships.
|
|
2508
2571
|
*/
|
|
2509
2572
|
async updateOrderShipping(orderId, data) {
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2573
|
+
void orderId;
|
|
2574
|
+
void data;
|
|
2575
|
+
throw new BrainerceError(
|
|
2576
|
+
"updateOrderShipping is not a route on the API-key /v1 surface. There is no orders/:id/shipping endpoint to call, so the address was not changed; edit it in the Brainerce dashboard.",
|
|
2577
|
+
400
|
|
2514
2578
|
);
|
|
2515
2579
|
}
|
|
2516
2580
|
/**
|
|
@@ -2576,17 +2640,19 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2576
2640
|
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments`);
|
|
2577
2641
|
}
|
|
2578
2642
|
/**
|
|
2579
|
-
* Cancel an order
|
|
2580
|
-
* Works for Shopify and WooCommerce orders that haven't been fulfilled
|
|
2643
|
+
* Cancel an order.
|
|
2581
2644
|
*
|
|
2582
|
-
*
|
|
2583
|
-
*
|
|
2584
|
-
*
|
|
2585
|
-
*
|
|
2586
|
-
* ```
|
|
2645
|
+
* **Not callable.** The API-key `/v1` surface has no cancel route, so this
|
|
2646
|
+
* throws in every mode. A status move to cancelled may be reachable through
|
|
2647
|
+
* {@link updateOrder} depending on what the order's state machine allows;
|
|
2648
|
+
* otherwise cancel from the dashboard.
|
|
2587
2649
|
*/
|
|
2588
2650
|
async cancelOrder(orderId) {
|
|
2589
|
-
|
|
2651
|
+
void orderId;
|
|
2652
|
+
throw new BrainerceError(
|
|
2653
|
+
"cancelOrder is not a route on the API-key /v1 surface. There is no orders/:id/cancel endpoint to call, so the order was not cancelled. Try updateOrder(orderId, { status }) for a plain status move, or cancel from the Brainerce dashboard.",
|
|
2654
|
+
400
|
|
2655
|
+
);
|
|
2590
2656
|
}
|
|
2591
2657
|
/**
|
|
2592
2658
|
* Fulfill an order (mark as shipped), or correct the tracking of an order
|
|
@@ -2600,110 +2666,87 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2600
2666
|
* ship date is not rewritten, and no fulfilment event fires. That is the way
|
|
2601
2667
|
* to fix a mistyped tracking number.
|
|
2602
2668
|
*
|
|
2603
|
-
*
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2607
|
-
*
|
|
2608
|
-
* trackingCompany: 'UPS',
|
|
2609
|
-
* trackingUrl: 'https://www.ups.com/track?tracknum=1Z999AA10123456784',
|
|
2610
|
-
* notifyCustomer: true,
|
|
2611
|
-
* });
|
|
2612
|
-
*
|
|
2613
|
-
* // Correction — silent unless you opt back in.
|
|
2614
|
-
* await client.fulfillOrder('order_123', {
|
|
2615
|
-
* trackingNumber: '1Z999AA10123456785',
|
|
2616
|
-
* });
|
|
2617
|
-
* ```
|
|
2669
|
+
* **Not callable.** The API-key `/v1` surface has no fulfil route, so this
|
|
2670
|
+
* throws in every mode. To ship an order over the API today, buy a label
|
|
2671
|
+
* with {@link createShippingLabel} — the carrier's webhooks then move the
|
|
2672
|
+
* shipment through in-transit and delivered on their own. Otherwise fulfil
|
|
2673
|
+
* from the dashboard.
|
|
2618
2674
|
*/
|
|
2619
2675
|
async fulfillOrder(orderId, data) {
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2676
|
+
void orderId;
|
|
2677
|
+
void data;
|
|
2678
|
+
throw new BrainerceError(
|
|
2679
|
+
"fulfillOrder is not a route on the API-key /v1 surface. There is no orders/:id/fulfill endpoint to call, so nothing was fulfilled and no shipped email went out. Use createShippingLabel to ship over the API, or fulfil from the Brainerce dashboard.",
|
|
2680
|
+
400
|
|
2624
2681
|
);
|
|
2625
2682
|
}
|
|
2626
2683
|
/**
|
|
2627
|
-
* Sync draft orders from connected platforms
|
|
2684
|
+
* Sync draft orders from connected platforms.
|
|
2628
2685
|
*
|
|
2629
|
-
*
|
|
2630
|
-
*
|
|
2631
|
-
*
|
|
2632
|
-
* console.log('Draft orders synced');
|
|
2633
|
-
* ```
|
|
2686
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2687
|
+
* all, so this throws in every mode. {@link triggerSync} covers a general
|
|
2688
|
+
* platform sync; draft orders are managed from the dashboard.
|
|
2634
2689
|
*/
|
|
2635
2690
|
async syncDraftOrders() {
|
|
2636
|
-
|
|
2691
|
+
throw new BrainerceError(
|
|
2692
|
+
"syncDraftOrders is not a route on the API-key /v1 surface. There is no orders/sync-drafts endpoint to call, so nothing was synced. Use triggerSync for a platform sync, or work with drafts in the Brainerce dashboard.",
|
|
2693
|
+
400
|
|
2694
|
+
);
|
|
2637
2695
|
}
|
|
2638
2696
|
/**
|
|
2639
|
-
* Complete a draft order (convert to regular order)
|
|
2697
|
+
* Complete a draft order (convert to regular order).
|
|
2640
2698
|
*
|
|
2641
|
-
*
|
|
2642
|
-
*
|
|
2643
|
-
* const order = await client.completeDraftOrder('draft_123', {
|
|
2644
|
-
* paymentPending: false,
|
|
2645
|
-
* });
|
|
2646
|
-
* ```
|
|
2699
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2700
|
+
* all, so this throws in every mode. Complete drafts from the dashboard.
|
|
2647
2701
|
*/
|
|
2648
2702
|
async completeDraftOrder(orderId, data) {
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2703
|
+
void orderId;
|
|
2704
|
+
void data;
|
|
2705
|
+
throw new BrainerceError(
|
|
2706
|
+
"completeDraftOrder is not a route on the API-key /v1 surface. There is no orders/:id/complete-draft endpoint to call, so the draft was not converted; complete it in the Brainerce dashboard.",
|
|
2707
|
+
400
|
|
2653
2708
|
);
|
|
2654
2709
|
}
|
|
2655
2710
|
/**
|
|
2656
|
-
* Send invoice for a draft order
|
|
2711
|
+
* Send invoice for a draft order.
|
|
2657
2712
|
*
|
|
2658
|
-
*
|
|
2659
|
-
*
|
|
2660
|
-
* await client.sendDraftInvoice('draft_123', {
|
|
2661
|
-
* to: 'customer@example.com',
|
|
2662
|
-
* subject: 'Your Invoice',
|
|
2663
|
-
* customMessage: 'Thank you for your order!',
|
|
2664
|
-
* });
|
|
2665
|
-
* ```
|
|
2713
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2714
|
+
* all, so this throws in every mode. Send the invoice from the dashboard.
|
|
2666
2715
|
*/
|
|
2667
2716
|
async sendDraftInvoice(orderId, data) {
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2717
|
+
void orderId;
|
|
2718
|
+
void data;
|
|
2719
|
+
throw new BrainerceError(
|
|
2720
|
+
"sendDraftInvoice is not a route on the API-key /v1 surface. There is no orders/:id/send-invoice endpoint to call, so no invoice was sent; send it from the Brainerce dashboard.",
|
|
2721
|
+
400
|
|
2672
2722
|
);
|
|
2673
2723
|
}
|
|
2674
2724
|
/**
|
|
2675
|
-
* Delete a draft order
|
|
2725
|
+
* Delete a draft order.
|
|
2676
2726
|
*
|
|
2677
|
-
*
|
|
2678
|
-
*
|
|
2679
|
-
* await client.deleteDraftOrder('draft_123');
|
|
2680
|
-
* ```
|
|
2727
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2728
|
+
* all, so this throws in every mode. Delete drafts from the dashboard.
|
|
2681
2729
|
*/
|
|
2682
2730
|
async deleteDraftOrder(orderId) {
|
|
2683
|
-
|
|
2731
|
+
void orderId;
|
|
2732
|
+
throw new BrainerceError(
|
|
2733
|
+
"deleteDraftOrder is not a route on the API-key /v1 surface. There is no orders/:id/draft endpoint to call, so nothing was deleted; delete the draft in the Brainerce dashboard.",
|
|
2734
|
+
400
|
|
2735
|
+
);
|
|
2684
2736
|
}
|
|
2685
2737
|
/**
|
|
2686
|
-
* Update a draft order
|
|
2738
|
+
* Update a draft order.
|
|
2687
2739
|
*
|
|
2688
|
-
*
|
|
2689
|
-
*
|
|
2690
|
-
* const order = await client.updateDraftOrder('draft_123', {
|
|
2691
|
-
* note: 'Updated customer note',
|
|
2692
|
-
* email: 'newemail@example.com',
|
|
2693
|
-
* shippingAddress: {
|
|
2694
|
-
* firstName: 'John',
|
|
2695
|
-
* lastName: 'Doe',
|
|
2696
|
-
* address1: '123 Main St',
|
|
2697
|
-
* city: 'New York',
|
|
2698
|
-
* province: 'NY',
|
|
2699
|
-
* country: 'US',
|
|
2700
|
-
* zip: '10001',
|
|
2701
|
-
* },
|
|
2702
|
-
* });
|
|
2703
|
-
* ```
|
|
2740
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2741
|
+
* all, so this throws in every mode. Edit drafts from the dashboard.
|
|
2704
2742
|
*/
|
|
2705
2743
|
async updateDraftOrder(orderId, data) {
|
|
2706
|
-
|
|
2744
|
+
void orderId;
|
|
2745
|
+
void data;
|
|
2746
|
+
throw new BrainerceError(
|
|
2747
|
+
"updateDraftOrder is not a route on the API-key /v1 surface. There is no orders/:id/draft endpoint to call, so the draft was not changed; edit it in the Brainerce dashboard.",
|
|
2748
|
+
400
|
|
2749
|
+
);
|
|
2707
2750
|
}
|
|
2708
2751
|
// -------------------- Inventory --------------------
|
|
2709
2752
|
/**
|
|
@@ -2718,82 +2761,91 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2718
2761
|
);
|
|
2719
2762
|
}
|
|
2720
2763
|
/**
|
|
2721
|
-
* Get current inventory for a product
|
|
2764
|
+
* Get current inventory for a product.
|
|
2765
|
+
*
|
|
2766
|
+
* **Admin mode only** — the API key needs the `inventory:read` scope.
|
|
2767
|
+
*
|
|
2768
|
+
* This used to request `/api/v1/inventory/:productId`, which does not
|
|
2769
|
+
* exist and 404'd silently. The live route is product-scoped:
|
|
2770
|
+
* `GET /api/v1/products/:id/inventory`. A product with no inventory row
|
|
2771
|
+
* reads back as all zeroes rather than 404ing.
|
|
2722
2772
|
*/
|
|
2723
2773
|
async getInventory(productId) {
|
|
2724
|
-
return this.
|
|
2774
|
+
return this.adminRequest(
|
|
2775
|
+
"GET",
|
|
2776
|
+
`/api/v1/products/${encodePathSegment(productId)}/inventory`
|
|
2777
|
+
);
|
|
2725
2778
|
}
|
|
2726
2779
|
/**
|
|
2727
|
-
* Edit inventory manually with reason for audit trail
|
|
2780
|
+
* Edit inventory manually with a reason for the audit trail.
|
|
2728
2781
|
*
|
|
2729
|
-
*
|
|
2730
|
-
*
|
|
2731
|
-
*
|
|
2732
|
-
*
|
|
2733
|
-
*
|
|
2734
|
-
*
|
|
2735
|
-
*
|
|
2736
|
-
*
|
|
2782
|
+
* **Not callable.** The API-key `/v1` surface carries no `inventory`
|
|
2783
|
+
* namespace, so this throws in every mode.
|
|
2784
|
+
*
|
|
2785
|
+
* {@link updateInventory} is the closest working call: it sets the same
|
|
2786
|
+
* absolute stock level over `PUT /api/v1/products/:id/inventory`, but the
|
|
2787
|
+
* reason is not yours to choose — the server records a generic
|
|
2788
|
+
* "Updated via External API" against the audit trail. If the reason text
|
|
2789
|
+
* matters, make the edit from the dashboard.
|
|
2737
2790
|
*/
|
|
2738
2791
|
async editInventory(data) {
|
|
2739
|
-
|
|
2792
|
+
void data;
|
|
2793
|
+
throw new BrainerceError(
|
|
2794
|
+
"editInventory is not a route on the API-key /v1 surface. There is no inventory/edit endpoint to call, so stock was not changed. Use updateInventory(productId, { quantity }) to set the same level, though it records a generic audit reason. Edit from the dashboard when the reason text matters.",
|
|
2795
|
+
400
|
|
2796
|
+
);
|
|
2740
2797
|
}
|
|
2741
2798
|
/**
|
|
2742
|
-
* Get inventory sync status for all products in the store
|
|
2799
|
+
* Get inventory sync status for all products in the store.
|
|
2743
2800
|
*
|
|
2744
|
-
*
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2747
|
-
* console.log(`${status.pending} products pending sync`);
|
|
2748
|
-
* console.log(`Last sync: ${status.lastSyncAt}`);
|
|
2749
|
-
* ```
|
|
2801
|
+
* **Not callable.** The API-key `/v1` surface carries no `inventory`
|
|
2802
|
+
* namespace, so this throws in every mode. Sync state is visible in the
|
|
2803
|
+
* dashboard; {@link getSyncStatus} covers platform sync jobs.
|
|
2750
2804
|
*/
|
|
2751
2805
|
async getInventorySyncStatus() {
|
|
2752
|
-
|
|
2806
|
+
throw new BrainerceError(
|
|
2807
|
+
"getInventorySyncStatus is not a route on the API-key /v1 surface. There is no inventory/sync-status endpoint to call; check inventory sync state in the Brainerce dashboard.",
|
|
2808
|
+
400
|
|
2809
|
+
);
|
|
2753
2810
|
}
|
|
2754
2811
|
/**
|
|
2755
|
-
* Get inventory for multiple products at once
|
|
2812
|
+
* Get inventory for multiple products at once.
|
|
2756
2813
|
*
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
2761
|
-
* console.log(`${inv.productId}: ${inv.available} available`);
|
|
2762
|
-
* });
|
|
2763
|
-
* ```
|
|
2814
|
+
* **Not callable.** The API-key `/v1` surface carries no `inventory`
|
|
2815
|
+
* namespace, so this throws in every mode. There is no bulk stock read on
|
|
2816
|
+
* the API key today: fall back to {@link getInventory} per product, or read
|
|
2817
|
+
* the stock that {@link getProducts} already returns on each product.
|
|
2764
2818
|
*/
|
|
2765
2819
|
async getBulkInventory(productIds) {
|
|
2766
|
-
|
|
2820
|
+
void productIds;
|
|
2821
|
+
throw new BrainerceError(
|
|
2822
|
+
"getBulkInventory is not a route on the API-key /v1 surface. There is no inventory/bulk endpoint to call; read stock per product with getInventory, or off the products returned by getProducts.",
|
|
2823
|
+
400
|
|
2824
|
+
);
|
|
2767
2825
|
}
|
|
2768
2826
|
/**
|
|
2769
|
-
* Reconcile inventory between Brainerce and connected platforms
|
|
2770
|
-
* Detects and optionally fixes discrepancies
|
|
2771
|
-
*
|
|
2772
|
-
* @example
|
|
2773
|
-
* ```typescript
|
|
2774
|
-
* // Reconcile single product (dry run)
|
|
2775
|
-
* const result = await client.reconcileInventory({ productId: 'prod_123' });
|
|
2827
|
+
* Reconcile inventory between Brainerce and connected platforms.
|
|
2828
|
+
* Detects and optionally fixes discrepancies.
|
|
2776
2829
|
*
|
|
2777
|
-
*
|
|
2778
|
-
*
|
|
2779
|
-
*
|
|
2780
|
-
* ```
|
|
2830
|
+
* **Not callable.** The API-key `/v1` surface carries no `inventory`
|
|
2831
|
+
* namespace, so this throws in every mode, `autoFix` included. Reconcile
|
|
2832
|
+
* from the dashboard.
|
|
2781
2833
|
*/
|
|
2782
2834
|
async reconcileInventory(options) {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
"POST",
|
|
2788
|
-
"/api/v1/inventory/reconcile",
|
|
2789
|
-
void 0,
|
|
2790
|
-
queryParams
|
|
2835
|
+
void options;
|
|
2836
|
+
throw new BrainerceError(
|
|
2837
|
+
"reconcileInventory is not a route on the API-key /v1 surface. There is no inventory/reconcile endpoint to call, so nothing was reconciled or fixed; run reconciliation from the Brainerce dashboard.",
|
|
2838
|
+
400
|
|
2791
2839
|
);
|
|
2792
2840
|
}
|
|
2793
2841
|
/**
|
|
2794
2842
|
* Check stock availability for one or more items before adding to cart or checkout
|
|
2795
2843
|
* Use this to validate stock before operations that might fail due to insufficient inventory
|
|
2796
2844
|
*
|
|
2845
|
+
* **Vibe-coded or storefront mode only.** There is no stock-check route on
|
|
2846
|
+
* the API-key `/v1` surface; in admin mode this throws. The same applies to
|
|
2847
|
+
* {@link checkCartStock}, which routes through here.
|
|
2848
|
+
*
|
|
2797
2849
|
* @example
|
|
2798
2850
|
* ```typescript
|
|
2799
2851
|
* // Check if items are available before adding to cart
|
|
@@ -2825,9 +2877,10 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2825
2877
|
{ items }
|
|
2826
2878
|
);
|
|
2827
2879
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2880
|
+
throw new BrainerceError(
|
|
2881
|
+
"checkStockAvailability is only available in vibe-coded or storefront mode. The API-key /v1 surface has no inventory/check-availability route; read stock per product with getInventory instead.",
|
|
2882
|
+
400
|
|
2883
|
+
);
|
|
2831
2884
|
}
|
|
2832
2885
|
/**
|
|
2833
2886
|
* Check stock availability for cart items before checkout.
|
|
@@ -3134,6 +3187,11 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3134
3187
|
* Register a new customer with password (creates account)
|
|
3135
3188
|
* Works in vibe-coded, storefront, and admin mode
|
|
3136
3189
|
*
|
|
3190
|
+
* `birthMonth`/`birthDay` are optional and must be sent together. When
|
|
3191
|
+
* `getStoreInfo().requireBirthday` is true the merchant made the birthday
|
|
3192
|
+
* mandatory on that sales channel, and a call without both fields is
|
|
3193
|
+
* rejected with HTTP 400.
|
|
3194
|
+
*
|
|
3137
3195
|
* @example
|
|
3138
3196
|
* ```typescript
|
|
3139
3197
|
* const auth = await client.registerCustomer({
|
|
@@ -3141,6 +3199,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3141
3199
|
* password: 'securepassword123',
|
|
3142
3200
|
* firstName: 'Jane',
|
|
3143
3201
|
* lastName: 'Doe',
|
|
3202
|
+
* birthMonth: 4, // optional, unless the channel requires a birthday
|
|
3203
|
+
* birthDay: 17,
|
|
3144
3204
|
* });
|
|
3145
3205
|
* ```
|
|
3146
3206
|
*/
|
|
@@ -3157,49 +3217,43 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3157
3217
|
* Request a password reset email for a customer
|
|
3158
3218
|
* Works in vibe-coded, storefront, and admin mode
|
|
3159
3219
|
*
|
|
3160
|
-
* The
|
|
3161
|
-
*
|
|
3162
|
-
*
|
|
3163
|
-
*
|
|
3164
|
-
*
|
|
3165
|
-
*
|
|
3166
|
-
*
|
|
3220
|
+
* The reset link's host is chosen by the server, not by the caller: it is
|
|
3221
|
+
* derived from the sales channel's own domain, falling back to the backend's
|
|
3222
|
+
* configured frontend URL, and the request is rejected if neither resolves.
|
|
3223
|
+
*
|
|
3224
|
+
* The SDK used to send a `resetUrl` in the request body. The backend
|
|
3225
|
+
* deliberately removed that field: any caller could submit an arbitrary URL
|
|
3226
|
+
* and have it emailed, from a Brainerce-domained sender, to the address
|
|
3227
|
+
* holder — a phishing-link injection. `ForgotPasswordDto` now declares
|
|
3228
|
+
* `email` and nothing else, and the API's global validation pipe runs with
|
|
3229
|
+
* `whitelist` + `forbidNonWhitelisted`, so a body carrying `resetUrl` fails
|
|
3230
|
+
* the whole call with `400 property resetUrl should not exist`. Only `email`
|
|
3231
|
+
* is sent.
|
|
3232
|
+
*
|
|
3233
|
+
* The endpoint always answers 200 so it cannot be used to enumerate
|
|
3234
|
+
* accounts; the mail is only sent when a matching customer exists.
|
|
3167
3235
|
*
|
|
3168
3236
|
* @param email - Customer email address
|
|
3169
|
-
* @param options -
|
|
3170
|
-
* @param options.resetUrl - Reset URL the email links should point to.
|
|
3171
|
-
* Required outside the browser; recommended inside it.
|
|
3237
|
+
* @param options - Accepted for source compatibility only. Ignored.
|
|
3172
3238
|
*/
|
|
3173
3239
|
async forgotPassword(email, options) {
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
if (typeof window === "undefined") {
|
|
3177
|
-
throw new BrainerceError(
|
|
3178
|
-
'forgotPassword: `resetUrl` is required outside the browser. Pass `{ resetUrl: "https://your-site.example/reset-password" }` so the email links to the right host.',
|
|
3179
|
-
400
|
|
3180
|
-
);
|
|
3181
|
-
}
|
|
3182
|
-
console.warn(
|
|
3183
|
-
"BrainerceClient.forgotPassword: deriving `resetUrl` from `window.location.origin` \u2014 pass `{ resetUrl }` explicitly to avoid wrong-host links behind proxies or in SSR."
|
|
3184
|
-
);
|
|
3185
|
-
resetUrl = `${window.location.origin}/reset-password`;
|
|
3186
|
-
}
|
|
3240
|
+
void options;
|
|
3241
|
+
const body = { email };
|
|
3187
3242
|
if (this.isVibeCodedMode()) {
|
|
3188
|
-
return this.vibeCodedRequest("POST", "/customers/forgot-password",
|
|
3189
|
-
email,
|
|
3190
|
-
resetUrl
|
|
3191
|
-
});
|
|
3243
|
+
return this.vibeCodedRequest("POST", "/customers/forgot-password", body);
|
|
3192
3244
|
}
|
|
3193
3245
|
if (this.storeId && !this.apiKey) {
|
|
3194
|
-
return this.storefrontRequest(
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3246
|
+
return this.storefrontRequest(
|
|
3247
|
+
"POST",
|
|
3248
|
+
"/customers/forgot-password",
|
|
3249
|
+
body
|
|
3250
|
+
);
|
|
3198
3251
|
}
|
|
3199
|
-
return this.adminRequest(
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3252
|
+
return this.adminRequest(
|
|
3253
|
+
"POST",
|
|
3254
|
+
"/api/v1/customers/forgot-password",
|
|
3255
|
+
body
|
|
3256
|
+
);
|
|
3203
3257
|
}
|
|
3204
3258
|
/**
|
|
3205
3259
|
* Reset customer password using a reset token received via email
|
|
@@ -4419,16 +4473,27 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4419
4473
|
* List visible reviews for a product (storefront / sales-channel modes).
|
|
4420
4474
|
* Reviews that the merchant has hidden are excluded.
|
|
4421
4475
|
*
|
|
4476
|
+
* Each review carries `images` — the photos its author attached, already
|
|
4477
|
+
* filtered to the ones shoppers are allowed to see. Always an array.
|
|
4478
|
+
*
|
|
4479
|
+
* Ordering defaults to `photos_first`: reviews carrying photos lead, newest-first
|
|
4480
|
+
* within each group. Pass `sort: 'newest'` for plain chronological order. On a
|
|
4481
|
+
* store with no review photos the two are identical.
|
|
4482
|
+
*
|
|
4422
4483
|
* @example
|
|
4423
4484
|
* ```typescript
|
|
4424
4485
|
* const { data, meta } = await client.listProductReviews('prod_123', { page: 1, limit: 20 });
|
|
4425
|
-
* data.forEach(r =>
|
|
4486
|
+
* data.forEach(r => {
|
|
4487
|
+
* console.log(r.rating, r.body, r.verifiedPurchase);
|
|
4488
|
+
* r.images.forEach(img => console.log(img.thumbnailUrl ?? img.url));
|
|
4489
|
+
* });
|
|
4426
4490
|
* ```
|
|
4427
4491
|
*/
|
|
4428
4492
|
async listProductReviews(productId, params) {
|
|
4429
4493
|
const queryParams = {};
|
|
4430
4494
|
if (params?.page) queryParams.page = params.page;
|
|
4431
4495
|
if (params?.limit) queryParams.limit = params.limit;
|
|
4496
|
+
if (params?.sort) queryParams.sort = params.sort;
|
|
4432
4497
|
if (this.isVibeCodedMode()) {
|
|
4433
4498
|
return this.vibeCodedRequest(
|
|
4434
4499
|
"GET",
|
|
@@ -4586,6 +4651,37 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4586
4651
|
storeId ? { storeId } : void 0
|
|
4587
4652
|
);
|
|
4588
4653
|
}
|
|
4654
|
+
/**
|
|
4655
|
+
* Admin: hide ONE photo on a review, leaving the review and its other photos
|
|
4656
|
+
* visible. Requires an API key with `reviews:write`.
|
|
4657
|
+
*/
|
|
4658
|
+
async hideProductReviewImage(imageId, storeId) {
|
|
4659
|
+
if (!this.apiKey) {
|
|
4660
|
+
throw new BrainerceError("hideProductReviewImage() requires admin (API key) mode", 400);
|
|
4661
|
+
}
|
|
4662
|
+
return this.adminRequest(
|
|
4663
|
+
"PATCH",
|
|
4664
|
+
`/api/v1/review-images/${encodePathSegment(imageId)}/hide`,
|
|
4665
|
+
void 0,
|
|
4666
|
+
storeId ? { storeId } : void 0
|
|
4667
|
+
);
|
|
4668
|
+
}
|
|
4669
|
+
/**
|
|
4670
|
+
* Admin: show one review photo. This is also the approve action — a photo that
|
|
4671
|
+
* has never been approved carries no `approvedAt`, and showing it stamps one, so
|
|
4672
|
+
* stores using review-photo approval need no separate verb.
|
|
4673
|
+
*/
|
|
4674
|
+
async showProductReviewImage(imageId, storeId) {
|
|
4675
|
+
if (!this.apiKey) {
|
|
4676
|
+
throw new BrainerceError("showProductReviewImage() requires admin (API key) mode", 400);
|
|
4677
|
+
}
|
|
4678
|
+
return this.adminRequest(
|
|
4679
|
+
"PATCH",
|
|
4680
|
+
`/api/v1/review-images/${encodePathSegment(imageId)}/show`,
|
|
4681
|
+
void 0,
|
|
4682
|
+
storeId ? { storeId } : void 0
|
|
4683
|
+
);
|
|
4684
|
+
}
|
|
4589
4685
|
/** Admin: unhide a previously hidden review. */
|
|
4590
4686
|
async showProductReview(reviewId, storeId) {
|
|
4591
4687
|
if (!this.apiKey) {
|
|
@@ -7241,11 +7337,20 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
7241
7337
|
);
|
|
7242
7338
|
}
|
|
7243
7339
|
/**
|
|
7244
|
-
* Update the current customer's profile (requires customerToken)
|
|
7245
|
-
* Only available in storefront mode
|
|
7340
|
+
* Update the current customer's profile (requires customerToken).
|
|
7341
|
+
* Only available in storefront and vibe-coded mode.
|
|
7246
7342
|
*
|
|
7247
|
-
* `birthMonth`/`birthDay` (1-12 / 1-31
|
|
7248
|
-
*
|
|
7343
|
+
* `birthMonth`/`birthDay` (1-12 / 1-31) power the loyalty birthday gift.
|
|
7344
|
+
* Month and day only, never a year, for privacy. Send both or neither, and
|
|
7345
|
+
* the day has to exist in the month: anything else is rejected with HTTP 400.
|
|
7346
|
+
* Send `null` for both to REMOVE a stored birthday. Leaving the keys out is a
|
|
7347
|
+
* different request and keeps the stored value, so a profile form whose
|
|
7348
|
+
* fields the shopper cleared has to send nulls, not omit them.
|
|
7349
|
+
*
|
|
7350
|
+
* The saved values now come back on the returned `CustomerProfile`, on
|
|
7351
|
+
* `getMyProfile()` and on every customer read type, so a profile form can
|
|
7352
|
+
* re-render the birthday it just saved. Before this, no API returned the two
|
|
7353
|
+
* fields at all and the form always came back blank.
|
|
7249
7354
|
*/
|
|
7250
7355
|
async updateMyProfile(data) {
|
|
7251
7356
|
if (!this.customerToken && !this.proxyMode) {
|
|
@@ -9422,6 +9527,68 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9422
9527
|
}
|
|
9423
9528
|
throw new Error("uploadCustomizationFile requires storefront or vibe-coded mode");
|
|
9424
9529
|
}
|
|
9530
|
+
/**
|
|
9531
|
+
* Upload one photo to attach to a product review.
|
|
9532
|
+
*
|
|
9533
|
+
* Available in storefront and vibe-coded modes, and — unlike
|
|
9534
|
+
* `uploadCustomizationFile` — it REQUIRES a logged-in customer who has actually
|
|
9535
|
+
* bought the product. Call `setCustomerToken(...)` first. That is the same bar as
|
|
9536
|
+
* writing the review itself, checked here so an ineligible shopper is told before
|
|
9537
|
+
* they wait for the upload rather than after.
|
|
9538
|
+
*
|
|
9539
|
+
* Returns a storage `key`. Collect the keys and pass them as `imageKeys` when you
|
|
9540
|
+
* submit or update the review — the `url` is for a local preview only, and sending
|
|
9541
|
+
* it back instead of the key will be rejected.
|
|
9542
|
+
*
|
|
9543
|
+
* Server rules:
|
|
9544
|
+
* - `image/jpeg|png|webp|gif` only, cross-checked against the file's real bytes.
|
|
9545
|
+
* - Max 5 MB and 40 megapixels per file.
|
|
9546
|
+
* - Throttled to 10 uploads / minute → HTTP 429.
|
|
9547
|
+
* - 403 when the store has review photos turned off, or the customer has not
|
|
9548
|
+
* bought the product; 400 once the review is already at its photo cap.
|
|
9549
|
+
* - EXIF is stripped (so GPS coordinates never reach the storefront) while the
|
|
9550
|
+
* orientation tag is applied first, so phone photos stay upright.
|
|
9551
|
+
* - A photo uploaded but never attached to a submitted review is reclaimed after
|
|
9552
|
+
* 7 days.
|
|
9553
|
+
*
|
|
9554
|
+
* Read `photos` from `getMyProductReview()` for the store's live limits rather
|
|
9555
|
+
* than hard-coding them.
|
|
9556
|
+
*
|
|
9557
|
+
* @example
|
|
9558
|
+
* ```ts
|
|
9559
|
+
* const { photos } = await client.getMyProductReview(productId);
|
|
9560
|
+
* if (photos.enabled) {
|
|
9561
|
+
* const uploads = await Promise.all(
|
|
9562
|
+
* [...fileInput.files].slice(0, photos.maxPerReview)
|
|
9563
|
+
* .map(f => client.uploadReviewPhoto(productId, f))
|
|
9564
|
+
* );
|
|
9565
|
+
* await client.submitProductReview(productId, {
|
|
9566
|
+
* rating: 5,
|
|
9567
|
+
* body: 'Arrived beautifully wrapped.',
|
|
9568
|
+
* imageKeys: uploads.map(u => u.key),
|
|
9569
|
+
* });
|
|
9570
|
+
* }
|
|
9571
|
+
* ```
|
|
9572
|
+
*/
|
|
9573
|
+
async uploadReviewPhoto(productId, file) {
|
|
9574
|
+
const formData = new FormData();
|
|
9575
|
+
formData.append("file", file);
|
|
9576
|
+
if (this.isVibeCodedMode()) {
|
|
9577
|
+
return this.vibeCodedRequest(
|
|
9578
|
+
"POST",
|
|
9579
|
+
`/products/${encodePathSegment(productId)}/review-photo`,
|
|
9580
|
+
formData
|
|
9581
|
+
);
|
|
9582
|
+
}
|
|
9583
|
+
if (this.storeId && !this.apiKey) {
|
|
9584
|
+
return this.storefrontRequest(
|
|
9585
|
+
"POST",
|
|
9586
|
+
`/products/${encodePathSegment(productId)}/review-photo`,
|
|
9587
|
+
formData
|
|
9588
|
+
);
|
|
9589
|
+
}
|
|
9590
|
+
throw new Error("uploadReviewPhoto requires storefront or vibe-coded mode");
|
|
9591
|
+
}
|
|
9425
9592
|
// -------------------- Team Management (Admin) - DEPRECATED --------------------
|
|
9426
9593
|
// Account-level team methods. These are the ONLY team endpoints reachable with
|
|
9427
9594
|
// a `brainerce_*` API key. The store-level equivalents (`getStoreTeam`,
|
|
@@ -10109,7 +10276,7 @@ var DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\
|
|
|
10109
10276
|
var MIN_OFFSET_MINUTES = -12 * 60;
|
|
10110
10277
|
var MAX_OFFSET_MINUTES = 14 * 60;
|
|
10111
10278
|
var VALID_OFFSET_MINUTE_PARTS = [0, 30, 45];
|
|
10112
|
-
function validateDateAvailabilityConfig(config, fieldType) {
|
|
10279
|
+
function validateDateAvailabilityConfig(config, fieldType, surface = "checkout") {
|
|
10113
10280
|
const errors = [];
|
|
10114
10281
|
if (!config) return errors;
|
|
10115
10282
|
if (config.minDate !== void 0 && !DATE_RE.test(config.minDate)) {
|
|
@@ -10121,6 +10288,29 @@ function validateDateAvailabilityConfig(config, fieldType) {
|
|
|
10121
10288
|
if (config.minDate && config.maxDate && DATE_RE.test(config.minDate) && DATE_RE.test(config.maxDate) && config.minDate > config.maxDate) {
|
|
10122
10289
|
errors.push("minDate must be on or before maxDate");
|
|
10123
10290
|
}
|
|
10291
|
+
if (config.leadTimeMinutes !== void 0) {
|
|
10292
|
+
if (!Number.isInteger(config.leadTimeMinutes) || config.leadTimeMinutes < 0) {
|
|
10293
|
+
errors.push("leadTimeMinutes must be a non-negative integer");
|
|
10294
|
+
}
|
|
10295
|
+
}
|
|
10296
|
+
if (config.maxDaysAhead !== void 0) {
|
|
10297
|
+
if (!Number.isInteger(config.maxDaysAhead) || config.maxDaysAhead < 1) {
|
|
10298
|
+
errors.push("maxDaysAhead must be a positive integer");
|
|
10299
|
+
}
|
|
10300
|
+
}
|
|
10301
|
+
if (config.cutoffTime !== void 0 && !TIME_RE.test(config.cutoffTime)) {
|
|
10302
|
+
errors.push("cutoffTime must be in HH:mm format");
|
|
10303
|
+
}
|
|
10304
|
+
if (surface !== "checkout") {
|
|
10305
|
+
const relativeKeys = ["leadTimeMinutes", "cutoffTime", "maxDaysAhead"].filter(
|
|
10306
|
+
(k) => config[k] !== void 0
|
|
10307
|
+
);
|
|
10308
|
+
if (relativeKeys.length > 0) {
|
|
10309
|
+
errors.push(
|
|
10310
|
+
`${relativeKeys.join("/")} only apply to checkout fields, not ${surface} fields`
|
|
10311
|
+
);
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10124
10314
|
if (config.blockedWeekdays) {
|
|
10125
10315
|
for (const d of config.blockedWeekdays) {
|
|
10126
10316
|
if (!Number.isInteger(d) || d < 0 || d > 6) {
|
|
@@ -10138,22 +10328,37 @@ function validateDateAvailabilityConfig(config, fieldType) {
|
|
|
10138
10328
|
}
|
|
10139
10329
|
}
|
|
10140
10330
|
if (config.businessHours) {
|
|
10141
|
-
const
|
|
10331
|
+
const windowsByWeekday = /* @__PURE__ */ new Map();
|
|
10142
10332
|
for (const w of config.businessHours) {
|
|
10143
10333
|
if (!Number.isInteger(w.weekday) || w.weekday < 0 || w.weekday > 6) {
|
|
10144
10334
|
errors.push(`businessHours.weekday must be an integer 0-6, got ${JSON.stringify(w.weekday)}`);
|
|
10145
10335
|
continue;
|
|
10146
10336
|
}
|
|
10147
|
-
if (seenWeekdays.has(w.weekday)) {
|
|
10148
|
-
errors.push(`businessHours has more than one window for weekday ${w.weekday}`);
|
|
10149
|
-
}
|
|
10150
|
-
seenWeekdays.add(w.weekday);
|
|
10151
10337
|
const openValid = TIME_RE.test(w.open);
|
|
10152
10338
|
const closeValid = TIME_RE.test(w.close);
|
|
10153
10339
|
if (!openValid) errors.push(`businessHours.open must be HH:mm, got "${w.open}"`);
|
|
10154
10340
|
if (!closeValid) errors.push(`businessHours.close must be HH:mm, got "${w.close}"`);
|
|
10155
|
-
if (openValid
|
|
10341
|
+
if (!openValid || !closeValid) continue;
|
|
10342
|
+
if (w.open >= w.close) {
|
|
10156
10343
|
errors.push(`businessHours for weekday ${w.weekday}: open must be before close`);
|
|
10344
|
+
continue;
|
|
10345
|
+
}
|
|
10346
|
+
const forDay = windowsByWeekday.get(w.weekday);
|
|
10347
|
+
if (forDay) forDay.push(w);
|
|
10348
|
+
else windowsByWeekday.set(w.weekday, [w]);
|
|
10349
|
+
}
|
|
10350
|
+
const weekdaysWithWindows = Array.from(windowsByWeekday.keys()).sort((a, b) => a - b);
|
|
10351
|
+
for (const weekday of weekdaysWithWindows) {
|
|
10352
|
+
const sorted = [...windowsByWeekday.get(weekday) ?? []].sort(
|
|
10353
|
+
(a, b) => a.open < b.open ? -1 : a.open > b.open ? 1 : 0
|
|
10354
|
+
);
|
|
10355
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
10356
|
+
if (sorted[i].open < sorted[i - 1].close) {
|
|
10357
|
+
errors.push(
|
|
10358
|
+
`businessHours for weekday ${weekday}: ${sorted[i - 1].open}-${sorted[i - 1].close} overlaps ${sorted[i].open}-${sorted[i].close}`
|
|
10359
|
+
);
|
|
10360
|
+
break;
|
|
10361
|
+
}
|
|
10157
10362
|
}
|
|
10158
10363
|
}
|
|
10159
10364
|
}
|
|
@@ -10209,6 +10414,32 @@ function resolveStoreLocalParts(instant, timezone) {
|
|
|
10209
10414
|
weekday: WEEKDAY_TO_INDEX[get("weekday")] ?? instant.getUTCDay()
|
|
10210
10415
|
};
|
|
10211
10416
|
}
|
|
10417
|
+
function resolveRelativeBounds(config, clock) {
|
|
10418
|
+
if (!config || !clock) return {};
|
|
10419
|
+
const leadMinutes = typeof config.leadTimeMinutes === "number" && Number.isFinite(config.leadTimeMinutes) ? Math.max(0, Math.floor(config.leadTimeMinutes)) : 0;
|
|
10420
|
+
const cutoff = typeof config.cutoffTime === "string" && TIME_RE.test(config.cutoffTime) ? config.cutoffTime : null;
|
|
10421
|
+
const daysAhead = typeof config.maxDaysAhead === "number" && Number.isFinite(config.maxDaysAhead) && config.maxDaysAhead >= 1 ? Math.floor(config.maxDaysAhead) : null;
|
|
10422
|
+
if (leadMinutes === 0 && !cutoff && daysAhead === null) return {};
|
|
10423
|
+
const now = clock.now ?? /* @__PURE__ */ new Date();
|
|
10424
|
+
const nowLocal = resolveStoreLocalParts(now, clock.timezone);
|
|
10425
|
+
const bounds = {};
|
|
10426
|
+
if (leadMinutes > 0 || cutoff) {
|
|
10427
|
+
const earliestInstant = new Date(now.getTime() + leadMinutes * 6e4);
|
|
10428
|
+
let earliestDate = resolveStoreLocalParts(earliestInstant, clock.timezone).dateYYYYMMDD;
|
|
10429
|
+
if (cutoff && nowLocal.hhmm >= cutoff) earliestDate = addCalendarDays(earliestDate, 1);
|
|
10430
|
+
bounds.earliestDate = earliestDate;
|
|
10431
|
+
if (leadMinutes > 0) bounds.earliestInstant = earliestInstant;
|
|
10432
|
+
}
|
|
10433
|
+
if (daysAhead !== null) {
|
|
10434
|
+
bounds.latestDate = addCalendarDays(nowLocal.dateYYYYMMDD, daysAhead);
|
|
10435
|
+
}
|
|
10436
|
+
return bounds;
|
|
10437
|
+
}
|
|
10438
|
+
function addCalendarDays(dateYYYYMMDD, days) {
|
|
10439
|
+
const [year, month, day] = dateYYYYMMDD.split("-").map(Number);
|
|
10440
|
+
const shifted = new Date(Date.UTC(year, month - 1, day + days));
|
|
10441
|
+
return `${shifted.getUTCFullYear()}-${pad2(shifted.getUTCMonth() + 1)}-${pad2(shifted.getUTCDate())}`;
|
|
10442
|
+
}
|
|
10212
10443
|
function parseDateFieldValue(raw, fieldType, timezone) {
|
|
10213
10444
|
const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
|
|
10214
10445
|
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";
|
|
@@ -10304,19 +10535,30 @@ function timezoneOffsetMs(utcMillis, timezone) {
|
|
|
10304
10535
|
const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
|
|
10305
10536
|
return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
|
|
10306
10537
|
}
|
|
10307
|
-
function isCalendarDateAllowed(dateYYYYMMDD, config) {
|
|
10538
|
+
function isCalendarDateAllowed(dateYYYYMMDD, config, clock) {
|
|
10308
10539
|
if (!config) return true;
|
|
10309
10540
|
if (config.minDate && dateYYYYMMDD < config.minDate) return false;
|
|
10310
10541
|
if (config.maxDate && dateYYYYMMDD > config.maxDate) return false;
|
|
10542
|
+
if (relativeBoundFailure(dateYYYYMMDD, config, clock)) return false;
|
|
10311
10543
|
if (config.blockedDates?.includes(dateYYYYMMDD)) return false;
|
|
10312
10544
|
if (config.blockedWeekdays?.length) {
|
|
10313
10545
|
if (config.blockedWeekdays.includes(weekdayOfDateString(dateYYYYMMDD))) return false;
|
|
10314
10546
|
}
|
|
10315
10547
|
return true;
|
|
10316
10548
|
}
|
|
10317
|
-
function
|
|
10549
|
+
function relativeBoundFailure(dateYYYYMMDD, config, clock) {
|
|
10550
|
+
const bounds = resolveRelativeBounds(config, clock);
|
|
10551
|
+
if (bounds.earliestDate && dateYYYYMMDD < bounds.earliestDate) {
|
|
10552
|
+
return `the earliest available date is ${bounds.earliestDate}`;
|
|
10553
|
+
}
|
|
10554
|
+
if (bounds.latestDate && dateYYYYMMDD > bounds.latestDate) {
|
|
10555
|
+
return `the latest available date is ${bounds.latestDate}`;
|
|
10556
|
+
}
|
|
10557
|
+
return null;
|
|
10558
|
+
}
|
|
10559
|
+
function computeAvailableSlots(config, dateYYYYMMDD, clock) {
|
|
10318
10560
|
if (!config) return [];
|
|
10319
|
-
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
10561
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) return [];
|
|
10320
10562
|
if (!config.businessHours?.length || !config.slotDurationMinutes) return [];
|
|
10321
10563
|
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
10322
10564
|
const windows = config.businessHours.filter((w) => w.weekday === weekday);
|
|
@@ -10329,27 +10571,47 @@ function computeAvailableSlots(config, dateYYYYMMDD) {
|
|
|
10329
10571
|
slots.push(minutesToHHMM(t));
|
|
10330
10572
|
}
|
|
10331
10573
|
}
|
|
10332
|
-
|
|
10574
|
+
const ordered = Array.from(new Set(slots)).sort();
|
|
10575
|
+
const { earliestInstant } = resolveRelativeBounds(config, clock);
|
|
10576
|
+
if (!earliestInstant || !clock) return ordered;
|
|
10577
|
+
return ordered.filter((hhmm) => {
|
|
10578
|
+
const [hour, minute] = hhmm.split(":").map(Number);
|
|
10579
|
+
const start = instantFromStoreLocal(dateYYYYMMDD, hour, minute, 0, 0, clock.timezone);
|
|
10580
|
+
return start.getTime() >= earliestInstant.getTime();
|
|
10581
|
+
});
|
|
10333
10582
|
}
|
|
10334
|
-
function getBusinessHoursForDate(config, dateYYYYMMDD) {
|
|
10583
|
+
function getBusinessHoursForDate(config, dateYYYYMMDD, clock) {
|
|
10335
10584
|
if (!config?.businessHours?.length) return [];
|
|
10336
|
-
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
10585
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) return [];
|
|
10337
10586
|
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
10338
10587
|
return config.businessHours.filter((w) => w.weekday === weekday);
|
|
10339
10588
|
}
|
|
10340
|
-
function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
10589
|
+
function isDateValueAllowed(instant, config, fieldType, timezone, now) {
|
|
10341
10590
|
if (!config) return { allowed: true };
|
|
10591
|
+
const clock = { timezone, now };
|
|
10342
10592
|
if (fieldType === "DATE") {
|
|
10343
10593
|
const dateYYYYMMDD = `${instant.getUTCFullYear()}-${pad2(instant.getUTCMonth() + 1)}-${pad2(instant.getUTCDate())}`;
|
|
10344
|
-
|
|
10594
|
+
const relative2 = relativeBoundFailure(dateYYYYMMDD, config, clock);
|
|
10595
|
+
if (relative2) return { allowed: false, reason: relative2 };
|
|
10596
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) {
|
|
10345
10597
|
return { allowed: false, reason: "date is outside the allowed range" };
|
|
10346
10598
|
}
|
|
10347
10599
|
return { allowed: true };
|
|
10348
10600
|
}
|
|
10349
10601
|
const local = resolveStoreLocalParts(instant, timezone);
|
|
10350
|
-
|
|
10602
|
+
const relative = relativeBoundFailure(local.dateYYYYMMDD, config, clock);
|
|
10603
|
+
if (relative) return { allowed: false, reason: relative };
|
|
10604
|
+
if (!isCalendarDateAllowed(local.dateYYYYMMDD, config, clock)) {
|
|
10351
10605
|
return { allowed: false, reason: "date is outside the allowed range" };
|
|
10352
10606
|
}
|
|
10607
|
+
const { earliestInstant } = resolveRelativeBounds(config, clock);
|
|
10608
|
+
if (earliestInstant && instant.getTime() < earliestInstant.getTime()) {
|
|
10609
|
+
const earliestLocal = resolveStoreLocalParts(earliestInstant, timezone);
|
|
10610
|
+
return {
|
|
10611
|
+
allowed: false,
|
|
10612
|
+
reason: `the earliest available time is ${earliestLocal.dateYYYYMMDD} ${earliestLocal.hhmm}`
|
|
10613
|
+
};
|
|
10614
|
+
}
|
|
10353
10615
|
if (!config.businessHours?.length) {
|
|
10354
10616
|
return { allowed: true };
|
|
10355
10617
|
}
|
|
@@ -10358,7 +10620,7 @@ function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
|
10358
10620
|
return { allowed: false, reason: "no business hours are configured for this day" };
|
|
10359
10621
|
}
|
|
10360
10622
|
if (config.slotDurationMinutes) {
|
|
10361
|
-
const slots = computeAvailableSlots(config, local.dateYYYYMMDD);
|
|
10623
|
+
const slots = computeAvailableSlots(config, local.dateYYYYMMDD, clock);
|
|
10362
10624
|
if (!slots.includes(local.hhmm)) {
|
|
10363
10625
|
return { allowed: false, reason: "time does not match an available slot" };
|
|
10364
10626
|
}
|
|
@@ -10989,6 +11251,7 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
10989
11251
|
jsonLdScriptProps,
|
|
10990
11252
|
parseDateFieldValue,
|
|
10991
11253
|
parseWebhookEvent,
|
|
11254
|
+
resolveRelativeBounds,
|
|
10992
11255
|
resolveStoreLocalParts,
|
|
10993
11256
|
safePaymentRedirect,
|
|
10994
11257
|
stripHtml,
|