brainerce 2.5.0 → 2.8.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/dist/index.js CHANGED
@@ -204,7 +204,7 @@ function isDevGuardsEnabled() {
204
204
  }
205
205
 
206
206
  // src/version.ts
207
- var SDK_VERSION = "2.5.0";
207
+ var SDK_VERSION = "2.8.0";
208
208
 
209
209
  // src/client.ts
210
210
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -381,10 +381,14 @@ var _BrainerceClient = class _BrainerceClient {
381
381
  * Storefront (public) and vibe-coded modes only. Rate-limited server-side to
382
382
  * 3 requests / 60s per IP, plus one confirmation email per address per 24h.
383
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.
384
+ * **Where the discount goes.** Configure the newsletter welcome offer and the
385
+ * platform issues the coupon itself: read it with `marketing.getBenefit()`,
386
+ * show those terms beside the field, and stop there.
387
+ *
388
+ * ⛔ DO NOT SHOW A CODE AFTER THIS CALL RESOLVES. No coupon exists yet. It is
389
+ * minted when the recipient clicks the confirmation link, and it is mailed to
390
+ * them at that moment — a code rendered here is a code that was never issued.
391
+ * Say "check your email", the same as for the subscription itself.
388
392
  *
389
393
  * @example
390
394
  * ```typescript
@@ -412,6 +416,163 @@ var _BrainerceClient = class _BrainerceClient {
412
416
  "/marketing/subscribe",
413
417
  input
414
418
  );
419
+ },
420
+ /**
421
+ * The welcome offer to render beside the signup field, or `null` when this
422
+ * store offers none.
423
+ *
424
+ * Show the discount, how long the coupon lasts, any minimum order, whether
425
+ * it is first-order only, and the merchant's own headline and terms. Then
426
+ * post to `marketing.subscribe()` and tell the shopper to check their
427
+ * inbox.
428
+ *
429
+ * ⛔ THE COUPON DOES NOT EXIST YET at any point in that sequence. It is
430
+ * created when the recipient clicks the confirmation link in their email,
431
+ * and it is mailed to them there. Rendering a code on this screen renders a
432
+ * code nobody was issued.
433
+ *
434
+ * ⛔ Takes no email address and returns nothing about any individual, on
435
+ * purpose. There is no "has this person already claimed" call, because an
436
+ * unauthenticated one would be an oracle for who shops here. If you need to
437
+ * discourage a repeat signup, say the offer is one per address; do not try
438
+ * to detect it.
439
+ *
440
+ * `null` is the common case on a store that never set this up, so handle it
441
+ * rather than assuming the object. Cache it per page load: it belongs to
442
+ * the store, not to the visitor.
443
+ *
444
+ * Storefront (public) and vibe-coded modes.
445
+ *
446
+ * @param locale - Storefront locale, e.g. `"he"`. Picks the language of the
447
+ * headline and terms; falls back to the store language when omitted.
448
+ *
449
+ * @example
450
+ * ```typescript
451
+ * const offer = await brainerce.marketing.getBenefit('he');
452
+ * if (offer) {
453
+ * // "10% הנחה על ההזמנה הראשונה"
454
+ * render(offer.headline ?? defaultHeadline(offer), offer.terms);
455
+ * }
456
+ * await brainerce.marketing.subscribe({ email, locale: 'he', honeypot });
457
+ * // → "בדקו את המייל שלכם" — never a coupon code
458
+ * ```
459
+ */
460
+ getBenefit: async (locale) => {
461
+ const query = locale ? { locale } : void 0;
462
+ if (this.isVibeCodedMode()) {
463
+ return this.vibeCodedRequest(
464
+ "GET",
465
+ "/newsletter-benefit",
466
+ void 0,
467
+ query
468
+ );
469
+ }
470
+ return this.storefrontRequest(
471
+ "GET",
472
+ "/newsletter-benefit",
473
+ void 0,
474
+ query
475
+ );
476
+ }
477
+ };
478
+ // -------------------- Newsletter signup benefit (Admin) --------------------
479
+ /**
480
+ * Manage the newsletter welcome offer: the terms merchants configure, and the
481
+ * benefits that offer has produced.
482
+ *
483
+ * Admin mode (`apiKey`) only, on the `coupons:read` / `coupons:write` scopes.
484
+ * The benefit IS a coupon feature — it mints a Coupon row and the coupon
485
+ * machinery enforces it — so it carries no scope of its own.
486
+ *
487
+ * ⛔ THERE IS NO "ISSUE A BENEFIT TO THIS ADDRESS" CALL, and there will not
488
+ * be one. A benefit exists because someone submitted the signup form AND
489
+ * clicked the confirmation link; handing one out directly would skip the
490
+ * consent the double opt-in exists to collect and break the one-per-address
491
+ * guarantee that the grant's unique constraint provides. `resend` re-sends a
492
+ * code that already exists; it never creates one.
493
+ */
494
+ this.newsletterBenefit = {
495
+ /**
496
+ * The store's configuration, or `null` when none was ever saved.
497
+ *
498
+ * `null` and `{ enabled: false }` are different: never configured, versus
499
+ * configured and switched off. Both mean "offer nothing" to a storefront.
500
+ */
501
+ getSettings: async () => {
502
+ return this.adminRequest(
503
+ "GET",
504
+ "/api/v1/newsletter-benefit/settings"
505
+ );
506
+ },
507
+ /**
508
+ * Create or replace the offer.
509
+ *
510
+ * ⛔ A FULL REPLACEMENT, not a patch. Every field is written, so a field you
511
+ * omit is cleared rather than kept.
512
+ *
513
+ * Saving never rewrites a promise already made: signups still waiting for a
514
+ * confirmation click keep the terms they were shown, and coupons already
515
+ * issued are untouched. Switching `enabled` off stops new offers and leaves
516
+ * every issued coupon working until it expires.
517
+ *
518
+ * @example
519
+ * ```typescript
520
+ * await brainerce.newsletterBenefit.updateSettings({
521
+ * enabled: true,
522
+ * discountType: 'PERCENTAGE',
523
+ * discountValue: 10,
524
+ * minimumOrderAmount: 200,
525
+ * combinesWithOther: false,
526
+ * validityDays: 7,
527
+ * eligibilityTtlHours: 168,
528
+ * firstOrderOnly: true,
529
+ * content: { he: { headline: '10% הנחה על ההזמנה הראשונה' } },
530
+ * });
531
+ * ```
532
+ */
533
+ updateSettings: async (input) => {
534
+ return this.adminRequest(
535
+ "PUT",
536
+ "/api/v1/newsletter-benefit/settings",
537
+ input
538
+ );
539
+ },
540
+ /**
541
+ * Issued benefits, newest first, as `{ data, meta }`.
542
+ *
543
+ * ⛔ NO EMAIL FILTER — the API refuses the parameter. Filter the page you
544
+ * get back rather than asking the server about one address.
545
+ */
546
+ listGrants: async (params = {}) => {
547
+ return this.adminRequest(
548
+ "GET",
549
+ "/api/v1/newsletter-benefit/grants",
550
+ void 0,
551
+ {
552
+ page: params.page,
553
+ limit: params.limit,
554
+ status: params.status,
555
+ from: params.from,
556
+ to: params.to
557
+ }
558
+ );
559
+ },
560
+ /**
561
+ * Re-send one benefit that went astray.
562
+ *
563
+ * ⛔ SENDS THE SAME CODE. It never mints a second coupon, so a support
564
+ * ticket cannot become two discounts. For a benefit whose issuance failed
565
+ * before any coupon existed, this retries the issuance and mails the result.
566
+ *
567
+ * Rejects a signup that has not been confirmed and one that lapsed before a
568
+ * coupon was minted: there is nothing to re-send in either case, and
569
+ * nothing that may be created.
570
+ */
571
+ resend: async (grantId) => {
572
+ return this.adminRequest(
573
+ "POST",
574
+ `/api/v1/newsletter-benefit/grants/${encodePathSegment(grantId)}/resend`
575
+ );
415
576
  }
416
577
  };
417
578
  // -------------------- Stock alerts --------------------
@@ -2505,8 +2666,13 @@ var _BrainerceClient = class _BrainerceClient {
2505
2666
  *
2506
2667
  * Rejected: a product that is not a KIT, a component from another store, a
2507
2668
  * component that is itself a KIT, a VARIABLE component with no variant
2508
- * pinned, a variant that does not belong to its product, and the same slot
2509
- * listed twice.
2669
+ * pinned, a variant that does not belong to its product, the same slot
2670
+ * listed twice, and a component whose product or pinned variant is not
2671
+ * published.
2672
+ *
2673
+ * That last one is checked over the WHOLE list you send, not just the rows
2674
+ * you changed. Once a product already inside a kit is unpublished, no edit
2675
+ * to that kit saves until you publish it again or drop it from the list.
2510
2676
  *
2511
2677
  * @example
2512
2678
  * ```typescript
@@ -2795,6 +2961,79 @@ var _BrainerceClient = class _BrainerceClient {
2795
2961
  async updateOrder(orderId, data) {
2796
2962
  return this.request("PATCH", `/api/v1/orders/${encodePathSegment(orderId)}`, data);
2797
2963
  }
2964
+ /**
2965
+ * List the store's order custom field definitions.
2966
+ *
2967
+ * Call this before writing values: the `key` of each definition is what
2968
+ * {@link setOrderCustomFieldValues} accepts, and `type` is what a value has
2969
+ * to fit. Inactive definitions are included, so you can tell "the merchant
2970
+ * turned this field off" apart from "the merchant never created it".
2971
+ *
2972
+ * Requires an API key with the `orders:read` scope.
2973
+ */
2974
+ async getOrderCustomFieldDefinitions() {
2975
+ return this.adminRequest("GET", "/api/v1/order-custom-fields");
2976
+ }
2977
+ /**
2978
+ * Read the custom field values stored on one order.
2979
+ *
2980
+ * Requires an API key with the `orders:read` scope.
2981
+ */
2982
+ async getOrderCustomFieldValues(orderId) {
2983
+ return this.adminRequest(
2984
+ "GET",
2985
+ `/api/v1/orders/${encodePathSegment(orderId)}/custom-fields`
2986
+ );
2987
+ }
2988
+ /**
2989
+ * Write custom field values onto an order.
2990
+ *
2991
+ * This is how work that finishes OUTSIDE Brainerce gets back onto the order
2992
+ * it belongs to. Subscribe to the `order.paid` webhook, call whatever third
2993
+ * party issues the thing you sell — a licence key, a booking reference, a
2994
+ * warranty number — then write the answer here. The value travels to the
2995
+ * merchant's own order email templates as `orderCustomFields` and, when the
2996
+ * definition is `isPublic`, to the customer's own order page. No email
2997
+ * template or endpoint has to be built per integration.
2998
+ *
2999
+ * ⛔ No default template PRINTS `orderCustomFields`. The variable reaches
3000
+ * every order email, but until the merchant adds the block to their template
3001
+ * once, a value written here is invisible to the customer. Writing the field
3002
+ * is not the same as the customer being told.
3003
+ *
3004
+ * The write is a MERGE: keys you leave out keep their current value, and
3005
+ * `null` clears a field that is not required. Values are coerced to the
3006
+ * definition's type and rejected with a 400 when they cannot be — but a key
3007
+ * with no active definition on the store is IGNORED rather than failing the
3008
+ * whole call, so read the returned `fields` to confirm what was stored.
3009
+ *
3010
+ * Pass an `idempotencyKey` when the caller may retry: an identical re-send
3011
+ * then replays the original response instead of writing again.
3012
+ *
3013
+ * Requires an API key with the `orders:write` scope.
3014
+ *
3015
+ * @example
3016
+ * ```typescript
3017
+ * // after the third party answered
3018
+ * await client.setOrderCustomFieldValues(
3019
+ * order.id,
3020
+ * { licence_key: 'ABCD-EFGH-IJKL' },
3021
+ * { idempotencyKey: `licence-${order.id}` }
3022
+ * );
3023
+ * // fires the "order completed" email, which carries the field
3024
+ * await client.updateOrder(order.id, { status: 'COMPLETED' });
3025
+ * ```
3026
+ */
3027
+ async setOrderCustomFieldValues(orderId, fields, options) {
3028
+ return this.adminRequest(
3029
+ "PATCH",
3030
+ `/api/v1/orders/${encodePathSegment(orderId)}/custom-fields`,
3031
+ { fields },
3032
+ void 0,
3033
+ "json",
3034
+ this.idempotencyHeaders(options)
3035
+ );
3036
+ }
2798
3037
  /**
2799
3038
  * Update order status.
2800
3039
  *
@@ -3111,6 +3350,13 @@ var _BrainerceClient = class _BrainerceClient {
3111
3350
  * exist and 404'd silently. The live route is product-scoped:
3112
3351
  * `GET /api/v1/products/:id/inventory`. A product with no inventory row
3113
3352
  * reads back as all zeroes rather than 404ing.
3353
+ *
3354
+ * The response carries the whole {@link ProductInventoryResponse} — the
3355
+ * `InventoryItem` id, `trackingMode`, `backorderMode`, `backorderLimit`,
3356
+ * `lowStockThreshold`, `lastInventorySyncAt` and `updatedAt` alongside the
3357
+ * three counters. It always did; only the three counters were declared.
3358
+ * On the all-zeroes no-row branch everything but the counters is absent,
3359
+ * so test `id` rather than expecting a 404.
3114
3360
  */
3115
3361
  async getInventory(productId) {
3116
3362
  return this.adminRequest(
@@ -11094,6 +11340,13 @@ var ALLOWED_PAYMENT_HOSTS = [
11094
11340
  // reachable, so a terminal configured against it keeps working.
11095
11341
  "pay.hyp.co.il",
11096
11342
  "icom.yaad.net",
11343
+ // iCredit (ריווחית) — the hosted payment page returned by
11344
+ // PaymentPageRequest. Both environments are listed EXPLICITLY rather than
11345
+ // allowing `rivhit.co.il`: the matcher below also accepts `*.<host>`, so a
11346
+ // bare parent entry would open every Rivhit subdomain (their accounting app,
11347
+ // marketing site, anything they add later) to a payment redirect.
11348
+ "icredit.rivhit.co.il",
11349
+ "testicredit.rivhit.co.il",
11097
11350
  // Brainerce-hosted payment embeds (backend payment-embed proxy at
11098
11351
  // `/api/payment/embed/...` that fronts provider apps' embed shells —
11099
11352
  // e.g. cardcom-payments OpenFields wrapper). The match also covers
package/dist/index.mjs CHANGED
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
115
115
  }
116
116
 
117
117
  // src/version.ts
118
- var SDK_VERSION = "2.5.0";
118
+ var SDK_VERSION = "2.8.0";
119
119
 
120
120
  // src/client.ts
121
121
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -292,10 +292,14 @@ var _BrainerceClient = class _BrainerceClient {
292
292
  * Storefront (public) and vibe-coded modes only. Rate-limited server-side to
293
293
  * 3 requests / 60s per IP, plus one confirmation email per address per 24h.
294
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.
295
+ * **Where the discount goes.** Configure the newsletter welcome offer and the
296
+ * platform issues the coupon itself: read it with `marketing.getBenefit()`,
297
+ * show those terms beside the field, and stop there.
298
+ *
299
+ * ⛔ DO NOT SHOW A CODE AFTER THIS CALL RESOLVES. No coupon exists yet. It is
300
+ * minted when the recipient clicks the confirmation link, and it is mailed to
301
+ * them at that moment — a code rendered here is a code that was never issued.
302
+ * Say "check your email", the same as for the subscription itself.
299
303
  *
300
304
  * @example
301
305
  * ```typescript
@@ -323,6 +327,163 @@ var _BrainerceClient = class _BrainerceClient {
323
327
  "/marketing/subscribe",
324
328
  input
325
329
  );
330
+ },
331
+ /**
332
+ * The welcome offer to render beside the signup field, or `null` when this
333
+ * store offers none.
334
+ *
335
+ * Show the discount, how long the coupon lasts, any minimum order, whether
336
+ * it is first-order only, and the merchant's own headline and terms. Then
337
+ * post to `marketing.subscribe()` and tell the shopper to check their
338
+ * inbox.
339
+ *
340
+ * ⛔ THE COUPON DOES NOT EXIST YET at any point in that sequence. It is
341
+ * created when the recipient clicks the confirmation link in their email,
342
+ * and it is mailed to them there. Rendering a code on this screen renders a
343
+ * code nobody was issued.
344
+ *
345
+ * ⛔ Takes no email address and returns nothing about any individual, on
346
+ * purpose. There is no "has this person already claimed" call, because an
347
+ * unauthenticated one would be an oracle for who shops here. If you need to
348
+ * discourage a repeat signup, say the offer is one per address; do not try
349
+ * to detect it.
350
+ *
351
+ * `null` is the common case on a store that never set this up, so handle it
352
+ * rather than assuming the object. Cache it per page load: it belongs to
353
+ * the store, not to the visitor.
354
+ *
355
+ * Storefront (public) and vibe-coded modes.
356
+ *
357
+ * @param locale - Storefront locale, e.g. `"he"`. Picks the language of the
358
+ * headline and terms; falls back to the store language when omitted.
359
+ *
360
+ * @example
361
+ * ```typescript
362
+ * const offer = await brainerce.marketing.getBenefit('he');
363
+ * if (offer) {
364
+ * // "10% הנחה על ההזמנה הראשונה"
365
+ * render(offer.headline ?? defaultHeadline(offer), offer.terms);
366
+ * }
367
+ * await brainerce.marketing.subscribe({ email, locale: 'he', honeypot });
368
+ * // → "בדקו את המייל שלכם" — never a coupon code
369
+ * ```
370
+ */
371
+ getBenefit: async (locale) => {
372
+ const query = locale ? { locale } : void 0;
373
+ if (this.isVibeCodedMode()) {
374
+ return this.vibeCodedRequest(
375
+ "GET",
376
+ "/newsletter-benefit",
377
+ void 0,
378
+ query
379
+ );
380
+ }
381
+ return this.storefrontRequest(
382
+ "GET",
383
+ "/newsletter-benefit",
384
+ void 0,
385
+ query
386
+ );
387
+ }
388
+ };
389
+ // -------------------- Newsletter signup benefit (Admin) --------------------
390
+ /**
391
+ * Manage the newsletter welcome offer: the terms merchants configure, and the
392
+ * benefits that offer has produced.
393
+ *
394
+ * Admin mode (`apiKey`) only, on the `coupons:read` / `coupons:write` scopes.
395
+ * The benefit IS a coupon feature — it mints a Coupon row and the coupon
396
+ * machinery enforces it — so it carries no scope of its own.
397
+ *
398
+ * ⛔ THERE IS NO "ISSUE A BENEFIT TO THIS ADDRESS" CALL, and there will not
399
+ * be one. A benefit exists because someone submitted the signup form AND
400
+ * clicked the confirmation link; handing one out directly would skip the
401
+ * consent the double opt-in exists to collect and break the one-per-address
402
+ * guarantee that the grant's unique constraint provides. `resend` re-sends a
403
+ * code that already exists; it never creates one.
404
+ */
405
+ this.newsletterBenefit = {
406
+ /**
407
+ * The store's configuration, or `null` when none was ever saved.
408
+ *
409
+ * `null` and `{ enabled: false }` are different: never configured, versus
410
+ * configured and switched off. Both mean "offer nothing" to a storefront.
411
+ */
412
+ getSettings: async () => {
413
+ return this.adminRequest(
414
+ "GET",
415
+ "/api/v1/newsletter-benefit/settings"
416
+ );
417
+ },
418
+ /**
419
+ * Create or replace the offer.
420
+ *
421
+ * ⛔ A FULL REPLACEMENT, not a patch. Every field is written, so a field you
422
+ * omit is cleared rather than kept.
423
+ *
424
+ * Saving never rewrites a promise already made: signups still waiting for a
425
+ * confirmation click keep the terms they were shown, and coupons already
426
+ * issued are untouched. Switching `enabled` off stops new offers and leaves
427
+ * every issued coupon working until it expires.
428
+ *
429
+ * @example
430
+ * ```typescript
431
+ * await brainerce.newsletterBenefit.updateSettings({
432
+ * enabled: true,
433
+ * discountType: 'PERCENTAGE',
434
+ * discountValue: 10,
435
+ * minimumOrderAmount: 200,
436
+ * combinesWithOther: false,
437
+ * validityDays: 7,
438
+ * eligibilityTtlHours: 168,
439
+ * firstOrderOnly: true,
440
+ * content: { he: { headline: '10% הנחה על ההזמנה הראשונה' } },
441
+ * });
442
+ * ```
443
+ */
444
+ updateSettings: async (input) => {
445
+ return this.adminRequest(
446
+ "PUT",
447
+ "/api/v1/newsletter-benefit/settings",
448
+ input
449
+ );
450
+ },
451
+ /**
452
+ * Issued benefits, newest first, as `{ data, meta }`.
453
+ *
454
+ * ⛔ NO EMAIL FILTER — the API refuses the parameter. Filter the page you
455
+ * get back rather than asking the server about one address.
456
+ */
457
+ listGrants: async (params = {}) => {
458
+ return this.adminRequest(
459
+ "GET",
460
+ "/api/v1/newsletter-benefit/grants",
461
+ void 0,
462
+ {
463
+ page: params.page,
464
+ limit: params.limit,
465
+ status: params.status,
466
+ from: params.from,
467
+ to: params.to
468
+ }
469
+ );
470
+ },
471
+ /**
472
+ * Re-send one benefit that went astray.
473
+ *
474
+ * ⛔ SENDS THE SAME CODE. It never mints a second coupon, so a support
475
+ * ticket cannot become two discounts. For a benefit whose issuance failed
476
+ * before any coupon existed, this retries the issuance and mails the result.
477
+ *
478
+ * Rejects a signup that has not been confirmed and one that lapsed before a
479
+ * coupon was minted: there is nothing to re-send in either case, and
480
+ * nothing that may be created.
481
+ */
482
+ resend: async (grantId) => {
483
+ return this.adminRequest(
484
+ "POST",
485
+ `/api/v1/newsletter-benefit/grants/${encodePathSegment(grantId)}/resend`
486
+ );
326
487
  }
327
488
  };
328
489
  // -------------------- Stock alerts --------------------
@@ -2416,8 +2577,13 @@ var _BrainerceClient = class _BrainerceClient {
2416
2577
  *
2417
2578
  * Rejected: a product that is not a KIT, a component from another store, a
2418
2579
  * component that is itself a KIT, a VARIABLE component with no variant
2419
- * pinned, a variant that does not belong to its product, and the same slot
2420
- * listed twice.
2580
+ * pinned, a variant that does not belong to its product, the same slot
2581
+ * listed twice, and a component whose product or pinned variant is not
2582
+ * published.
2583
+ *
2584
+ * That last one is checked over the WHOLE list you send, not just the rows
2585
+ * you changed. Once a product already inside a kit is unpublished, no edit
2586
+ * to that kit saves until you publish it again or drop it from the list.
2421
2587
  *
2422
2588
  * @example
2423
2589
  * ```typescript
@@ -2706,6 +2872,79 @@ var _BrainerceClient = class _BrainerceClient {
2706
2872
  async updateOrder(orderId, data) {
2707
2873
  return this.request("PATCH", `/api/v1/orders/${encodePathSegment(orderId)}`, data);
2708
2874
  }
2875
+ /**
2876
+ * List the store's order custom field definitions.
2877
+ *
2878
+ * Call this before writing values: the `key` of each definition is what
2879
+ * {@link setOrderCustomFieldValues} accepts, and `type` is what a value has
2880
+ * to fit. Inactive definitions are included, so you can tell "the merchant
2881
+ * turned this field off" apart from "the merchant never created it".
2882
+ *
2883
+ * Requires an API key with the `orders:read` scope.
2884
+ */
2885
+ async getOrderCustomFieldDefinitions() {
2886
+ return this.adminRequest("GET", "/api/v1/order-custom-fields");
2887
+ }
2888
+ /**
2889
+ * Read the custom field values stored on one order.
2890
+ *
2891
+ * Requires an API key with the `orders:read` scope.
2892
+ */
2893
+ async getOrderCustomFieldValues(orderId) {
2894
+ return this.adminRequest(
2895
+ "GET",
2896
+ `/api/v1/orders/${encodePathSegment(orderId)}/custom-fields`
2897
+ );
2898
+ }
2899
+ /**
2900
+ * Write custom field values onto an order.
2901
+ *
2902
+ * This is how work that finishes OUTSIDE Brainerce gets back onto the order
2903
+ * it belongs to. Subscribe to the `order.paid` webhook, call whatever third
2904
+ * party issues the thing you sell — a licence key, a booking reference, a
2905
+ * warranty number — then write the answer here. The value travels to the
2906
+ * merchant's own order email templates as `orderCustomFields` and, when the
2907
+ * definition is `isPublic`, to the customer's own order page. No email
2908
+ * template or endpoint has to be built per integration.
2909
+ *
2910
+ * ⛔ No default template PRINTS `orderCustomFields`. The variable reaches
2911
+ * every order email, but until the merchant adds the block to their template
2912
+ * once, a value written here is invisible to the customer. Writing the field
2913
+ * is not the same as the customer being told.
2914
+ *
2915
+ * The write is a MERGE: keys you leave out keep their current value, and
2916
+ * `null` clears a field that is not required. Values are coerced to the
2917
+ * definition's type and rejected with a 400 when they cannot be — but a key
2918
+ * with no active definition on the store is IGNORED rather than failing the
2919
+ * whole call, so read the returned `fields` to confirm what was stored.
2920
+ *
2921
+ * Pass an `idempotencyKey` when the caller may retry: an identical re-send
2922
+ * then replays the original response instead of writing again.
2923
+ *
2924
+ * Requires an API key with the `orders:write` scope.
2925
+ *
2926
+ * @example
2927
+ * ```typescript
2928
+ * // after the third party answered
2929
+ * await client.setOrderCustomFieldValues(
2930
+ * order.id,
2931
+ * { licence_key: 'ABCD-EFGH-IJKL' },
2932
+ * { idempotencyKey: `licence-${order.id}` }
2933
+ * );
2934
+ * // fires the "order completed" email, which carries the field
2935
+ * await client.updateOrder(order.id, { status: 'COMPLETED' });
2936
+ * ```
2937
+ */
2938
+ async setOrderCustomFieldValues(orderId, fields, options) {
2939
+ return this.adminRequest(
2940
+ "PATCH",
2941
+ `/api/v1/orders/${encodePathSegment(orderId)}/custom-fields`,
2942
+ { fields },
2943
+ void 0,
2944
+ "json",
2945
+ this.idempotencyHeaders(options)
2946
+ );
2947
+ }
2709
2948
  /**
2710
2949
  * Update order status.
2711
2950
  *
@@ -3022,6 +3261,13 @@ var _BrainerceClient = class _BrainerceClient {
3022
3261
  * exist and 404'd silently. The live route is product-scoped:
3023
3262
  * `GET /api/v1/products/:id/inventory`. A product with no inventory row
3024
3263
  * reads back as all zeroes rather than 404ing.
3264
+ *
3265
+ * The response carries the whole {@link ProductInventoryResponse} — the
3266
+ * `InventoryItem` id, `trackingMode`, `backorderMode`, `backorderLimit`,
3267
+ * `lowStockThreshold`, `lastInventorySyncAt` and `updatedAt` alongside the
3268
+ * three counters. It always did; only the three counters were declared.
3269
+ * On the all-zeroes no-row branch everything but the counters is absent,
3270
+ * so test `id` rather than expecting a 404.
3025
3271
  */
3026
3272
  async getInventory(productId) {
3027
3273
  return this.adminRequest(
@@ -11005,6 +11251,13 @@ var ALLOWED_PAYMENT_HOSTS = [
11005
11251
  // reachable, so a terminal configured against it keeps working.
11006
11252
  "pay.hyp.co.il",
11007
11253
  "icom.yaad.net",
11254
+ // iCredit (ריווחית) — the hosted payment page returned by
11255
+ // PaymentPageRequest. Both environments are listed EXPLICITLY rather than
11256
+ // allowing `rivhit.co.il`: the matcher below also accepts `*.<host>`, so a
11257
+ // bare parent entry would open every Rivhit subdomain (their accounting app,
11258
+ // marketing site, anything they add later) to a payment redirect.
11259
+ "icredit.rivhit.co.il",
11260
+ "testicredit.rivhit.co.il",
11008
11261
  // Brainerce-hosted payment embeds (backend payment-embed proxy at
11009
11262
  // `/api/payment/embed/...` that fronts provider apps' embed shells —
11010
11263
  // e.g. cardcom-payments OpenFields wrapper). The match also covers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "2.5.0",
3
+ "version": "2.8.0",
4
4
  "description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",