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.mjs
CHANGED
|
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// src/version.ts
|
|
118
|
-
var SDK_VERSION = "
|
|
118
|
+
var SDK_VERSION = "2.0.0";
|
|
119
119
|
|
|
120
120
|
// src/client.ts
|
|
121
121
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -272,6 +272,87 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
272
272
|
);
|
|
273
273
|
}
|
|
274
274
|
};
|
|
275
|
+
// -------------------- Marketing signup (newsletter) --------------------
|
|
276
|
+
/**
|
|
277
|
+
* Email marketing signup for a storefront — a newsletter popup, a footer
|
|
278
|
+
* capture bar, an exit-intent modal.
|
|
279
|
+
*
|
|
280
|
+
* **Confirmed opt-in, always.** `subscribe()` creates the contact and mails
|
|
281
|
+
* them a confirmation link. The address is NOT subscribed and CANNOT receive
|
|
282
|
+
* a campaign until the recipient clicks that link. This is not a setting:
|
|
283
|
+
* consent has to come from the mailbox, or anyone could subscribe anyone.
|
|
284
|
+
*
|
|
285
|
+
* So do not render "You're subscribed!" on success — render "Check your
|
|
286
|
+
* email to confirm." The one is a lie until the click lands.
|
|
287
|
+
*
|
|
288
|
+
* The response is identical for a brand-new address, one that is already
|
|
289
|
+
* subscribed, and one suppressed after a bounce, so the form can't be used to
|
|
290
|
+
* probe who shops here. Show the same message for every success.
|
|
291
|
+
*
|
|
292
|
+
* Storefront (public) and vibe-coded modes only. Rate-limited server-side to
|
|
293
|
+
* 3 requests / 60s per IP, plus one confirmation email per address per 24h.
|
|
294
|
+
*
|
|
295
|
+
* **Where the discount goes.** A "10% off your first order" popup needs a
|
|
296
|
+
* coupon from the dashboard — create one with the `customer_first_order`
|
|
297
|
+
* condition and show the code after a successful call. Subscribing does not
|
|
298
|
+
* mint a code on its own.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* // Newsletter popup — hidden honeypot input, Hebrew storefront
|
|
303
|
+
* await brainerce.marketing.subscribe({
|
|
304
|
+
* email: 'jane@example.com',
|
|
305
|
+
* locale: 'he',
|
|
306
|
+
* source: 'popup',
|
|
307
|
+
* honeypot: hiddenFieldValue,
|
|
308
|
+
* });
|
|
309
|
+
* // → show "בדקו את המייל שלכם כדי לאשר" — NOT "נרשמת בהצלחה"
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
this.marketing = {
|
|
313
|
+
subscribe: async (input) => {
|
|
314
|
+
if (this.isVibeCodedMode()) {
|
|
315
|
+
return this.vibeCodedRequest(
|
|
316
|
+
"POST",
|
|
317
|
+
"/marketing/subscribe",
|
|
318
|
+
input
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return this.storefrontRequest(
|
|
322
|
+
"POST",
|
|
323
|
+
"/marketing/subscribe",
|
|
324
|
+
input
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
// -------------------- Stock alerts --------------------
|
|
329
|
+
/**
|
|
330
|
+
* "Email me when this is back."
|
|
331
|
+
*
|
|
332
|
+
* ⛔ Not a newsletter signup, and must not be worded as one. It grants no
|
|
333
|
+
* marketing consent, creates no customer account, and the person is never
|
|
334
|
+
* mailed anything else as a result — exactly one message, about this item,
|
|
335
|
+
* with a link that stops it. Someone who unsubscribed from marketing can
|
|
336
|
+
* still use this, so do not gate it on consent.
|
|
337
|
+
*
|
|
338
|
+
* Show the affordance only on an item that is out of stock AND cannot be
|
|
339
|
+
* backordered. Every other case is silently ignored server-side — the
|
|
340
|
+
* response is uniform on purpose, so it cannot be used to read stock levels
|
|
341
|
+
* or test who is a customer — which means a button on an in-stock item looks
|
|
342
|
+
* like it worked and does nothing.
|
|
343
|
+
*
|
|
344
|
+
* Pass `variantId` on any product with variants. Without it the alert waits
|
|
345
|
+
* on the product as a whole, and a shopper who wanted the medium hears when
|
|
346
|
+
* the small comes back.
|
|
347
|
+
*/
|
|
348
|
+
this.stockAlerts = {
|
|
349
|
+
subscribe: async (input) => {
|
|
350
|
+
if (this.isVibeCodedMode()) {
|
|
351
|
+
return this.vibeCodedRequest("POST", "/stock-alerts", input);
|
|
352
|
+
}
|
|
353
|
+
return this.storefrontRequest("POST", "/stock-alerts", input);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
275
356
|
// -------------------- Content (typed merchant content) --------------------
|
|
276
357
|
/**
|
|
277
358
|
* Typed merchant content store: FAQ, Footer, Header, Announcement,
|
|
@@ -480,7 +561,7 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
480
561
|
*/
|
|
481
562
|
this.blog = /* @__PURE__ */ (() => {
|
|
482
563
|
const publicBase = "/blog/posts";
|
|
483
|
-
const adminBase = "/blog/posts";
|
|
564
|
+
const adminBase = "/api/blog/posts";
|
|
484
565
|
const requireAdmin = (action) => {
|
|
485
566
|
if (this.isVibeCodedMode() || this.storeId && !this.apiKey) {
|
|
486
567
|
throw new BrainerceError(
|
|
@@ -2314,115 +2395,97 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2314
2395
|
return this.request("PATCH", `/api/v1/orders/${encodePathSegment(orderId)}`, data);
|
|
2315
2396
|
}
|
|
2316
2397
|
/**
|
|
2317
|
-
* Update order status
|
|
2398
|
+
* Update order status.
|
|
2399
|
+
*
|
|
2400
|
+
* **Not callable — use {@link updateOrder} instead.** Status changes do work
|
|
2401
|
+
* over the API key, just by a different route.
|
|
2402
|
+
*
|
|
2403
|
+
* @deprecated Call `updateOrder(orderId, { status })`.
|
|
2318
2404
|
*
|
|
2319
2405
|
* @example
|
|
2320
2406
|
* ```typescript
|
|
2321
|
-
* const order = await client.
|
|
2407
|
+
* const order = await client.updateOrder('order_123', { status: 'SHIPPED' });
|
|
2322
2408
|
* ```
|
|
2323
2409
|
*/
|
|
2324
2410
|
async updateOrderStatus(orderId, status) {
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2411
|
+
void orderId;
|
|
2412
|
+
void status;
|
|
2413
|
+
throw new BrainerceError(
|
|
2414
|
+
"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.",
|
|
2415
|
+
400
|
|
2416
|
+
);
|
|
2328
2417
|
}
|
|
2329
2418
|
/**
|
|
2330
|
-
* Update order payment method
|
|
2331
|
-
* Note: Only WooCommerce supports syncing payment method changes back to platform
|
|
2419
|
+
* Update order payment method.
|
|
2332
2420
|
*
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
*
|
|
2336
|
-
* ```
|
|
2421
|
+
* **Not callable.** The API-key `/v1` surface has no payment-method route,
|
|
2422
|
+
* so this throws in every mode. Change the payment method from the
|
|
2423
|
+
* dashboard until the route ships.
|
|
2337
2424
|
*/
|
|
2338
2425
|
async updatePaymentMethod(orderId, paymentMethod) {
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
}
|
|
2426
|
+
void orderId;
|
|
2427
|
+
void paymentMethod;
|
|
2428
|
+
throw new BrainerceError(
|
|
2429
|
+
"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.",
|
|
2430
|
+
400
|
|
2345
2431
|
);
|
|
2346
2432
|
}
|
|
2347
2433
|
/**
|
|
2348
|
-
* Update order notes
|
|
2434
|
+
* Update order notes.
|
|
2349
2435
|
*
|
|
2350
|
-
*
|
|
2351
|
-
*
|
|
2352
|
-
*
|
|
2353
|
-
* ```
|
|
2436
|
+
* **Not callable.** The API-key `/v1` surface has no order-notes route, so
|
|
2437
|
+
* this throws in every mode. Edit notes from the dashboard until the route
|
|
2438
|
+
* ships.
|
|
2354
2439
|
*/
|
|
2355
2440
|
async updateOrderNotes(orderId, notes) {
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2441
|
+
void orderId;
|
|
2442
|
+
void notes;
|
|
2443
|
+
throw new BrainerceError(
|
|
2444
|
+
"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.",
|
|
2445
|
+
400
|
|
2446
|
+
);
|
|
2359
2447
|
}
|
|
2360
2448
|
/**
|
|
2361
|
-
* Get refunds for an order
|
|
2362
|
-
* Returns refunds from the source platform (Shopify/WooCommerce only)
|
|
2449
|
+
* Get refunds for an order.
|
|
2363
2450
|
*
|
|
2364
|
-
*
|
|
2365
|
-
*
|
|
2366
|
-
*
|
|
2367
|
-
* console.log('Total refunds:', refunds.length);
|
|
2368
|
-
* ```
|
|
2451
|
+
* **Not callable.** The API-key `/v1` surface has no refunds route, so this
|
|
2452
|
+
* throws in every mode. Read refunds from the dashboard until the route
|
|
2453
|
+
* ships.
|
|
2369
2454
|
*/
|
|
2370
2455
|
async getOrderRefunds(orderId) {
|
|
2371
|
-
|
|
2456
|
+
void orderId;
|
|
2457
|
+
throw new BrainerceError(
|
|
2458
|
+
"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.",
|
|
2459
|
+
400
|
|
2460
|
+
);
|
|
2372
2461
|
}
|
|
2373
2462
|
/**
|
|
2374
|
-
* Create a refund for an order
|
|
2375
|
-
* Creates refund on the source platform (Shopify/WooCommerce only)
|
|
2463
|
+
* Create a refund for an order.
|
|
2376
2464
|
*
|
|
2377
|
-
*
|
|
2378
|
-
*
|
|
2379
|
-
* // Full refund
|
|
2380
|
-
* const refund = await client.createRefund('order_123', {
|
|
2381
|
-
* type: 'full',
|
|
2382
|
-
* restockInventory: true,
|
|
2383
|
-
* notifyCustomer: true,
|
|
2384
|
-
* reason: 'Customer request',
|
|
2385
|
-
* });
|
|
2386
|
-
*
|
|
2387
|
-
* // Partial refund
|
|
2388
|
-
* const partialRefund = await client.createRefund('order_123', {
|
|
2389
|
-
* type: 'partial',
|
|
2390
|
-
* items: [
|
|
2391
|
-
* { lineItemId: 'item_456', quantity: 1 },
|
|
2392
|
-
* ],
|
|
2393
|
-
* restockInventory: true,
|
|
2394
|
-
* });
|
|
2395
|
-
* ```
|
|
2465
|
+
* **Not callable.** The API-key `/v1` surface has no refunds route, so this
|
|
2466
|
+
* throws in every mode. Refund from the dashboard until the route ships.
|
|
2396
2467
|
*/
|
|
2397
2468
|
async createRefund(orderId, data) {
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2469
|
+
void orderId;
|
|
2470
|
+
void data;
|
|
2471
|
+
throw new BrainerceError(
|
|
2472
|
+
"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.",
|
|
2473
|
+
400
|
|
2402
2474
|
);
|
|
2403
2475
|
}
|
|
2404
2476
|
/**
|
|
2405
|
-
* Update order shipping address
|
|
2406
|
-
* Syncs to source platform (Shopify/WooCommerce only)
|
|
2477
|
+
* Update order shipping address.
|
|
2407
2478
|
*
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2410
|
-
*
|
|
2411
|
-
* firstName: 'John',
|
|
2412
|
-
* lastName: 'Doe',
|
|
2413
|
-
* line1: '456 New Address',
|
|
2414
|
-
* city: 'Los Angeles',
|
|
2415
|
-
* state: 'CA',
|
|
2416
|
-
* country: 'US',
|
|
2417
|
-
* postalCode: '90001',
|
|
2418
|
-
* });
|
|
2419
|
-
* ```
|
|
2479
|
+
* **Not callable.** The API-key `/v1` surface has no order-shipping route,
|
|
2480
|
+
* so this throws in every mode. Correct the address from the dashboard
|
|
2481
|
+
* until the route ships.
|
|
2420
2482
|
*/
|
|
2421
2483
|
async updateOrderShipping(orderId, data) {
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2484
|
+
void orderId;
|
|
2485
|
+
void data;
|
|
2486
|
+
throw new BrainerceError(
|
|
2487
|
+
"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.",
|
|
2488
|
+
400
|
|
2426
2489
|
);
|
|
2427
2490
|
}
|
|
2428
2491
|
/**
|
|
@@ -2488,17 +2551,19 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2488
2551
|
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments`);
|
|
2489
2552
|
}
|
|
2490
2553
|
/**
|
|
2491
|
-
* Cancel an order
|
|
2492
|
-
* Works for Shopify and WooCommerce orders that haven't been fulfilled
|
|
2554
|
+
* Cancel an order.
|
|
2493
2555
|
*
|
|
2494
|
-
*
|
|
2495
|
-
*
|
|
2496
|
-
*
|
|
2497
|
-
*
|
|
2498
|
-
* ```
|
|
2556
|
+
* **Not callable.** The API-key `/v1` surface has no cancel route, so this
|
|
2557
|
+
* throws in every mode. A status move to cancelled may be reachable through
|
|
2558
|
+
* {@link updateOrder} depending on what the order's state machine allows;
|
|
2559
|
+
* otherwise cancel from the dashboard.
|
|
2499
2560
|
*/
|
|
2500
2561
|
async cancelOrder(orderId) {
|
|
2501
|
-
|
|
2562
|
+
void orderId;
|
|
2563
|
+
throw new BrainerceError(
|
|
2564
|
+
"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.",
|
|
2565
|
+
400
|
|
2566
|
+
);
|
|
2502
2567
|
}
|
|
2503
2568
|
/**
|
|
2504
2569
|
* Fulfill an order (mark as shipped), or correct the tracking of an order
|
|
@@ -2512,110 +2577,87 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2512
2577
|
* ship date is not rewritten, and no fulfilment event fires. That is the way
|
|
2513
2578
|
* to fix a mistyped tracking number.
|
|
2514
2579
|
*
|
|
2515
|
-
*
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2519
|
-
*
|
|
2520
|
-
* trackingCompany: 'UPS',
|
|
2521
|
-
* trackingUrl: 'https://www.ups.com/track?tracknum=1Z999AA10123456784',
|
|
2522
|
-
* notifyCustomer: true,
|
|
2523
|
-
* });
|
|
2524
|
-
*
|
|
2525
|
-
* // Correction — silent unless you opt back in.
|
|
2526
|
-
* await client.fulfillOrder('order_123', {
|
|
2527
|
-
* trackingNumber: '1Z999AA10123456785',
|
|
2528
|
-
* });
|
|
2529
|
-
* ```
|
|
2580
|
+
* **Not callable.** The API-key `/v1` surface has no fulfil route, so this
|
|
2581
|
+
* throws in every mode. To ship an order over the API today, buy a label
|
|
2582
|
+
* with {@link createShippingLabel} — the carrier's webhooks then move the
|
|
2583
|
+
* shipment through in-transit and delivered on their own. Otherwise fulfil
|
|
2584
|
+
* from the dashboard.
|
|
2530
2585
|
*/
|
|
2531
2586
|
async fulfillOrder(orderId, data) {
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2587
|
+
void orderId;
|
|
2588
|
+
void data;
|
|
2589
|
+
throw new BrainerceError(
|
|
2590
|
+
"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.",
|
|
2591
|
+
400
|
|
2536
2592
|
);
|
|
2537
2593
|
}
|
|
2538
2594
|
/**
|
|
2539
|
-
* Sync draft orders from connected platforms
|
|
2595
|
+
* Sync draft orders from connected platforms.
|
|
2540
2596
|
*
|
|
2541
|
-
*
|
|
2542
|
-
*
|
|
2543
|
-
*
|
|
2544
|
-
* console.log('Draft orders synced');
|
|
2545
|
-
* ```
|
|
2597
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2598
|
+
* all, so this throws in every mode. {@link triggerSync} covers a general
|
|
2599
|
+
* platform sync; draft orders are managed from the dashboard.
|
|
2546
2600
|
*/
|
|
2547
2601
|
async syncDraftOrders() {
|
|
2548
|
-
|
|
2602
|
+
throw new BrainerceError(
|
|
2603
|
+
"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.",
|
|
2604
|
+
400
|
|
2605
|
+
);
|
|
2549
2606
|
}
|
|
2550
2607
|
/**
|
|
2551
|
-
* Complete a draft order (convert to regular order)
|
|
2608
|
+
* Complete a draft order (convert to regular order).
|
|
2552
2609
|
*
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2555
|
-
* const order = await client.completeDraftOrder('draft_123', {
|
|
2556
|
-
* paymentPending: false,
|
|
2557
|
-
* });
|
|
2558
|
-
* ```
|
|
2610
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2611
|
+
* all, so this throws in every mode. Complete drafts from the dashboard.
|
|
2559
2612
|
*/
|
|
2560
2613
|
async completeDraftOrder(orderId, data) {
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2614
|
+
void orderId;
|
|
2615
|
+
void data;
|
|
2616
|
+
throw new BrainerceError(
|
|
2617
|
+
"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.",
|
|
2618
|
+
400
|
|
2565
2619
|
);
|
|
2566
2620
|
}
|
|
2567
2621
|
/**
|
|
2568
|
-
* Send invoice for a draft order
|
|
2622
|
+
* Send invoice for a draft order.
|
|
2569
2623
|
*
|
|
2570
|
-
*
|
|
2571
|
-
*
|
|
2572
|
-
* await client.sendDraftInvoice('draft_123', {
|
|
2573
|
-
* to: 'customer@example.com',
|
|
2574
|
-
* subject: 'Your Invoice',
|
|
2575
|
-
* customMessage: 'Thank you for your order!',
|
|
2576
|
-
* });
|
|
2577
|
-
* ```
|
|
2624
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2625
|
+
* all, so this throws in every mode. Send the invoice from the dashboard.
|
|
2578
2626
|
*/
|
|
2579
2627
|
async sendDraftInvoice(orderId, data) {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2628
|
+
void orderId;
|
|
2629
|
+
void data;
|
|
2630
|
+
throw new BrainerceError(
|
|
2631
|
+
"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.",
|
|
2632
|
+
400
|
|
2584
2633
|
);
|
|
2585
2634
|
}
|
|
2586
2635
|
/**
|
|
2587
|
-
* Delete a draft order
|
|
2636
|
+
* Delete a draft order.
|
|
2588
2637
|
*
|
|
2589
|
-
*
|
|
2590
|
-
*
|
|
2591
|
-
* await client.deleteDraftOrder('draft_123');
|
|
2592
|
-
* ```
|
|
2638
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2639
|
+
* all, so this throws in every mode. Delete drafts from the dashboard.
|
|
2593
2640
|
*/
|
|
2594
2641
|
async deleteDraftOrder(orderId) {
|
|
2595
|
-
|
|
2642
|
+
void orderId;
|
|
2643
|
+
throw new BrainerceError(
|
|
2644
|
+
"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.",
|
|
2645
|
+
400
|
|
2646
|
+
);
|
|
2596
2647
|
}
|
|
2597
2648
|
/**
|
|
2598
|
-
* Update a draft order
|
|
2649
|
+
* Update a draft order.
|
|
2599
2650
|
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2602
|
-
* const order = await client.updateDraftOrder('draft_123', {
|
|
2603
|
-
* note: 'Updated customer note',
|
|
2604
|
-
* email: 'newemail@example.com',
|
|
2605
|
-
* shippingAddress: {
|
|
2606
|
-
* firstName: 'John',
|
|
2607
|
-
* lastName: 'Doe',
|
|
2608
|
-
* address1: '123 Main St',
|
|
2609
|
-
* city: 'New York',
|
|
2610
|
-
* province: 'NY',
|
|
2611
|
-
* country: 'US',
|
|
2612
|
-
* zip: '10001',
|
|
2613
|
-
* },
|
|
2614
|
-
* });
|
|
2615
|
-
* ```
|
|
2651
|
+
* **Not callable.** The API-key `/v1` surface has no draft-order routes at
|
|
2652
|
+
* all, so this throws in every mode. Edit drafts from the dashboard.
|
|
2616
2653
|
*/
|
|
2617
2654
|
async updateDraftOrder(orderId, data) {
|
|
2618
|
-
|
|
2655
|
+
void orderId;
|
|
2656
|
+
void data;
|
|
2657
|
+
throw new BrainerceError(
|
|
2658
|
+
"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.",
|
|
2659
|
+
400
|
|
2660
|
+
);
|
|
2619
2661
|
}
|
|
2620
2662
|
// -------------------- Inventory --------------------
|
|
2621
2663
|
/**
|
|
@@ -2630,82 +2672,91 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2630
2672
|
);
|
|
2631
2673
|
}
|
|
2632
2674
|
/**
|
|
2633
|
-
* Get current inventory for a product
|
|
2675
|
+
* Get current inventory for a product.
|
|
2676
|
+
*
|
|
2677
|
+
* **Admin mode only** — the API key needs the `inventory:read` scope.
|
|
2678
|
+
*
|
|
2679
|
+
* This used to request `/api/v1/inventory/:productId`, which does not
|
|
2680
|
+
* exist and 404'd silently. The live route is product-scoped:
|
|
2681
|
+
* `GET /api/v1/products/:id/inventory`. A product with no inventory row
|
|
2682
|
+
* reads back as all zeroes rather than 404ing.
|
|
2634
2683
|
*/
|
|
2635
2684
|
async getInventory(productId) {
|
|
2636
|
-
return this.
|
|
2685
|
+
return this.adminRequest(
|
|
2686
|
+
"GET",
|
|
2687
|
+
`/api/v1/products/${encodePathSegment(productId)}/inventory`
|
|
2688
|
+
);
|
|
2637
2689
|
}
|
|
2638
2690
|
/**
|
|
2639
|
-
* Edit inventory manually with reason for audit trail
|
|
2691
|
+
* Edit inventory manually with a reason for the audit trail.
|
|
2640
2692
|
*
|
|
2641
|
-
*
|
|
2642
|
-
*
|
|
2643
|
-
*
|
|
2644
|
-
*
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2647
|
-
*
|
|
2648
|
-
*
|
|
2693
|
+
* **Not callable.** The API-key `/v1` surface carries no `inventory`
|
|
2694
|
+
* namespace, so this throws in every mode.
|
|
2695
|
+
*
|
|
2696
|
+
* {@link updateInventory} is the closest working call: it sets the same
|
|
2697
|
+
* absolute stock level over `PUT /api/v1/products/:id/inventory`, but the
|
|
2698
|
+
* reason is not yours to choose — the server records a generic
|
|
2699
|
+
* "Updated via External API" against the audit trail. If the reason text
|
|
2700
|
+
* matters, make the edit from the dashboard.
|
|
2649
2701
|
*/
|
|
2650
2702
|
async editInventory(data) {
|
|
2651
|
-
|
|
2703
|
+
void data;
|
|
2704
|
+
throw new BrainerceError(
|
|
2705
|
+
"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.",
|
|
2706
|
+
400
|
|
2707
|
+
);
|
|
2652
2708
|
}
|
|
2653
2709
|
/**
|
|
2654
|
-
* Get inventory sync status for all products in the store
|
|
2710
|
+
* Get inventory sync status for all products in the store.
|
|
2655
2711
|
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2658
|
-
*
|
|
2659
|
-
* console.log(`${status.pending} products pending sync`);
|
|
2660
|
-
* console.log(`Last sync: ${status.lastSyncAt}`);
|
|
2661
|
-
* ```
|
|
2712
|
+
* **Not callable.** The API-key `/v1` surface carries no `inventory`
|
|
2713
|
+
* namespace, so this throws in every mode. Sync state is visible in the
|
|
2714
|
+
* dashboard; {@link getSyncStatus} covers platform sync jobs.
|
|
2662
2715
|
*/
|
|
2663
2716
|
async getInventorySyncStatus() {
|
|
2664
|
-
|
|
2717
|
+
throw new BrainerceError(
|
|
2718
|
+
"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.",
|
|
2719
|
+
400
|
|
2720
|
+
);
|
|
2665
2721
|
}
|
|
2666
2722
|
/**
|
|
2667
|
-
* Get inventory for multiple products at once
|
|
2723
|
+
* Get inventory for multiple products at once.
|
|
2668
2724
|
*
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
*
|
|
2672
|
-
*
|
|
2673
|
-
* console.log(`${inv.productId}: ${inv.available} available`);
|
|
2674
|
-
* });
|
|
2675
|
-
* ```
|
|
2725
|
+
* **Not callable.** The API-key `/v1` surface carries no `inventory`
|
|
2726
|
+
* namespace, so this throws in every mode. There is no bulk stock read on
|
|
2727
|
+
* the API key today: fall back to {@link getInventory} per product, or read
|
|
2728
|
+
* the stock that {@link getProducts} already returns on each product.
|
|
2676
2729
|
*/
|
|
2677
2730
|
async getBulkInventory(productIds) {
|
|
2678
|
-
|
|
2731
|
+
void productIds;
|
|
2732
|
+
throw new BrainerceError(
|
|
2733
|
+
"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.",
|
|
2734
|
+
400
|
|
2735
|
+
);
|
|
2679
2736
|
}
|
|
2680
2737
|
/**
|
|
2681
|
-
* Reconcile inventory between Brainerce and connected platforms
|
|
2682
|
-
* Detects and optionally fixes discrepancies
|
|
2683
|
-
*
|
|
2684
|
-
* @example
|
|
2685
|
-
* ```typescript
|
|
2686
|
-
* // Reconcile single product (dry run)
|
|
2687
|
-
* const result = await client.reconcileInventory({ productId: 'prod_123' });
|
|
2738
|
+
* Reconcile inventory between Brainerce and connected platforms.
|
|
2739
|
+
* Detects and optionally fixes discrepancies.
|
|
2688
2740
|
*
|
|
2689
|
-
*
|
|
2690
|
-
*
|
|
2691
|
-
*
|
|
2692
|
-
* ```
|
|
2741
|
+
* **Not callable.** The API-key `/v1` surface carries no `inventory`
|
|
2742
|
+
* namespace, so this throws in every mode, `autoFix` included. Reconcile
|
|
2743
|
+
* from the dashboard.
|
|
2693
2744
|
*/
|
|
2694
2745
|
async reconcileInventory(options) {
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
"POST",
|
|
2700
|
-
"/api/v1/inventory/reconcile",
|
|
2701
|
-
void 0,
|
|
2702
|
-
queryParams
|
|
2746
|
+
void options;
|
|
2747
|
+
throw new BrainerceError(
|
|
2748
|
+
"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.",
|
|
2749
|
+
400
|
|
2703
2750
|
);
|
|
2704
2751
|
}
|
|
2705
2752
|
/**
|
|
2706
2753
|
* Check stock availability for one or more items before adding to cart or checkout
|
|
2707
2754
|
* Use this to validate stock before operations that might fail due to insufficient inventory
|
|
2708
2755
|
*
|
|
2756
|
+
* **Vibe-coded or storefront mode only.** There is no stock-check route on
|
|
2757
|
+
* the API-key `/v1` surface; in admin mode this throws. The same applies to
|
|
2758
|
+
* {@link checkCartStock}, which routes through here.
|
|
2759
|
+
*
|
|
2709
2760
|
* @example
|
|
2710
2761
|
* ```typescript
|
|
2711
2762
|
* // Check if items are available before adding to cart
|
|
@@ -2737,9 +2788,10 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2737
2788
|
{ items }
|
|
2738
2789
|
);
|
|
2739
2790
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2791
|
+
throw new BrainerceError(
|
|
2792
|
+
"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.",
|
|
2793
|
+
400
|
|
2794
|
+
);
|
|
2743
2795
|
}
|
|
2744
2796
|
/**
|
|
2745
2797
|
* Check stock availability for cart items before checkout.
|
|
@@ -3046,6 +3098,11 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3046
3098
|
* Register a new customer with password (creates account)
|
|
3047
3099
|
* Works in vibe-coded, storefront, and admin mode
|
|
3048
3100
|
*
|
|
3101
|
+
* `birthMonth`/`birthDay` are optional and must be sent together. When
|
|
3102
|
+
* `getStoreInfo().requireBirthday` is true the merchant made the birthday
|
|
3103
|
+
* mandatory on that sales channel, and a call without both fields is
|
|
3104
|
+
* rejected with HTTP 400.
|
|
3105
|
+
*
|
|
3049
3106
|
* @example
|
|
3050
3107
|
* ```typescript
|
|
3051
3108
|
* const auth = await client.registerCustomer({
|
|
@@ -3053,6 +3110,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3053
3110
|
* password: 'securepassword123',
|
|
3054
3111
|
* firstName: 'Jane',
|
|
3055
3112
|
* lastName: 'Doe',
|
|
3113
|
+
* birthMonth: 4, // optional, unless the channel requires a birthday
|
|
3114
|
+
* birthDay: 17,
|
|
3056
3115
|
* });
|
|
3057
3116
|
* ```
|
|
3058
3117
|
*/
|
|
@@ -3069,49 +3128,43 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3069
3128
|
* Request a password reset email for a customer
|
|
3070
3129
|
* Works in vibe-coded, storefront, and admin mode
|
|
3071
3130
|
*
|
|
3072
|
-
* The
|
|
3073
|
-
*
|
|
3074
|
-
*
|
|
3075
|
-
*
|
|
3076
|
-
*
|
|
3077
|
-
*
|
|
3078
|
-
*
|
|
3131
|
+
* The reset link's host is chosen by the server, not by the caller: it is
|
|
3132
|
+
* derived from the sales channel's own domain, falling back to the backend's
|
|
3133
|
+
* configured frontend URL, and the request is rejected if neither resolves.
|
|
3134
|
+
*
|
|
3135
|
+
* The SDK used to send a `resetUrl` in the request body. The backend
|
|
3136
|
+
* deliberately removed that field: any caller could submit an arbitrary URL
|
|
3137
|
+
* and have it emailed, from a Brainerce-domained sender, to the address
|
|
3138
|
+
* holder — a phishing-link injection. `ForgotPasswordDto` now declares
|
|
3139
|
+
* `email` and nothing else, and the API's global validation pipe runs with
|
|
3140
|
+
* `whitelist` + `forbidNonWhitelisted`, so a body carrying `resetUrl` fails
|
|
3141
|
+
* the whole call with `400 property resetUrl should not exist`. Only `email`
|
|
3142
|
+
* is sent.
|
|
3143
|
+
*
|
|
3144
|
+
* The endpoint always answers 200 so it cannot be used to enumerate
|
|
3145
|
+
* accounts; the mail is only sent when a matching customer exists.
|
|
3079
3146
|
*
|
|
3080
3147
|
* @param email - Customer email address
|
|
3081
|
-
* @param options -
|
|
3082
|
-
* @param options.resetUrl - Reset URL the email links should point to.
|
|
3083
|
-
* Required outside the browser; recommended inside it.
|
|
3148
|
+
* @param options - Accepted for source compatibility only. Ignored.
|
|
3084
3149
|
*/
|
|
3085
3150
|
async forgotPassword(email, options) {
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
if (typeof window === "undefined") {
|
|
3089
|
-
throw new BrainerceError(
|
|
3090
|
-
'forgotPassword: `resetUrl` is required outside the browser. Pass `{ resetUrl: "https://your-site.example/reset-password" }` so the email links to the right host.',
|
|
3091
|
-
400
|
|
3092
|
-
);
|
|
3093
|
-
}
|
|
3094
|
-
console.warn(
|
|
3095
|
-
"BrainerceClient.forgotPassword: deriving `resetUrl` from `window.location.origin` \u2014 pass `{ resetUrl }` explicitly to avoid wrong-host links behind proxies or in SSR."
|
|
3096
|
-
);
|
|
3097
|
-
resetUrl = `${window.location.origin}/reset-password`;
|
|
3098
|
-
}
|
|
3151
|
+
void options;
|
|
3152
|
+
const body = { email };
|
|
3099
3153
|
if (this.isVibeCodedMode()) {
|
|
3100
|
-
return this.vibeCodedRequest("POST", "/customers/forgot-password",
|
|
3101
|
-
email,
|
|
3102
|
-
resetUrl
|
|
3103
|
-
});
|
|
3154
|
+
return this.vibeCodedRequest("POST", "/customers/forgot-password", body);
|
|
3104
3155
|
}
|
|
3105
3156
|
if (this.storeId && !this.apiKey) {
|
|
3106
|
-
return this.storefrontRequest(
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3157
|
+
return this.storefrontRequest(
|
|
3158
|
+
"POST",
|
|
3159
|
+
"/customers/forgot-password",
|
|
3160
|
+
body
|
|
3161
|
+
);
|
|
3110
3162
|
}
|
|
3111
|
-
return this.adminRequest(
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3163
|
+
return this.adminRequest(
|
|
3164
|
+
"POST",
|
|
3165
|
+
"/api/v1/customers/forgot-password",
|
|
3166
|
+
body
|
|
3167
|
+
);
|
|
3115
3168
|
}
|
|
3116
3169
|
/**
|
|
3117
3170
|
* Reset customer password using a reset token received via email
|
|
@@ -4331,16 +4384,27 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4331
4384
|
* List visible reviews for a product (storefront / sales-channel modes).
|
|
4332
4385
|
* Reviews that the merchant has hidden are excluded.
|
|
4333
4386
|
*
|
|
4387
|
+
* Each review carries `images` — the photos its author attached, already
|
|
4388
|
+
* filtered to the ones shoppers are allowed to see. Always an array.
|
|
4389
|
+
*
|
|
4390
|
+
* Ordering defaults to `photos_first`: reviews carrying photos lead, newest-first
|
|
4391
|
+
* within each group. Pass `sort: 'newest'` for plain chronological order. On a
|
|
4392
|
+
* store with no review photos the two are identical.
|
|
4393
|
+
*
|
|
4334
4394
|
* @example
|
|
4335
4395
|
* ```typescript
|
|
4336
4396
|
* const { data, meta } = await client.listProductReviews('prod_123', { page: 1, limit: 20 });
|
|
4337
|
-
* data.forEach(r =>
|
|
4397
|
+
* data.forEach(r => {
|
|
4398
|
+
* console.log(r.rating, r.body, r.verifiedPurchase);
|
|
4399
|
+
* r.images.forEach(img => console.log(img.thumbnailUrl ?? img.url));
|
|
4400
|
+
* });
|
|
4338
4401
|
* ```
|
|
4339
4402
|
*/
|
|
4340
4403
|
async listProductReviews(productId, params) {
|
|
4341
4404
|
const queryParams = {};
|
|
4342
4405
|
if (params?.page) queryParams.page = params.page;
|
|
4343
4406
|
if (params?.limit) queryParams.limit = params.limit;
|
|
4407
|
+
if (params?.sort) queryParams.sort = params.sort;
|
|
4344
4408
|
if (this.isVibeCodedMode()) {
|
|
4345
4409
|
return this.vibeCodedRequest(
|
|
4346
4410
|
"GET",
|
|
@@ -4498,6 +4562,37 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4498
4562
|
storeId ? { storeId } : void 0
|
|
4499
4563
|
);
|
|
4500
4564
|
}
|
|
4565
|
+
/**
|
|
4566
|
+
* Admin: hide ONE photo on a review, leaving the review and its other photos
|
|
4567
|
+
* visible. Requires an API key with `reviews:write`.
|
|
4568
|
+
*/
|
|
4569
|
+
async hideProductReviewImage(imageId, storeId) {
|
|
4570
|
+
if (!this.apiKey) {
|
|
4571
|
+
throw new BrainerceError("hideProductReviewImage() requires admin (API key) mode", 400);
|
|
4572
|
+
}
|
|
4573
|
+
return this.adminRequest(
|
|
4574
|
+
"PATCH",
|
|
4575
|
+
`/api/v1/review-images/${encodePathSegment(imageId)}/hide`,
|
|
4576
|
+
void 0,
|
|
4577
|
+
storeId ? { storeId } : void 0
|
|
4578
|
+
);
|
|
4579
|
+
}
|
|
4580
|
+
/**
|
|
4581
|
+
* Admin: show one review photo. This is also the approve action — a photo that
|
|
4582
|
+
* has never been approved carries no `approvedAt`, and showing it stamps one, so
|
|
4583
|
+
* stores using review-photo approval need no separate verb.
|
|
4584
|
+
*/
|
|
4585
|
+
async showProductReviewImage(imageId, storeId) {
|
|
4586
|
+
if (!this.apiKey) {
|
|
4587
|
+
throw new BrainerceError("showProductReviewImage() requires admin (API key) mode", 400);
|
|
4588
|
+
}
|
|
4589
|
+
return this.adminRequest(
|
|
4590
|
+
"PATCH",
|
|
4591
|
+
`/api/v1/review-images/${encodePathSegment(imageId)}/show`,
|
|
4592
|
+
void 0,
|
|
4593
|
+
storeId ? { storeId } : void 0
|
|
4594
|
+
);
|
|
4595
|
+
}
|
|
4501
4596
|
/** Admin: unhide a previously hidden review. */
|
|
4502
4597
|
async showProductReview(reviewId, storeId) {
|
|
4503
4598
|
if (!this.apiKey) {
|
|
@@ -7153,11 +7248,20 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
7153
7248
|
);
|
|
7154
7249
|
}
|
|
7155
7250
|
/**
|
|
7156
|
-
* Update the current customer's profile (requires customerToken)
|
|
7157
|
-
* Only available in storefront mode
|
|
7251
|
+
* Update the current customer's profile (requires customerToken).
|
|
7252
|
+
* Only available in storefront and vibe-coded mode.
|
|
7158
7253
|
*
|
|
7159
|
-
* `birthMonth`/`birthDay` (1-12 / 1-31
|
|
7160
|
-
*
|
|
7254
|
+
* `birthMonth`/`birthDay` (1-12 / 1-31) power the loyalty birthday gift.
|
|
7255
|
+
* Month and day only, never a year, for privacy. Send both or neither, and
|
|
7256
|
+
* the day has to exist in the month: anything else is rejected with HTTP 400.
|
|
7257
|
+
* Send `null` for both to REMOVE a stored birthday. Leaving the keys out is a
|
|
7258
|
+
* different request and keeps the stored value, so a profile form whose
|
|
7259
|
+
* fields the shopper cleared has to send nulls, not omit them.
|
|
7260
|
+
*
|
|
7261
|
+
* The saved values now come back on the returned `CustomerProfile`, on
|
|
7262
|
+
* `getMyProfile()` and on every customer read type, so a profile form can
|
|
7263
|
+
* re-render the birthday it just saved. Before this, no API returned the two
|
|
7264
|
+
* fields at all and the form always came back blank.
|
|
7161
7265
|
*/
|
|
7162
7266
|
async updateMyProfile(data) {
|
|
7163
7267
|
if (!this.customerToken && !this.proxyMode) {
|
|
@@ -9334,6 +9438,68 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9334
9438
|
}
|
|
9335
9439
|
throw new Error("uploadCustomizationFile requires storefront or vibe-coded mode");
|
|
9336
9440
|
}
|
|
9441
|
+
/**
|
|
9442
|
+
* Upload one photo to attach to a product review.
|
|
9443
|
+
*
|
|
9444
|
+
* Available in storefront and vibe-coded modes, and — unlike
|
|
9445
|
+
* `uploadCustomizationFile` — it REQUIRES a logged-in customer who has actually
|
|
9446
|
+
* bought the product. Call `setCustomerToken(...)` first. That is the same bar as
|
|
9447
|
+
* writing the review itself, checked here so an ineligible shopper is told before
|
|
9448
|
+
* they wait for the upload rather than after.
|
|
9449
|
+
*
|
|
9450
|
+
* Returns a storage `key`. Collect the keys and pass them as `imageKeys` when you
|
|
9451
|
+
* submit or update the review — the `url` is for a local preview only, and sending
|
|
9452
|
+
* it back instead of the key will be rejected.
|
|
9453
|
+
*
|
|
9454
|
+
* Server rules:
|
|
9455
|
+
* - `image/jpeg|png|webp|gif` only, cross-checked against the file's real bytes.
|
|
9456
|
+
* - Max 5 MB and 40 megapixels per file.
|
|
9457
|
+
* - Throttled to 10 uploads / minute → HTTP 429.
|
|
9458
|
+
* - 403 when the store has review photos turned off, or the customer has not
|
|
9459
|
+
* bought the product; 400 once the review is already at its photo cap.
|
|
9460
|
+
* - EXIF is stripped (so GPS coordinates never reach the storefront) while the
|
|
9461
|
+
* orientation tag is applied first, so phone photos stay upright.
|
|
9462
|
+
* - A photo uploaded but never attached to a submitted review is reclaimed after
|
|
9463
|
+
* 7 days.
|
|
9464
|
+
*
|
|
9465
|
+
* Read `photos` from `getMyProductReview()` for the store's live limits rather
|
|
9466
|
+
* than hard-coding them.
|
|
9467
|
+
*
|
|
9468
|
+
* @example
|
|
9469
|
+
* ```ts
|
|
9470
|
+
* const { photos } = await client.getMyProductReview(productId);
|
|
9471
|
+
* if (photos.enabled) {
|
|
9472
|
+
* const uploads = await Promise.all(
|
|
9473
|
+
* [...fileInput.files].slice(0, photos.maxPerReview)
|
|
9474
|
+
* .map(f => client.uploadReviewPhoto(productId, f))
|
|
9475
|
+
* );
|
|
9476
|
+
* await client.submitProductReview(productId, {
|
|
9477
|
+
* rating: 5,
|
|
9478
|
+
* body: 'Arrived beautifully wrapped.',
|
|
9479
|
+
* imageKeys: uploads.map(u => u.key),
|
|
9480
|
+
* });
|
|
9481
|
+
* }
|
|
9482
|
+
* ```
|
|
9483
|
+
*/
|
|
9484
|
+
async uploadReviewPhoto(productId, file) {
|
|
9485
|
+
const formData = new FormData();
|
|
9486
|
+
formData.append("file", file);
|
|
9487
|
+
if (this.isVibeCodedMode()) {
|
|
9488
|
+
return this.vibeCodedRequest(
|
|
9489
|
+
"POST",
|
|
9490
|
+
`/products/${encodePathSegment(productId)}/review-photo`,
|
|
9491
|
+
formData
|
|
9492
|
+
);
|
|
9493
|
+
}
|
|
9494
|
+
if (this.storeId && !this.apiKey) {
|
|
9495
|
+
return this.storefrontRequest(
|
|
9496
|
+
"POST",
|
|
9497
|
+
`/products/${encodePathSegment(productId)}/review-photo`,
|
|
9498
|
+
formData
|
|
9499
|
+
);
|
|
9500
|
+
}
|
|
9501
|
+
throw new Error("uploadReviewPhoto requires storefront or vibe-coded mode");
|
|
9502
|
+
}
|
|
9337
9503
|
// -------------------- Team Management (Admin) - DEPRECATED --------------------
|
|
9338
9504
|
// Account-level team methods. These are the ONLY team endpoints reachable with
|
|
9339
9505
|
// a `brainerce_*` API key. The store-level equivalents (`getStoreTeam`,
|
|
@@ -10021,7 +10187,7 @@ var DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\
|
|
|
10021
10187
|
var MIN_OFFSET_MINUTES = -12 * 60;
|
|
10022
10188
|
var MAX_OFFSET_MINUTES = 14 * 60;
|
|
10023
10189
|
var VALID_OFFSET_MINUTE_PARTS = [0, 30, 45];
|
|
10024
|
-
function validateDateAvailabilityConfig(config, fieldType) {
|
|
10190
|
+
function validateDateAvailabilityConfig(config, fieldType, surface = "checkout") {
|
|
10025
10191
|
const errors = [];
|
|
10026
10192
|
if (!config) return errors;
|
|
10027
10193
|
if (config.minDate !== void 0 && !DATE_RE.test(config.minDate)) {
|
|
@@ -10033,6 +10199,29 @@ function validateDateAvailabilityConfig(config, fieldType) {
|
|
|
10033
10199
|
if (config.minDate && config.maxDate && DATE_RE.test(config.minDate) && DATE_RE.test(config.maxDate) && config.minDate > config.maxDate) {
|
|
10034
10200
|
errors.push("minDate must be on or before maxDate");
|
|
10035
10201
|
}
|
|
10202
|
+
if (config.leadTimeMinutes !== void 0) {
|
|
10203
|
+
if (!Number.isInteger(config.leadTimeMinutes) || config.leadTimeMinutes < 0) {
|
|
10204
|
+
errors.push("leadTimeMinutes must be a non-negative integer");
|
|
10205
|
+
}
|
|
10206
|
+
}
|
|
10207
|
+
if (config.maxDaysAhead !== void 0) {
|
|
10208
|
+
if (!Number.isInteger(config.maxDaysAhead) || config.maxDaysAhead < 1) {
|
|
10209
|
+
errors.push("maxDaysAhead must be a positive integer");
|
|
10210
|
+
}
|
|
10211
|
+
}
|
|
10212
|
+
if (config.cutoffTime !== void 0 && !TIME_RE.test(config.cutoffTime)) {
|
|
10213
|
+
errors.push("cutoffTime must be in HH:mm format");
|
|
10214
|
+
}
|
|
10215
|
+
if (surface !== "checkout") {
|
|
10216
|
+
const relativeKeys = ["leadTimeMinutes", "cutoffTime", "maxDaysAhead"].filter(
|
|
10217
|
+
(k) => config[k] !== void 0
|
|
10218
|
+
);
|
|
10219
|
+
if (relativeKeys.length > 0) {
|
|
10220
|
+
errors.push(
|
|
10221
|
+
`${relativeKeys.join("/")} only apply to checkout fields, not ${surface} fields`
|
|
10222
|
+
);
|
|
10223
|
+
}
|
|
10224
|
+
}
|
|
10036
10225
|
if (config.blockedWeekdays) {
|
|
10037
10226
|
for (const d of config.blockedWeekdays) {
|
|
10038
10227
|
if (!Number.isInteger(d) || d < 0 || d > 6) {
|
|
@@ -10050,22 +10239,37 @@ function validateDateAvailabilityConfig(config, fieldType) {
|
|
|
10050
10239
|
}
|
|
10051
10240
|
}
|
|
10052
10241
|
if (config.businessHours) {
|
|
10053
|
-
const
|
|
10242
|
+
const windowsByWeekday = /* @__PURE__ */ new Map();
|
|
10054
10243
|
for (const w of config.businessHours) {
|
|
10055
10244
|
if (!Number.isInteger(w.weekday) || w.weekday < 0 || w.weekday > 6) {
|
|
10056
10245
|
errors.push(`businessHours.weekday must be an integer 0-6, got ${JSON.stringify(w.weekday)}`);
|
|
10057
10246
|
continue;
|
|
10058
10247
|
}
|
|
10059
|
-
if (seenWeekdays.has(w.weekday)) {
|
|
10060
|
-
errors.push(`businessHours has more than one window for weekday ${w.weekday}`);
|
|
10061
|
-
}
|
|
10062
|
-
seenWeekdays.add(w.weekday);
|
|
10063
10248
|
const openValid = TIME_RE.test(w.open);
|
|
10064
10249
|
const closeValid = TIME_RE.test(w.close);
|
|
10065
10250
|
if (!openValid) errors.push(`businessHours.open must be HH:mm, got "${w.open}"`);
|
|
10066
10251
|
if (!closeValid) errors.push(`businessHours.close must be HH:mm, got "${w.close}"`);
|
|
10067
|
-
if (openValid
|
|
10252
|
+
if (!openValid || !closeValid) continue;
|
|
10253
|
+
if (w.open >= w.close) {
|
|
10068
10254
|
errors.push(`businessHours for weekday ${w.weekday}: open must be before close`);
|
|
10255
|
+
continue;
|
|
10256
|
+
}
|
|
10257
|
+
const forDay = windowsByWeekday.get(w.weekday);
|
|
10258
|
+
if (forDay) forDay.push(w);
|
|
10259
|
+
else windowsByWeekday.set(w.weekday, [w]);
|
|
10260
|
+
}
|
|
10261
|
+
const weekdaysWithWindows = Array.from(windowsByWeekday.keys()).sort((a, b) => a - b);
|
|
10262
|
+
for (const weekday of weekdaysWithWindows) {
|
|
10263
|
+
const sorted = [...windowsByWeekday.get(weekday) ?? []].sort(
|
|
10264
|
+
(a, b) => a.open < b.open ? -1 : a.open > b.open ? 1 : 0
|
|
10265
|
+
);
|
|
10266
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
10267
|
+
if (sorted[i].open < sorted[i - 1].close) {
|
|
10268
|
+
errors.push(
|
|
10269
|
+
`businessHours for weekday ${weekday}: ${sorted[i - 1].open}-${sorted[i - 1].close} overlaps ${sorted[i].open}-${sorted[i].close}`
|
|
10270
|
+
);
|
|
10271
|
+
break;
|
|
10272
|
+
}
|
|
10069
10273
|
}
|
|
10070
10274
|
}
|
|
10071
10275
|
}
|
|
@@ -10121,6 +10325,32 @@ function resolveStoreLocalParts(instant, timezone) {
|
|
|
10121
10325
|
weekday: WEEKDAY_TO_INDEX[get("weekday")] ?? instant.getUTCDay()
|
|
10122
10326
|
};
|
|
10123
10327
|
}
|
|
10328
|
+
function resolveRelativeBounds(config, clock) {
|
|
10329
|
+
if (!config || !clock) return {};
|
|
10330
|
+
const leadMinutes = typeof config.leadTimeMinutes === "number" && Number.isFinite(config.leadTimeMinutes) ? Math.max(0, Math.floor(config.leadTimeMinutes)) : 0;
|
|
10331
|
+
const cutoff = typeof config.cutoffTime === "string" && TIME_RE.test(config.cutoffTime) ? config.cutoffTime : null;
|
|
10332
|
+
const daysAhead = typeof config.maxDaysAhead === "number" && Number.isFinite(config.maxDaysAhead) && config.maxDaysAhead >= 1 ? Math.floor(config.maxDaysAhead) : null;
|
|
10333
|
+
if (leadMinutes === 0 && !cutoff && daysAhead === null) return {};
|
|
10334
|
+
const now = clock.now ?? /* @__PURE__ */ new Date();
|
|
10335
|
+
const nowLocal = resolveStoreLocalParts(now, clock.timezone);
|
|
10336
|
+
const bounds = {};
|
|
10337
|
+
if (leadMinutes > 0 || cutoff) {
|
|
10338
|
+
const earliestInstant = new Date(now.getTime() + leadMinutes * 6e4);
|
|
10339
|
+
let earliestDate = resolveStoreLocalParts(earliestInstant, clock.timezone).dateYYYYMMDD;
|
|
10340
|
+
if (cutoff && nowLocal.hhmm >= cutoff) earliestDate = addCalendarDays(earliestDate, 1);
|
|
10341
|
+
bounds.earliestDate = earliestDate;
|
|
10342
|
+
if (leadMinutes > 0) bounds.earliestInstant = earliestInstant;
|
|
10343
|
+
}
|
|
10344
|
+
if (daysAhead !== null) {
|
|
10345
|
+
bounds.latestDate = addCalendarDays(nowLocal.dateYYYYMMDD, daysAhead);
|
|
10346
|
+
}
|
|
10347
|
+
return bounds;
|
|
10348
|
+
}
|
|
10349
|
+
function addCalendarDays(dateYYYYMMDD, days) {
|
|
10350
|
+
const [year, month, day] = dateYYYYMMDD.split("-").map(Number);
|
|
10351
|
+
const shifted = new Date(Date.UTC(year, month - 1, day + days));
|
|
10352
|
+
return `${shifted.getUTCFullYear()}-${pad2(shifted.getUTCMonth() + 1)}-${pad2(shifted.getUTCDate())}`;
|
|
10353
|
+
}
|
|
10124
10354
|
function parseDateFieldValue(raw, fieldType, timezone) {
|
|
10125
10355
|
const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
|
|
10126
10356
|
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";
|
|
@@ -10216,19 +10446,30 @@ function timezoneOffsetMs(utcMillis, timezone) {
|
|
|
10216
10446
|
const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
|
|
10217
10447
|
return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
|
|
10218
10448
|
}
|
|
10219
|
-
function isCalendarDateAllowed(dateYYYYMMDD, config) {
|
|
10449
|
+
function isCalendarDateAllowed(dateYYYYMMDD, config, clock) {
|
|
10220
10450
|
if (!config) return true;
|
|
10221
10451
|
if (config.minDate && dateYYYYMMDD < config.minDate) return false;
|
|
10222
10452
|
if (config.maxDate && dateYYYYMMDD > config.maxDate) return false;
|
|
10453
|
+
if (relativeBoundFailure(dateYYYYMMDD, config, clock)) return false;
|
|
10223
10454
|
if (config.blockedDates?.includes(dateYYYYMMDD)) return false;
|
|
10224
10455
|
if (config.blockedWeekdays?.length) {
|
|
10225
10456
|
if (config.blockedWeekdays.includes(weekdayOfDateString(dateYYYYMMDD))) return false;
|
|
10226
10457
|
}
|
|
10227
10458
|
return true;
|
|
10228
10459
|
}
|
|
10229
|
-
function
|
|
10460
|
+
function relativeBoundFailure(dateYYYYMMDD, config, clock) {
|
|
10461
|
+
const bounds = resolveRelativeBounds(config, clock);
|
|
10462
|
+
if (bounds.earliestDate && dateYYYYMMDD < bounds.earliestDate) {
|
|
10463
|
+
return `the earliest available date is ${bounds.earliestDate}`;
|
|
10464
|
+
}
|
|
10465
|
+
if (bounds.latestDate && dateYYYYMMDD > bounds.latestDate) {
|
|
10466
|
+
return `the latest available date is ${bounds.latestDate}`;
|
|
10467
|
+
}
|
|
10468
|
+
return null;
|
|
10469
|
+
}
|
|
10470
|
+
function computeAvailableSlots(config, dateYYYYMMDD, clock) {
|
|
10230
10471
|
if (!config) return [];
|
|
10231
|
-
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
10472
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) return [];
|
|
10232
10473
|
if (!config.businessHours?.length || !config.slotDurationMinutes) return [];
|
|
10233
10474
|
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
10234
10475
|
const windows = config.businessHours.filter((w) => w.weekday === weekday);
|
|
@@ -10241,27 +10482,47 @@ function computeAvailableSlots(config, dateYYYYMMDD) {
|
|
|
10241
10482
|
slots.push(minutesToHHMM(t));
|
|
10242
10483
|
}
|
|
10243
10484
|
}
|
|
10244
|
-
|
|
10485
|
+
const ordered = Array.from(new Set(slots)).sort();
|
|
10486
|
+
const { earliestInstant } = resolveRelativeBounds(config, clock);
|
|
10487
|
+
if (!earliestInstant || !clock) return ordered;
|
|
10488
|
+
return ordered.filter((hhmm) => {
|
|
10489
|
+
const [hour, minute] = hhmm.split(":").map(Number);
|
|
10490
|
+
const start = instantFromStoreLocal(dateYYYYMMDD, hour, minute, 0, 0, clock.timezone);
|
|
10491
|
+
return start.getTime() >= earliestInstant.getTime();
|
|
10492
|
+
});
|
|
10245
10493
|
}
|
|
10246
|
-
function getBusinessHoursForDate(config, dateYYYYMMDD) {
|
|
10494
|
+
function getBusinessHoursForDate(config, dateYYYYMMDD, clock) {
|
|
10247
10495
|
if (!config?.businessHours?.length) return [];
|
|
10248
|
-
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
10496
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) return [];
|
|
10249
10497
|
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
10250
10498
|
return config.businessHours.filter((w) => w.weekday === weekday);
|
|
10251
10499
|
}
|
|
10252
|
-
function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
10500
|
+
function isDateValueAllowed(instant, config, fieldType, timezone, now) {
|
|
10253
10501
|
if (!config) return { allowed: true };
|
|
10502
|
+
const clock = { timezone, now };
|
|
10254
10503
|
if (fieldType === "DATE") {
|
|
10255
10504
|
const dateYYYYMMDD = `${instant.getUTCFullYear()}-${pad2(instant.getUTCMonth() + 1)}-${pad2(instant.getUTCDate())}`;
|
|
10256
|
-
|
|
10505
|
+
const relative2 = relativeBoundFailure(dateYYYYMMDD, config, clock);
|
|
10506
|
+
if (relative2) return { allowed: false, reason: relative2 };
|
|
10507
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) {
|
|
10257
10508
|
return { allowed: false, reason: "date is outside the allowed range" };
|
|
10258
10509
|
}
|
|
10259
10510
|
return { allowed: true };
|
|
10260
10511
|
}
|
|
10261
10512
|
const local = resolveStoreLocalParts(instant, timezone);
|
|
10262
|
-
|
|
10513
|
+
const relative = relativeBoundFailure(local.dateYYYYMMDD, config, clock);
|
|
10514
|
+
if (relative) return { allowed: false, reason: relative };
|
|
10515
|
+
if (!isCalendarDateAllowed(local.dateYYYYMMDD, config, clock)) {
|
|
10263
10516
|
return { allowed: false, reason: "date is outside the allowed range" };
|
|
10264
10517
|
}
|
|
10518
|
+
const { earliestInstant } = resolveRelativeBounds(config, clock);
|
|
10519
|
+
if (earliestInstant && instant.getTime() < earliestInstant.getTime()) {
|
|
10520
|
+
const earliestLocal = resolveStoreLocalParts(earliestInstant, timezone);
|
|
10521
|
+
return {
|
|
10522
|
+
allowed: false,
|
|
10523
|
+
reason: `the earliest available time is ${earliestLocal.dateYYYYMMDD} ${earliestLocal.hhmm}`
|
|
10524
|
+
};
|
|
10525
|
+
}
|
|
10265
10526
|
if (!config.businessHours?.length) {
|
|
10266
10527
|
return { allowed: true };
|
|
10267
10528
|
}
|
|
@@ -10270,7 +10531,7 @@ function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
|
10270
10531
|
return { allowed: false, reason: "no business hours are configured for this day" };
|
|
10271
10532
|
}
|
|
10272
10533
|
if (config.slotDurationMinutes) {
|
|
10273
|
-
const slots = computeAvailableSlots(config, local.dateYYYYMMDD);
|
|
10534
|
+
const slots = computeAvailableSlots(config, local.dateYYYYMMDD, clock);
|
|
10274
10535
|
if (!slots.includes(local.hhmm)) {
|
|
10275
10536
|
return { allowed: false, reason: "time does not match an available slot" };
|
|
10276
10537
|
}
|
|
@@ -10900,6 +11161,7 @@ export {
|
|
|
10900
11161
|
jsonLdScriptProps,
|
|
10901
11162
|
parseDateFieldValue,
|
|
10902
11163
|
parseWebhookEvent,
|
|
11164
|
+
resolveRelativeBounds,
|
|
10903
11165
|
resolveStoreLocalParts,
|
|
10904
11166
|
safePaymentRedirect,
|
|
10905
11167
|
stripHtml2 as stripHtml,
|