brainerce 1.16.0 → 1.18.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
@@ -386,6 +386,9 @@ var BrainerceClient = class {
386
386
  if (this.origin) {
387
387
  headers["Origin"] = this.origin;
388
388
  }
389
+ if (this.locale) {
390
+ headers["Accept-Language"] = this.locale;
391
+ }
389
392
  const response = await fetch(url.toString(), {
390
393
  method,
391
394
  headers,
@@ -421,7 +424,7 @@ var BrainerceClient = class {
421
424
  /**
422
425
  * Make a request to the Vibe-Coded API (public, uses connectionId)
423
426
  */
424
- async vibeCodedRequest(method, path, body, queryParams) {
427
+ async vibeCodedRequest(method, path, body, queryParams, headerOverrides) {
425
428
  if (!this.connectionId) {
426
429
  throw new BrainerceError("connectionId is required for vibe-coded requests", 400);
427
430
  }
@@ -444,6 +447,12 @@ var BrainerceClient = class {
444
447
  if (this.origin) {
445
448
  headers["Origin"] = this.origin;
446
449
  }
450
+ if (this.locale) {
451
+ headers["Accept-Language"] = this.locale;
452
+ }
453
+ if (headerOverrides) {
454
+ Object.assign(headers, headerOverrides);
455
+ }
447
456
  if (this.proxyMode && method !== "GET") {
448
457
  headers["X-Requested-With"] = "brainerce";
449
458
  }
@@ -492,7 +501,7 @@ var BrainerceClient = class {
492
501
  /**
493
502
  * Make a request to the Storefront API (public, uses storeId)
494
503
  */
495
- async storefrontRequest(method, path, body, queryParams) {
504
+ async storefrontRequest(method, path, body, queryParams, headerOverrides) {
496
505
  if (!this.storeId) {
497
506
  throw new BrainerceError("storeId is required for storefront requests", 400);
498
507
  }
@@ -515,6 +524,12 @@ var BrainerceClient = class {
515
524
  if (this.origin) {
516
525
  headers["Origin"] = this.origin;
517
526
  }
527
+ if (this.locale) {
528
+ headers["Accept-Language"] = this.locale;
529
+ }
530
+ if (headerOverrides) {
531
+ Object.assign(headers, headerOverrides);
532
+ }
518
533
  if (this.customerToken) {
519
534
  headers["Authorization"] = `Bearer ${this.customerToken}`;
520
535
  }
@@ -616,7 +631,6 @@ var BrainerceClient = class {
616
631
  maxPrice: params?.maxPrice,
617
632
  sortBy: params?.sortBy,
618
633
  sortOrder: params?.sortOrder,
619
- locale: params?.locale || this.locale,
620
634
  // Admin-only params
621
635
  type: params?.type
622
636
  };
@@ -648,13 +662,14 @@ var BrainerceClient = class {
648
662
  * Works in vibe-coded, storefront (public), and admin mode
649
663
  */
650
664
  async getProduct(productId, options) {
651
- const localeParams = { locale: options?.locale || this.locale };
665
+ const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
652
666
  if (this.isVibeCodedMode()) {
653
667
  return this.vibeCodedRequest(
654
668
  "GET",
655
669
  `/products/${productId}`,
656
670
  void 0,
657
- localeParams
671
+ void 0,
672
+ headerOverrides
658
673
  );
659
674
  }
660
675
  if (this.storeId && !this.apiKey) {
@@ -662,7 +677,8 @@ var BrainerceClient = class {
662
677
  "GET",
663
678
  `/products/${productId}`,
664
679
  void 0,
665
- localeParams
680
+ void 0,
681
+ headerOverrides
666
682
  );
667
683
  }
668
684
  return this.adminRequest("GET", `/api/v1/products/${productId}`);
@@ -678,13 +694,14 @@ var BrainerceClient = class {
678
694
  * ```
679
695
  */
680
696
  async getProductBySlug(slug, options) {
681
- const localeParams = { locale: options?.locale || this.locale };
697
+ const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
682
698
  if (this.isVibeCodedMode()) {
683
699
  return this.vibeCodedRequest(
684
700
  "GET",
685
701
  `/products/slug/${slug}`,
686
702
  void 0,
687
- localeParams
703
+ void 0,
704
+ headerOverrides
688
705
  );
689
706
  }
690
707
  if (this.storeId && !this.apiKey) {
@@ -692,7 +709,8 @@ var BrainerceClient = class {
692
709
  "GET",
693
710
  `/products/slug/${slug}`,
694
711
  void 0,
695
- localeParams
712
+ void 0,
713
+ headerOverrides
696
714
  );
697
715
  }
698
716
  return this.adminRequest("GET", `/api/v1/products/by-slug/${slug}`);
@@ -708,9 +726,9 @@ var BrainerceClient = class {
708
726
  * ```
709
727
  */
710
728
  async getCategories(options) {
711
- const localeParams = { locale: options?.locale || this.locale };
729
+ const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
712
730
  if (this.isVibeCodedMode()) {
713
- return this.vibeCodedRequest("GET", "/categories", void 0, localeParams);
731
+ return this.vibeCodedRequest("GET", "/categories", void 0, void 0, headerOverrides);
714
732
  }
715
733
  throw new BrainerceError("getCategories is only available in vibe-coded mode", 400);
716
734
  }
@@ -725,9 +743,9 @@ var BrainerceClient = class {
725
743
  * ```
726
744
  */
727
745
  async getBrands(options) {
728
- const localeParams = { locale: options?.locale || this.locale };
746
+ const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
729
747
  if (this.isVibeCodedMode()) {
730
- return this.vibeCodedRequest("GET", "/brands", void 0, localeParams);
748
+ return this.vibeCodedRequest("GET", "/brands", void 0, void 0, headerOverrides);
731
749
  }
732
750
  throw new BrainerceError("getBrands is only available in vibe-coded mode", 400);
733
751
  }
@@ -741,11 +759,10 @@ var BrainerceClient = class {
741
759
  * // Use tag IDs in getProducts({ tags: ['tag_id'] })
742
760
  * ```
743
761
  */
744
- async getTags() {
762
+ async getTags(options) {
763
+ const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
745
764
  if (this.isVibeCodedMode()) {
746
- return this.vibeCodedRequest("GET", "/tags", void 0, {
747
- locale: this.locale
748
- });
765
+ return this.vibeCodedRequest("GET", "/tags", void 0, void 0, headerOverrides);
749
766
  }
750
767
  throw new BrainerceError("getTags is only available in vibe-coded mode", 400);
751
768
  }
@@ -789,7 +806,7 @@ var BrainerceClient = class {
789
806
  if (!query || query.trim().length === 0) {
790
807
  return { products: [], categories: [] };
791
808
  }
792
- const queryParams = { q: query, limit, locale: this.locale };
809
+ const queryParams = { q: query, limit };
793
810
  if (this.isVibeCodedMode()) {
794
811
  return this.vibeCodedRequest(
795
812
  "GET",
@@ -2095,30 +2112,82 @@ var BrainerceClient = class {
2095
2112
  }
2096
2113
  /**
2097
2114
  * Get a cart by ID
2115
+ *
2116
+ * @param cartId - The cart ID
2117
+ * @param options - Optional settings. Use `include` to fetch related data in a single request.
2118
+ *
2119
+ * @example
2120
+ * ```typescript
2121
+ * // Basic cart fetch
2122
+ * const cart = await client.getCart('cart_123');
2123
+ *
2124
+ * // Fetch cart with recommendations, upgrades, and bundles in one call
2125
+ * const enriched = await client.getCart('cart_123', {
2126
+ * include: ['recommendations', 'upgrades', 'bundles'],
2127
+ * });
2128
+ * console.log(enriched.recommendations); // { recommendations: [...] }
2129
+ * console.log(enriched.upgrades); // { upgrades: { ... } }
2130
+ * console.log(enriched.bundles); // { bundles: [...] }
2131
+ * ```
2098
2132
  */
2099
- async getCart(cartId) {
2133
+ async getCart(cartId, options) {
2100
2134
  if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
2101
2135
  console.warn('getCart("__local__") is deprecated. Use smartGetCart() instead.');
2102
2136
  return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
2103
2137
  }
2138
+ const queryParams = options?.include?.length ? { include: options.include.join(",") } : void 0;
2104
2139
  if (this.isVibeCodedMode()) {
2105
- return this.withGuards(this.vibeCodedRequest("GET", `/cart/${cartId}`), "cart");
2140
+ return this.withGuards(
2141
+ this.vibeCodedRequest("GET", `/cart/${cartId}`, void 0, queryParams),
2142
+ "cart"
2143
+ );
2106
2144
  }
2107
2145
  if (this.storeId && !this.apiKey) {
2108
- return this.withGuards(this.storefrontRequest("GET", `/cart/${cartId}`), "cart");
2146
+ return this.withGuards(
2147
+ this.storefrontRequest("GET", `/cart/${cartId}`, void 0, queryParams),
2148
+ "cart"
2149
+ );
2109
2150
  }
2110
- return this.withGuards(this.adminRequest("GET", `/api/v1/cart/${cartId}`), "cart");
2151
+ return this.withGuards(
2152
+ this.adminRequest("GET", `/api/v1/cart/${cartId}`, void 0, queryParams),
2153
+ "cart"
2154
+ );
2111
2155
  }
2112
2156
  /**
2113
- * Add an item to the cart
2157
+ * Add an item to the cart.
2158
+ *
2159
+ * If the product has `customizationFields` (merchant-defined buyer input
2160
+ * like engraving text, uploaded photos, select options), pass the buyer's
2161
+ * values in `metadata`. Keys must match each field's `key`. For `IMAGE` /
2162
+ * `GALLERY` types, upload the file first via `uploadCustomizationFile()`
2163
+ * and pass the returned URL. The server validates every value against its
2164
+ * `MetafieldDefinition` and rejects the request with HTTP 400 if anything
2165
+ * fails (type mismatch, required missing, not in `enumValues`, etc.).
2166
+ *
2167
+ * Values are snapshotted onto the order line at checkout — later edits to
2168
+ * the field definition do not retroactively change existing orders.
2114
2169
  *
2115
2170
  * @example
2116
2171
  * ```typescript
2172
+ * // Product with no customization fields
2117
2173
  * const cart = await client.addToCart('cart_123', {
2118
2174
  * productId: 'prod_abc',
2119
2175
  * quantity: 2,
2120
2176
  * notes: 'Gift wrap please',
2121
2177
  * });
2178
+ *
2179
+ * // Product WITH customization fields (engraving + photo + select + multi)
2180
+ * const { url: photoUrl } = await client.uploadCustomizationFile(file);
2181
+ * const cart = await client.addToCart('cart_123', {
2182
+ * productId: 'prod_mug',
2183
+ * quantity: 1,
2184
+ * metadata: {
2185
+ * engraving_text: 'Happy Birthday!', // TEXT
2186
+ * frame_color: 'Gold', // SELECT (must be in enumValues)
2187
+ * upload_photo: photoUrl, // IMAGE
2188
+ * addons: ['Gift wrap'], // MULTI_SELECT (always array)
2189
+ * },
2190
+ * });
2122
2191
  * ```
2123
2192
  */
2124
2193
  async addToCart(cartId, item) {
@@ -2451,17 +2520,21 @@ var BrainerceClient = class {
2451
2520
  * ```
2452
2521
  */
2453
2522
  async getProductRecommendations(productId, type) {
2454
- const params = type ? `?type=${type}` : "";
2523
+ const queryParams = type ? { type } : void 0;
2455
2524
  if (this.isVibeCodedMode()) {
2456
2525
  return this.vibeCodedRequest(
2457
2526
  "GET",
2458
- `/products/${productId}/recommendations${params}`
2527
+ `/products/${productId}/recommendations`,
2528
+ void 0,
2529
+ queryParams
2459
2530
  );
2460
2531
  }
2461
2532
  if (this.storeId && !this.apiKey) {
2462
2533
  return this.storefrontRequest(
2463
2534
  "GET",
2464
- `/products/${productId}/recommendations${params}`
2535
+ `/products/${productId}/recommendations`,
2536
+ void 0,
2537
+ queryParams
2465
2538
  );
2466
2539
  }
2467
2540
  throw new BrainerceError(
@@ -2485,17 +2558,21 @@ var BrainerceClient = class {
2485
2558
  * ```
2486
2559
  */
2487
2560
  async getCartRecommendations(cartId, limit) {
2488
- const params = limit ? `?limit=${limit}` : "";
2561
+ const queryParams = limit ? { limit } : void 0;
2489
2562
  if (this.isVibeCodedMode()) {
2490
2563
  return this.vibeCodedRequest(
2491
2564
  "GET",
2492
- `/cart/${cartId}/recommendations${params}`
2565
+ `/cart/${cartId}/recommendations`,
2566
+ void 0,
2567
+ queryParams
2493
2568
  );
2494
2569
  }
2495
2570
  if (this.storeId && !this.apiKey) {
2496
2571
  return this.storefrontRequest(
2497
2572
  "GET",
2498
- `/cart/${cartId}/recommendations${params}`
2573
+ `/cart/${cartId}/recommendations`,
2574
+ void 0,
2575
+ queryParams
2499
2576
  );
2500
2577
  }
2501
2578
  throw new BrainerceError(
@@ -2758,11 +2835,21 @@ var BrainerceClient = class {
2758
2835
  * Uses promise dedup lock to prevent race conditions on parallel calls.
2759
2836
  * @internal
2760
2837
  */
2761
- async getOrCreateSessionCart() {
2762
- if (this._sessionCartPromise) return this._sessionCartPromise;
2838
+ async getOrCreateSessionCart(options) {
2839
+ if (this._sessionCartPromise) {
2840
+ const base = await this._sessionCartPromise;
2841
+ if (options?.include?.length && base.id) {
2842
+ return this.getCart(base.id, options);
2843
+ }
2844
+ return base;
2845
+ }
2763
2846
  this._sessionCartPromise = this._getOrCreateSessionCartImpl();
2764
2847
  try {
2765
- return await this._sessionCartPromise;
2848
+ const base = await this._sessionCartPromise;
2849
+ if (options?.include?.length && base.id) {
2850
+ return this.getCart(base.id, options);
2851
+ }
2852
+ return base;
2766
2853
  } finally {
2767
2854
  this._sessionCartPromise = null;
2768
2855
  }
@@ -2895,10 +2982,10 @@ var BrainerceClient = class {
2895
2982
  * Caches the cart ID for subsequent calls
2896
2983
  * @internal
2897
2984
  */
2898
- async getOrCreateCustomerCart() {
2985
+ async getOrCreateCustomerCart(options) {
2899
2986
  if (this.customerCartId) {
2900
2987
  try {
2901
- const existingCart = await this.getCart(this.customerCartId);
2988
+ const existingCart = await this.getCart(this.customerCartId, options);
2902
2989
  if (existingCart.status === "ACTIVE") {
2903
2990
  return existingCart;
2904
2991
  }
@@ -2911,6 +2998,9 @@ var BrainerceClient = class {
2911
2998
  const cart2 = await this.fetchCustomerCart();
2912
2999
  if (cart2.status === "ACTIVE") {
2913
3000
  this.customerCartId = cart2.id;
3001
+ if (options?.include?.length) {
3002
+ return this.getCart(cart2.id, options);
3003
+ }
2914
3004
  return cart2;
2915
3005
  }
2916
3006
  } catch {
@@ -2962,7 +3052,8 @@ var BrainerceClient = class {
2962
3052
  const updated = await this.addToCart(cart.id, {
2963
3053
  productId: item.productId,
2964
3054
  variantId: item.variantId,
2965
- quantity: item.quantity
3055
+ quantity: item.quantity,
3056
+ metadata: item.metadata
2966
3057
  });
2967
3058
  return updated;
2968
3059
  } else {
@@ -2970,7 +3061,8 @@ var BrainerceClient = class {
2970
3061
  const updated = await this.addToCart(cart.id, {
2971
3062
  productId: item.productId,
2972
3063
  variantId: item.variantId,
2973
- quantity: item.quantity
3064
+ quantity: item.quantity,
3065
+ metadata: item.metadata
2974
3066
  });
2975
3067
  this.updateSessionCartItemCount(updated.items?.length ?? 0);
2976
3068
  return updated;
@@ -2983,20 +3075,28 @@ var BrainerceClient = class {
2983
3075
  * - **Guest with session**: Returns server-side session cart
2984
3076
  * - **Guest without session**: Returns empty cart (no server call, cart created lazily on add)
2985
3077
  *
3078
+ * @param options - Optional. Use `include` to fetch recommendations, upgrades, and bundles
3079
+ * in a single request instead of separate calls.
3080
+ *
2986
3081
  * @example
2987
3082
  * ```typescript
2988
3083
  * const cart = await client.smartGetCart();
2989
3084
  * console.log('Items:', cart.items.length);
3085
+ *
3086
+ * // With includes — single request for cart + extras
3087
+ * const enriched = await client.smartGetCart({
3088
+ * include: ['recommendations', 'upgrades', 'bundles'],
3089
+ * });
2990
3090
  * ```
2991
3091
  */
2992
- async smartGetCart() {
3092
+ async smartGetCart(options) {
2993
3093
  if (this.isCustomerLoggedIn()) {
2994
- return this.getOrCreateCustomerCart();
3094
+ return this.getOrCreateCustomerCart(options);
2995
3095
  } else {
2996
3096
  if (!this.sessionToken) {
2997
3097
  return this.emptyCart();
2998
3098
  }
2999
- return this.getOrCreateSessionCart();
3099
+ return this.getOrCreateSessionCart(options);
3000
3100
  }
3001
3101
  }
3002
3102
  /**
@@ -5673,9 +5773,35 @@ var BrainerceClient = class {
5673
5773
  );
5674
5774
  }
5675
5775
  /**
5676
- * Upload a file for product customization (e.g., customer logo for printing).
5677
- * Available in storefront and vibe-coded modes.
5678
- * Returns the uploaded file URL and key.
5776
+ * Upload a buyer-submitted file (engraving photo, custom image, etc.) for a
5777
+ * product customization field of type `IMAGE` or `GALLERY`. The returned `url`
5778
+ * is what you pass back in the add-to-cart `metadata` payload.
5779
+ *
5780
+ * Available in storefront and vibe-coded modes — no customer login required.
5781
+ *
5782
+ * Server rules:
5783
+ * - `image/*` MIME only. Other types → HTTP 400.
5784
+ * - Max 5 MB per file.
5785
+ * - Throttled to 10 uploads / minute per IP → HTTP 429.
5786
+ * - Retention: at least 7 days. If the cart never becomes an order, the file
5787
+ * is reclaimed automatically. Once the order exists, the file is kept for
5788
+ * order-history purposes.
5789
+ *
5790
+ * @example
5791
+ * ```ts
5792
+ * // On a product page with IMAGE/GALLERY customization fields:
5793
+ * const { url } = await client.uploadCustomizationFile(fileInput.files[0]);
5794
+ *
5795
+ * // Then include the URL in the add-to-cart metadata:
5796
+ * await client.addToCart(cart.id, {
5797
+ * productId: product.id,
5798
+ * quantity: 1,
5799
+ * metadata: {
5800
+ * engraving_text: 'Happy Birthday!',
5801
+ * upload_photo: url,
5802
+ * },
5803
+ * });
5804
+ * ```
5679
5805
  */
5680
5806
  async uploadCustomizationFile(file) {
5681
5807
  const formData = new FormData();
@@ -5890,7 +6016,9 @@ var BrainerceClient = class {
5890
6016
  return this.adminRequest("PUT", "/api/v1/email/settings", data);
5891
6017
  }
5892
6018
  /**
5893
- * Get all email templates for the store
6019
+ * Get all email templates for the store, grouped by (eventType, language).
6020
+ * Response includes the store's primary language and supported languages so
6021
+ * the caller can render a locale selector.
5894
6022
  * Requires Admin mode (apiKey)
5895
6023
  */
5896
6024
  async getEmailTemplates() {
@@ -6195,6 +6323,19 @@ function getProductPriceInfo(product) {
6195
6323
  if (!product) {
6196
6324
  return { price: 0, originalPrice: 0, isOnSale: false, discountAmount: 0, discountPercent: 0 };
6197
6325
  }
6326
+ if (product.discount) {
6327
+ const ruleOriginal = parseFloat(product.discount.originalPrice) || 0;
6328
+ const ruleDiscounted = parseFloat(product.discount.discountedPrice) || 0;
6329
+ const ruleAmount = Math.max(0, ruleOriginal - ruleDiscounted);
6330
+ const rulePercent = ruleOriginal > 0 ? Math.round(ruleAmount / ruleOriginal * 100) : 0;
6331
+ return {
6332
+ price: ruleDiscounted,
6333
+ originalPrice: ruleOriginal,
6334
+ isOnSale: ruleDiscounted < ruleOriginal,
6335
+ discountAmount: ruleAmount,
6336
+ discountPercent: rulePercent
6337
+ };
6338
+ }
6198
6339
  const basePrice = parseFloat(product.basePrice) || 0;
6199
6340
  const salePrice = product.salePrice ? parseFloat(product.salePrice) : null;
6200
6341
  const isOnSale = salePrice !== null && salePrice < basePrice;