@brainerce/mcp-server 3.1.0 → 3.2.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/bin/stdio.js CHANGED
@@ -65,7 +65,7 @@ function getTypeQuickReference() {
65
65
  | OAuth | \`provider.name\` | \`provider\` | it IS the string: \`'GOOGLE' | 'FACEBOOK' | 'GITHUB'\` |
66
66
  | OAuth | \`oauth.url\` | \`oauth.authorizationUrl\` | |
67
67
  | Description | \`getDescriptionContent()\` | \`{ html } | { text } | null\` | check \`'html' in content\` before use |
68
- | \`smartGetCart()\` | always use \`.id\` | check \`'id' in cart\` | returns \`Cart | LocalCart\` |
68
+ | \`smartGetCart()\` | N/A | \`cart.id\` | returns \`CartWithIncludes\` (extends \`Cart\`); pass \`{ include: [...] }\` for extras |
69
69
  | \`startGuestCheckout()\` | always use \`.checkoutId\` | check \`result.tracked\` | discriminated union |
70
70
 
71
71
  **Rules:**
@@ -73,8 +73,8 @@ function getTypeQuickReference() {
73
73
  - CartItem/CheckoutLineItem = **NESTED** (\`item.product.name\`, \`item.unitPrice\`)
74
74
  - OrderItem = **FLAT** (\`item.name\`, \`item.price\`, \`item.totalPrice\`, \`item.image\`)
75
75
  - Cart has NO \`.total\` \u2014 use \`getCartTotals(cart)\`
76
- - \`getCartTotals()\` only works with server \`Cart\`, NOT \`LocalCart\`
77
- - \`smartGetCart()\` returns \`Cart | LocalCart\` \u2014 check \`'id' in cart\` before using Cart fields
76
+ - \`getCartTotals()\` works with server \`Cart\` (all carts are server-side now)
77
+ - \`smartGetCart()\` returns \`CartWithIncludes\` \u2014 always a server cart; pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` for extras
78
78
  - \`startGuestCheckout()\` returns discriminated union \u2014 check \`result.tracked\` before \`.checkoutId\`
79
79
  - \`SetShippingAddressDto.email\` is **required** \u2014 always include email
80
80
 
@@ -202,7 +202,7 @@ async function startCheckout() {
202
202
  6. Select shipping method \u2192 \`selectShippingMethod()\`
203
203
  6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
204
204
  7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
205
- 8. Branch on \`provider\`: \`'stripe'\` \u2192 Stripe Elements, \`'grow'\` \u2192 iframe, \`'paypal'\` \u2192 PayPal Buttons
205
+ 8. Branch on the **\`clientSdk.renderType\`** returned by \`createPaymentIntent()\` \u2014 NEVER hard-code by provider name. Values: \`'sdk-widget'\` (Stripe, PayPal, Grow), \`'iframe'\` (Cardcom hosted or Brainerce-hosted embed \u2014 detect via URL path), \`'redirect'\`, \`'sandbox'\`, \`'embedded-fields'\` (reserved).
206
206
  9. **Order is created AUTOMATICALLY after payment succeeds (via webhook) \u2014 for ALL providers!**
207
207
 
208
208
  ### Checkout Page Component
@@ -305,8 +305,8 @@ function CheckoutPage() {
305
305
  </div>
306
306
  );
307
307
  }
308
- if (paymentData.provider === 'grow') {
309
- return <GrowPaymentForm paymentUrl={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
308
+ if (paymentData.clientSdk?.renderType === 'iframe') {
309
+ return <PaymentIframe clientSecret={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
310
310
  }
311
311
  if (paymentData.provider === 'paypal' && paypalClientId) {
312
312
  return <PayPalPaymentForm clientId={paypalClientId} orderId={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
@@ -362,26 +362,82 @@ function StripePaymentForm({ checkoutId }: { checkoutId: string }) {
362
362
  }
363
363
  \`\`\`
364
364
 
365
- ### Grow Payment Form (Israeli stores \u2014 iframe based, no SDK needed)
365
+ ### Iframe-Based Providers (Cardcom, legacy Grow, etc.)
366
+
367
+ When \`clientSdk.renderType === 'iframe'\`, the payment intent returns a \`clientSecret\` which is a URL to load in an iframe. There are TWO flavors of iframe rendering you must handle:
368
+
369
+ **Flavor A \u2014 Brainerce-hosted embed (URL path contains \`/embed/\`):** The iframe loads a Brainerce-branded compact form (e.g. Cardcom OpenFields). Render it **INLINE** inside your checkout flow (next to the order summary) \u2014 NO modal, NO dark overlay. The embed page posts messages to resize itself and to request top-level navigation (e.g. for Bit express-pay buttons).
370
+
371
+ **Flavor B \u2014 Provider-hosted page (any other URL):** The iframe loads a full provider-branded page with its own header/chrome. Render inside a **modal overlay** so it doesn't fight your checkout layout.
372
+
373
+ Detect flavor by URL path \u2014 works across localhost/staging/prod without a domain list:
366
374
 
367
375
  \`\`\`typescript
368
- function GrowPaymentForm({ paymentUrl, checkoutId }: { paymentUrl: string; checkoutId: string }) {
369
- const [iframeLoaded, setIframeLoaded] = useState(false);
376
+ function PaymentIframe({ clientSecret, checkoutId }: { clientSecret: string; checkoutId: string }) {
377
+ const [height, setHeight] = useState(540); // default before resize message arrives
378
+ const isBrainerceEmbed = (() => {
379
+ try { return new URL(clientSecret).pathname.includes('/embed/'); }
380
+ catch { return false; }
381
+ })();
382
+
383
+ useEffect(() => {
384
+ function handleMessage(e: MessageEvent) {
385
+ const data = e.data as { type?: string; height?: number; url?: string };
386
+ if (data?.type === 'brainerce:resize' && typeof data.height === 'number') {
387
+ setHeight(data.height);
388
+ }
389
+ if (data?.type === 'brainerce:redirect' && typeof data.url === 'string') {
390
+ // Top-level navigation (e.g. Bit). ALWAYS validate against an allowlist
391
+ // before navigating \u2014 never trust the URL blindly.
392
+ if (isTrustedPaymentUrl(data.url)) { window.top!.location.href = data.url; }
393
+ }
394
+ if (data?.type === 'brainerce:payment-complete') {
395
+ // Payment done \u2014 redirect to confirmation page which verifies server-side
396
+ window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
397
+ }
398
+ }
399
+ window.addEventListener('message', handleMessage);
400
+ return () => window.removeEventListener('message', handleMessage);
401
+ }, [checkoutId]);
402
+
403
+ if (isBrainerceEmbed) {
404
+ // Inline: part of the checkout flow, no overlay
405
+ return (
406
+ <div className="w-full">
407
+ <iframe
408
+ src={clientSecret}
409
+ style={{ width: '100%', height, border: 0, transition: 'height 0.2s ease-out' }}
410
+ title="Payment"
411
+ allow="payment"
412
+ />
413
+ </div>
414
+ );
415
+ }
416
+
417
+ // Provider-hosted page: modal overlay
370
418
  return (
371
- <div className="w-full">
372
- {!iframeLoaded && <div className="flex items-center justify-center py-12"><span>Loading payment form...</span></div>}
373
- <iframe
374
- src={paymentUrl}
375
- onLoad={() => setIframeLoaded(true)}
376
- style={{ width: '100%', minHeight: '600px', border: 'none', display: iframeLoaded ? 'block' : 'none' }}
377
- allow="payment"
378
- />
379
- <p className="text-center text-sm text-gray-500 mt-4">
380
- Having trouble? <a href={paymentUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline">Open payment in new tab</a>
381
- </p>
419
+ <div className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 py-6 overflow-y-auto">
420
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl mx-4">
421
+ <iframe
422
+ src={clientSecret}
423
+ style={{ width: '100%', height: '90vh', minHeight: 700, border: 0 }}
424
+ title="Payment"
425
+ allow="payment"
426
+ />
427
+ </div>
382
428
  </div>
383
429
  );
384
430
  }
431
+
432
+ // Allowlist of origins you trust for top-level payment redirects.
433
+ function isTrustedPaymentUrl(url: string): boolean {
434
+ try {
435
+ const u = new URL(url);
436
+ if (u.protocol !== 'https:') return false;
437
+ // Add your provider hostnames here. Example: Cardcom for Bit express-pay.
438
+ return u.hostname === 'cardcom.solutions' || u.hostname.endsWith('.cardcom.solutions');
439
+ } catch { return false; }
440
+ }
385
441
  \`\`\`
386
442
 
387
443
  ### PayPal Payment Form
@@ -651,12 +707,23 @@ const growProvider = providers.find(p => p.provider === 'grow');
651
707
  const paypalProvider = providers.find(p => p.provider === 'paypal');
652
708
  \`\`\`
653
709
 
654
- Each provider has: \`id\`, \`provider\` ('stripe'|'grow'|'paypal'|'sandbox'), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
710
+ Each provider has: \`id\`, \`provider\` (flexible string \u2014 \`'stripe'\`, \`'grow'\`, \`'paypal'\`, \`'cardcom'\`, \`'sandbox'\`, and future providers), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
711
+
712
+ The payment intent returned from \`createPaymentIntent()\` includes a \`clientSdk\` object whose \`renderType\` tells you exactly how to render \u2014 **branch on THAT, not on the provider name**:
655
713
 
714
+ - \`renderType: 'sdk-widget'\` \u2014 load \`clientSdk.scriptUrl\`, mount into \`<div id={clientSdk.containerId}>\`. Used by Stripe, PayPal, Grow. The SDK paints its own form in your DOM.
715
+ - \`renderType: 'iframe'\` \u2014 render \`<iframe src={clientSecret}>\`. Two flavors detected by URL path:
716
+ - Path contains \`/embed/\` \u2192 Brainerce-hosted embed. Render **INLINE** (no modal). Listen for \`brainerce:resize\` / \`brainerce:redirect\` postMessages. Used by Cardcom (embedded mode).
717
+ - Any other URL \u2192 provider-hosted page. Render inside a **modal**. Used by Cardcom (hosted mode), legacy Grow iframe.
718
+ - \`renderType: 'redirect'\` \u2014 \`window.location.href = URL\`. Customer completes payment off-site and returns via SuccessRedirectUrl.
719
+ - \`renderType: 'sandbox'\` \u2014 show a "Complete Test Order" button; call \`completeGuestCheckout(checkoutId)\`. Orders are \`isTestOrder: true\`. Appears when \`sandboxPaymentsEnabled\` is true.
720
+ - \`renderType: 'embedded-fields'\` \u2014 reserved for future Stripe-Elements-style pattern (PCI micro-iframes for card/CVV mounted directly into the merchant form). **No provider ships this today** \u2014 handle via a "not supported yet" fallback.
721
+
722
+ Provider-specific install notes:
656
723
  - **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
657
- - **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
658
724
  - **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
659
- - **Sandbox:** No SDK needed \u2014 when \`sandboxPaymentsEnabled\` is true, \`getPaymentProviders()\` includes a sandbox provider with \`renderType: 'sandbox'\`. Show a "Complete Test Order" button. Call \`completeGuestCheckout(checkoutId)\` to finalize. Orders are marked \`isTestOrder: true\`.`;
725
+ - **Grow:** No SDK needed \u2014 JS SDK loaded via \`clientSdk.scriptUrl\`. Supports credit cards, Bit, Apple Pay, Google Pay.
726
+ - **Cardcom:** No SDK needed \u2014 rendering driven by \`renderType: 'iframe'\` + the inline/modal branch above. Supports credit cards, Bit (when terminal provisions it), installments, 3D Secure.`;
660
727
  }
661
728
  function getProductsSection(_currency) {
662
729
  return `## Products & Variants
@@ -687,10 +754,12 @@ When \`storeInfo.i18n.enabled\` is true, call \`client.setLocale(locale)\` once
687
754
  \`{ locale }\` params needed. Translated fields include: \`name\`,
688
755
  \`description\`, \`slug\`, \`seoTitle\`, \`seoDescription\`, plus
689
756
  \`categories[].name\`, \`brands[].name\`, \`tags[].name\`, \`variants[].name\`,
690
- \`productAttributeOptions[].attribute.name\`/\`attributeOption.name\`, and
691
- \`metafields[].value\`. No client-side overlay. See the full \`i18n\` section
692
- (\`get-sdk-docs\` topic \`i18n\`) for the \`[locale]\` route pattern, SDK setup,
693
- and the brand/tag badge examples.
757
+ \`variant.attributes\` keys and values (so \`getVariantOptions()\` returns
758
+ translated attribute/option names), \`productAttributeOptions[].attribute.name\`/
759
+ \`attributeOption.name\`, \`metafields[].value\`, cart item product/variant names,
760
+ and recommendation/bundle/bump product names. No client-side overlay. See the
761
+ full \`i18n\` section (\`get-sdk-docs\` topic \`i18n\`) for the \`[locale]\` route
762
+ pattern, SDK setup, and the brand/tag badge examples.
694
763
 
695
764
  ### Price Display (Use SDK Helper!)
696
765
 
@@ -827,6 +896,8 @@ const material = getProductMetafieldValue(product, 'material');
827
896
 
828
897
  Some products allow customers to provide input at purchase time (e.g., text on a cake, upload a logo). Check \`product.customizationFields\` and render input fields accordingly.
829
898
 
899
+ Merchants can also flag a \`MetafieldDefinition\` as \`appliesToAllProducts: true\` \u2014 those fields are folded into every product's \`customizationFields\` array automatically by the backend. Client code does not need to merge or union anything: always render exactly what's in \`product.customizationFields\`.
900
+
830
901
  \`\`\`typescript
831
902
  import { getProductCustomizationFields } from 'brainerce';
832
903
  import type { ProductCustomizationField } from 'brainerce';
@@ -885,7 +956,7 @@ function getCartSection(_currency) {
885
956
  ### Smart Cart Methods (RECOMMENDED)
886
957
 
887
958
  \`\`\`typescript
888
- const cart = await client.smartGetCart(); // Returns Cart | LocalCart
959
+ const cart = await client.smartGetCart(); // Returns CartWithIncludes (extends Cart)
889
960
  await client.smartAddToCart({
890
961
  productId: product.id,
891
962
  variantId: selectedVariant?.id,
@@ -1016,7 +1087,19 @@ const recs = (product as any).recommendations as ProductRecommendationsResponse
1016
1087
 
1017
1088
  ### Cart Page Features
1018
1089
 
1019
- **Cross-sell recommendations** (existing products the customer might also want):
1090
+ **Consolidated include** (fetch recommendations, upgrades, and bundles in one request):
1091
+ \`\`\`typescript
1092
+ import type { CartWithIncludes } from 'brainerce';
1093
+ const cart = await client.getCart(cartId, {
1094
+ include: ['recommendations', 'upgrades', 'bundles'],
1095
+ });
1096
+ // cart.recommendations \u2014 cross-sell recommendations
1097
+ // cart.upgrades \u2014 upgrade suggestions keyed by source product ID
1098
+ // cart.bundles \u2014 bundle offers
1099
+ // Also works with smartGetCart: client.smartGetCart({ include: ['recommendations', 'upgrades', 'bundles'] })
1100
+ \`\`\`
1101
+
1102
+ **Cross-sell recommendations** (or fetch individually for targeted refresh):
1020
1103
  \`\`\`typescript
1021
1104
  import type { CartRecommendationsResponse } from 'brainerce';
1022
1105
  const cartRecs = await client.getCartRecommendations(cartId, 4);
@@ -1108,6 +1191,110 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
1108
1191
  ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`, \`variants?\` (when variant selection needed).
1109
1192
  OrderBump/CartBundleOffer: includes \`requiresVariantSelection\` (boolean) and \`lockedVariant?\` (\`{ id, name, attributes }\`).`;
1110
1193
  }
1194
+ function getProductCustomizationFieldsSection() {
1195
+ return `## Product Customization Fields (buyer input on product page)
1196
+
1197
+ Some products let the buyer personalize them before adding to cart \u2014 engraving text, a photo upload, a color pick, a multi-select add-on list. The merchant defines these fields in the Brainerce dashboard; the storefront renders them, collects values, uploads any images, and passes everything as \`metadata\` on add-to-cart.
1198
+
1199
+ ### Where the fields come from
1200
+
1201
+ They arrive **embedded on the product response** \u2014 no extra API call:
1202
+
1203
+ \`\`\`typescript
1204
+ import type { Product, ProductCustomizationField, MetafieldType } from 'brainerce';
1205
+
1206
+ const product = await client.getProductBySlug(slug);
1207
+ const fields: ProductCustomizationField[] = product.customizationFields ?? [];
1208
+ // fields is sorted by position. If empty, render the product page normally.
1209
+ \`\`\`
1210
+
1211
+ Each field has:
1212
+ \`\`\`typescript
1213
+ {
1214
+ definitionId: string;
1215
+ key: string; // stable identifier \u2014 use this as the metadata key
1216
+ name: string; // display label (may be localized)
1217
+ description?: string | null;
1218
+ type: MetafieldType; // 'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'DATETIME' |
1219
+ // 'JSON' | 'URL' | 'COLOR' | 'DIMENSION' | 'WEIGHT' |
1220
+ // 'IMAGE' | 'GALLERY' | 'SELECT' | 'MULTI_SELECT'
1221
+ required: boolean;
1222
+ minLength?: number | null; // chars for TEXT/TEXTAREA; array size for MULTI_SELECT
1223
+ maxLength?: number | null;
1224
+ minValue?: number | null; // NUMBER / DIMENSION / WEIGHT
1225
+ maxValue?: number | null;
1226
+ enumValues?: string[]; // required for SELECT / MULTI_SELECT
1227
+ defaultValue?: string | null;
1228
+ position: number;
1229
+ }
1230
+ \`\`\`
1231
+
1232
+ ### Render one control per type
1233
+
1234
+ | type | Render as | Collected value |
1235
+ | --------------- | ---------------------------------------------------------- | ---------------------------- |
1236
+ | \`TEXT\` | \`<input type="text">\` | \`string\` |
1237
+ | \`TEXTAREA\` | \`<textarea>\` | \`string\` |
1238
+ | \`NUMBER\` | \`<input type="number">\` | \`number\` |
1239
+ | \`BOOLEAN\` | checkbox / switch | \`boolean\` |
1240
+ | \`DATE\` | \`<input type="date">\` | \`string\` (YYYY-MM-DD) |
1241
+ | \`DATETIME\` | \`<input type="datetime-local">\` | \`string\` (ISO 8601) |
1242
+ | \`URL\` | \`<input type="url">\` | \`string\` |
1243
+ | \`COLOR\` | \`<input type="color">\` | \`string\` (#RRGGBB) |
1244
+ | \`SELECT\` | \`<select>\` or radio group (use \`enumValues\`) | \`string\` (in enumValues) |
1245
+ | \`MULTI_SELECT\`| checkbox group (use \`enumValues\`) | \`string[]\` (every in enum) |
1246
+ | \`IMAGE\` | file input + upload via \`uploadCustomizationFile\` | \`string\` (URL) |
1247
+ | \`GALLERY\` | multi-file input + one upload per file | \`string[]\` (URLs) |
1248
+ | \`JSON\` | textarea; validate is parseable | \`string\` (JSON) |
1249
+
1250
+ ### Upload buyer-submitted images
1251
+
1252
+ \`IMAGE\` and \`GALLERY\` require uploading before add-to-cart:
1253
+
1254
+ \`\`\`typescript
1255
+ const { url } = await client.uploadCustomizationFile(file);
1256
+ // Server rules: image/* only, max 5 MB, 10 uploads/min per IP.
1257
+ // Files are retained for at least 7 days; after that, if the cart never
1258
+ // became an order, they are automatically deleted.
1259
+ \`\`\`
1260
+
1261
+ ### Add to cart with customization metadata
1262
+
1263
+ \`\`\`typescript
1264
+ // Collect values keyed by field.key \u2014 NOT by field.name or field.definitionId.
1265
+ const metadata: Record<string, unknown> = {
1266
+ engraving_text: 'Happy Birthday!', // TEXT
1267
+ frame_color: 'Gold', // SELECT (must be in enumValues)
1268
+ upload_photo: url, // IMAGE (URL from uploadCustomizationFile)
1269
+ addons: ['Gift wrap'], // MULTI_SELECT (always an array)
1270
+ };
1271
+
1272
+ await client.addToCart(cart.id, {
1273
+ productId: product.id,
1274
+ quantity: 1,
1275
+ metadata,
1276
+ });
1277
+ \`\`\`
1278
+
1279
+ Server validation (rejected with HTTP 400 on failure):
1280
+ - \`required: true\` \u2192 must be present and non-empty
1281
+ - \`TEXT\` / \`TEXTAREA\` \u2192 string; \`minLength\` / \`maxLength\` enforced as character count
1282
+ - \`NUMBER\` \u2192 \`minValue\` / \`maxValue\` enforced
1283
+ - \`SELECT\` \u2192 value must be one of \`enumValues\`
1284
+ - \`MULTI_SELECT\` \u2192 array; each element in \`enumValues\`; duplicates removed; \`minLength\` / \`maxLength\` enforced as array size
1285
+ - \`IMAGE\` / \`GALLERY\` \u2192 URL(s) must be from \`/customization-upload\` on this store
1286
+
1287
+ ### Order snapshot
1288
+
1289
+ When the order is created, each line's customization values are snapshotted onto the order item with \`{ key, name, type, value }\` \u2014 so later edits to the field definition don't orphan old orders. The merchant dashboard shows the values on the order detail page.
1290
+
1291
+ ### Common mistakes
1292
+
1293
+ - Passing values keyed by \`field.name\` instead of \`field.key\` \u2192 silently ignored (unknown key)
1294
+ - Sending a \`MULTI_SELECT\` as a string instead of \`string[]\` \u2192 HTTP 400
1295
+ - Using a raw external URL (not from \`/customization-upload\`) for \`IMAGE\` / \`GALLERY\` \u2192 HTTP 400
1296
+ - Assuming \`enumValues\` is present for all types \u2014 only \`SELECT\` / \`MULTI_SELECT\` require it`;
1297
+ }
1111
1298
  function getInventorySection() {
1112
1299
  return `## Inventory, Stock Display & Reservation Countdown
1113
1300
 
@@ -1364,7 +1551,50 @@ const { data: orders } = await client.getMyOrders({ page: 1, limit: 10 }); // Re
1364
1551
  // \u274C WRONG \u2014 these methods don't exist!
1365
1552
  client.getCustomerProfile();
1366
1553
  client.getCustomerOrders();
1367
- \`\`\``;
1554
+ \`\`\`
1555
+
1556
+ ### Order history should show more than just totals
1557
+
1558
+ A useful order card includes ALL of the following when the data is present. Each section is conditional \u2014 render nothing if the field is empty.
1559
+
1560
+ | Section | Source field | Notes |
1561
+ |---|---|---|
1562
+ | Header (number, status badge, date, total) | \`order.orderNumber\`, \`order.status\`, \`order.createdAt\`, \`order.totalAmount\` | Always present. |
1563
+ | Line items | \`order.items[]\` | Render image, name, qty, price. |
1564
+ | **Per-item customizations** | \`order.items[i].customizations\` | Map of { label, value, type }. Render by \`type\` \u2014 see table below. |
1565
+ | **Status timeline** | \`order.statusHistory\` | \`OrderStatusChange[]\`: \`{ status, at, note? }\`. Render as a vertical list. |
1566
+ | **Shipping address** | \`order.shippingAddress\` | Standard \`OrderAddress\` shape. |
1567
+ | **Tracking** | \`order.trackingNumber\`, \`order.trackingUrl\`, \`order.carrier\`, \`order.shippedAt\`, \`order.deliveredAt\` | Link out to \`trackingUrl\` when set. |
1568
+ | **Payment** | \`order.paymentMethod\`, \`order.financialStatus\` | Badge \`financialStatus\` (paid / pending / refunded / partially_refunded). |
1569
+ | Downloads | \`order.hasDownloads\` \u2192 \`client.getOrderDownloads(id)\` | Separate call; returns \`OrderDownloadLink[]\`. |
1570
+ | Financial summary | \`order.subtotal\`, \`order.appliedDiscounts\`, \`order.couponCode\` + \`couponDiscount\`, \`order.shippingAmount\`, \`order.taxAmount\`, \`order.totalAmount\` | Breakdown rows + final total. |
1571
+
1572
+ #### Rendering \`order.items[i].customizations\` by type
1573
+
1574
+ The map key is the metafield slug; the value is \`{ label, value, type }\`. Dispatch by \`type\`:
1575
+
1576
+ | Type | Value | Render |
1577
+ |---|---|---|
1578
+ | \`TEXT\`, \`TEXTAREA\`, \`URL\`, \`NUMBER\`, \`SELECT\` | string | Plain text (\`URL\` \u2192 anchor). |
1579
+ | \`BOOLEAN\` | \`"yes"\` / \`"no"\` | \u2713 / \u2717 |
1580
+ | \`MULTI_SELECT\` | \`string[]\` | Comma-separated. |
1581
+ | \`IMAGE\` | asset URL (string) | Thumbnail linking to full-size asset. |
1582
+ | \`GALLERY\` | \`string[]\` of URLs | Grid of thumbnails. |
1583
+ | \`COLOR\` | hex string | Swatch + hex text. |
1584
+ | \`DATE\` | ISO-8601 | \`toLocaleDateString()\` |
1585
+ | \`DATETIME\` | ISO-8601 | \`toLocaleString()\` |
1586
+ | Unknown | any | Plain text (defensive default). |
1587
+
1588
+ The storefront scaffolded by \`create-brainerce-store\` already implements this. See \`src/components/account/order-history.tsx\` + the sibling \`order-customizations.tsx\`, \`order-status-timeline.tsx\`, \`order-shipping-block.tsx\`, \`order-payment-block.tsx\` \u2014 mirror that structure.
1589
+
1590
+ ### Do NOT render
1591
+
1592
+ These fields are NOT returned to buyers by \`/customers/me/orders\`. If you see them in older examples, ignore them \u2014 they are for merchant/admin views only.
1593
+
1594
+ - \`order.accountId\`, \`order.storeId\`, \`order.customerId\` (already known on the request)
1595
+ - \`order.notes\` (internal merchant notes)
1596
+ - \`order.customFieldValues\` (order-level checkout fields \u2014 merchant concept; buyers see \`items[i].customizations\`)
1597
+ - \`order.appliedSurcharges\`, \`order.surchargeAmount\`, \`order.appliedRuleIds\`, \`order.downloadMeta\`, \`order.pickupLocationData\``;
1368
1598
  }
1369
1599
  function getOrderConfirmationSection() {
1370
1600
  return `## Order Confirmation Page (/order-confirmation) \u2014 REQUIRED!
@@ -1534,7 +1764,8 @@ overlay needed:
1534
1764
  - \`brands[].name\`
1535
1765
  - \`tags[].name\`
1536
1766
  - \`variants[].name\`
1537
- - \`productAttributeOptions[].attribute.name\`, \`attributeOption.name\`/\`value\`
1767
+ - \`variant.attributes\` keys and values \u2014 \`getVariantOptions(variant)\` returns translated attribute names and option values automatically
1768
+ - \`productAttributeOptions[].attribute.name\`, \`attributeOption.name\`
1538
1769
  - \`metafields[].value\`
1539
1770
 
1540
1771
  **Taxonomy (\`getCategories\`, \`getBrands\`, \`getTags\`):**
@@ -1543,6 +1774,10 @@ overlay needed:
1543
1774
  **Cart:**
1544
1775
  - \`items[].product.name\`, \`items[].variant.name\`
1545
1776
 
1777
+ **Bundles & Order Bumps:**
1778
+ - \`bundleProduct.name\`, \`bundleProduct.slug\`, \`bumpProduct.name\`, \`bumpProduct.slug\`
1779
+ - Variant names inside bundles/bumps
1780
+
1546
1781
  **Checkout:**
1547
1782
  - Line items: \`items[].product.name\`, \`items[].variant.name\`
1548
1783
  - Discount banners, nudges, badges
@@ -1653,6 +1888,8 @@ function getSectionByTopic(topic, connectionId, currency) {
1653
1888
  return getDiscountsSection();
1654
1889
  case "recommendations":
1655
1890
  return getRecommendationsSection();
1891
+ case "product-customization-fields":
1892
+ return getProductCustomizationFieldsSection();
1656
1893
  case "tax":
1657
1894
  return getTaxDisplaySection(cur);
1658
1895
  case "i18n":
@@ -1727,6 +1964,10 @@ function getSectionByTopic(topic, connectionId, currency) {
1727
1964
  "",
1728
1965
  "---",
1729
1966
  "",
1967
+ getProductCustomizationFieldsSection(),
1968
+ "",
1969
+ "---",
1970
+ "",
1730
1971
  getTaxDisplaySection(cur),
1731
1972
  "",
1732
1973
  "---",
@@ -1738,7 +1979,7 @@ function getSectionByTopic(topic, connectionId, currency) {
1738
1979
  getAdminApiSection()
1739
1980
  ].join("\n");
1740
1981
  default:
1741
- return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, checkout-custom-fields, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, i18n, critical-rules, type-reference, admin, all`;
1982
+ return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, checkout-custom-fields, payment, auth, order-confirmation, inventory, discounts, recommendations, product-customization-fields, tax, i18n, critical-rules, type-reference, admin, all`;
1742
1983
  }
1743
1984
  }
1744
1985
 
@@ -1758,6 +1999,7 @@ var GET_SDK_DOCS_SCHEMA = {
1758
1999
  "inventory",
1759
2000
  "discounts",
1760
2001
  "recommendations",
2002
+ "product-customization-fields",
1761
2003
  "tax",
1762
2004
  "critical-rules",
1763
2005
  "type-reference",
@@ -1802,6 +2044,7 @@ interface Product {
1802
2044
  brands?: Array<{ id: string; name: string }>;
1803
2045
  tags?: string[];
1804
2046
  metafields?: ProductMetafield[];
2047
+ customizationFields?: ProductCustomizationField[]; // Buyer input fields \u2014 render on PDP, send values in cart metadata. Account-wide fields (MetafieldDefinition.appliesToAllProducts=true) are folded in here automatically; no client-side merging.
1805
2048
  productAttributeOptions?: Array<{
1806
2049
  id: string;
1807
2050
  attributeId: string;
@@ -1860,6 +2103,32 @@ interface ProductMetafield {
1860
2103
  variantId?: string | null;
1861
2104
  }
1862
2105
 
2106
+ // Metafield type enum \u2014 used by ProductCustomizationField.type
2107
+ type MetafieldType =
2108
+ | 'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'DATETIME'
2109
+ | 'URL' | 'COLOR' | 'DIMENSION' | 'WEIGHT' | 'JSON'
2110
+ | 'IMAGE' | 'GALLERY'
2111
+ | 'SELECT' | 'MULTI_SELECT';
2112
+
2113
+ // Customer-facing input assigned per product. Render on the PDP in the order of \`position\`.
2114
+ // Submit values keyed by \`key\` inside AddToCartDto.metadata.
2115
+ // Upload files via \`uploadCustomizationFile()\` first, then place the returned \`url\` in metadata.
2116
+ interface ProductCustomizationField {
2117
+ definitionId: string;
2118
+ name: string; // Label to show buyers
2119
+ key: string; // Use as metadata key in AddToCartDto.metadata
2120
+ description?: string | null; // Help text
2121
+ type: MetafieldType;
2122
+ required: boolean;
2123
+ minLength?: number | null; // TEXT/TEXTAREA char bounds; MULTI_SELECT array bounds
2124
+ maxLength?: number | null;
2125
+ minValue?: number | null; // NUMBER bounds
2126
+ maxValue?: number | null;
2127
+ enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
2128
+ defaultValue?: string | null;
2129
+ position: number; // Render order
2130
+ }
2131
+
1863
2132
  interface ProductQueryParams {
1864
2133
  page?: number;
1865
2134
  limit?: number;
@@ -1943,6 +2212,7 @@ interface CartItem {
1943
2212
  sku?: string | null;
1944
2213
  image?: ProductImage | string | null;
1945
2214
  } | null;
2215
+ metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
1946
2216
  createdAt: string;
1947
2217
  updatedAt: string;
1948
2218
  }
@@ -2007,6 +2277,9 @@ interface Checkout {
2007
2277
  taxAmount: string;
2008
2278
  taxBreakdown?: TaxBreakdown | null;
2009
2279
  total: string;
2280
+ surchargeAmount: string;
2281
+ appliedSurcharges?: Array<{ key: string; name: string; value: unknown; amount: string }> | null;
2282
+ customFieldValues?: Record<string, unknown> | null;
2010
2283
  couponCode?: string | null;
2011
2284
  lineItems: CheckoutLineItem[]; // Use THIS for order summary, NOT cart.items!
2012
2285
  itemCount: number;
@@ -2025,6 +2298,7 @@ interface CheckoutLineItem {
2025
2298
  discountAmount: string;
2026
2299
  product: { id: string; name: string; sku: string; images?: ProductImage[]; };
2027
2300
  variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
2301
+ metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
2028
2302
  }
2029
2303
 
2030
2304
  interface CheckoutAddress {
@@ -2106,6 +2380,27 @@ interface Order {
2106
2380
  billingAddress?: OrderAddress;
2107
2381
  hasDownloads?: boolean;
2108
2382
  createdAt: string;
2383
+
2384
+ // Payment + fulfillment
2385
+ paymentMethod?: string | null;
2386
+ financialStatus?: string | null; // "pending" | "paid" | "refunded" | "partially_refunded" | "voided"
2387
+ fulfillmentStatus?: string | null; // "unfulfilled" | "partial" | "fulfilled"
2388
+
2389
+ // Tracking
2390
+ trackingNumber?: string | null;
2391
+ trackingUrl?: string | null;
2392
+ carrier?: string | null;
2393
+ shippedAt?: string | null; // ISO-8601
2394
+ deliveredAt?: string | null; // ISO-8601
2395
+
2396
+ // Timeline of status transitions, chronological
2397
+ statusHistory?: OrderStatusChange[] | null;
2398
+ }
2399
+
2400
+ interface OrderStatusChange {
2401
+ status: OrderStatus;
2402
+ at: string; // ISO-8601
2403
+ note?: string | null;
2109
2404
  }
2110
2405
 
2111
2406
  // \u26A0\uFE0F OrderItem is FLAT \u2014 unlike CartItem which is NESTED
@@ -2119,6 +2414,9 @@ interface OrderItem {
2119
2414
  unitPrice?: string; // alias
2120
2415
  totalPrice?: string;
2121
2416
  image?: string; // FLAT: item.image (NOT nested)
2417
+ // Snapshot of buyer-submitted customization values captured at checkout.
2418
+ // Keyed by metafield slug. \`value\` is string[] for MULTI_SELECT / GALLERY, string otherwise.
2419
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>;
2122
2420
  }
2123
2421
 
2124
2422
  interface OrderCustomer {
@@ -2340,6 +2638,32 @@ interface ProductRecommendationsResponse {
2340
2638
  }
2341
2639
  interface CartRecommendationsResponse {
2342
2640
  recommendations: ProductRecommendation[];
2641
+ }
2642
+
2643
+ // Cart include types (for consolidated getCart requests)
2644
+ type CartIncludeOption = 'recommendations' | 'upgrades' | 'bundles';
2645
+ interface CartIncludeOptions {
2646
+ include?: CartIncludeOption[];
2647
+ }
2648
+ interface CartWithIncludes extends Cart {
2649
+ recommendations?: { recommendations: ProductRecommendation[] };
2650
+ upgrades?: { upgrades: Record<string, CartUpgradeSuggestion> };
2651
+ bundles?: { bundles: CartBundleOffer[] };
2652
+ }
2653
+ interface CartUpgradeSuggestion {
2654
+ targetProduct: ProductRecommendation;
2655
+ priceDelta: string;
2656
+ deltaPercent: number;
2657
+ }
2658
+ interface CartBundleOffer {
2659
+ id: string;
2660
+ bundleProduct: ProductRecommendation;
2661
+ originalPrice: string;
2662
+ discountedPrice: string;
2663
+ discountType: 'PERCENTAGE' | 'FIXED_AMOUNT';
2664
+ discountValue: number;
2665
+ requiresVariantSelection: boolean;
2666
+ lockedVariant?: { id: string; name?: string };
2343
2667
  }`;
2344
2668
  var TYPES_BY_DOMAIN = {
2345
2669
  products: PRODUCTS_TYPES,
@@ -2403,7 +2727,8 @@ var GET_CODE_EXAMPLE_SCHEMA = {
2403
2727
  "coupon-apply-and-remove",
2404
2728
  "reservation-countdown",
2405
2729
  "search-autocomplete-debounce",
2406
- "i18n-set-locale"
2730
+ "i18n-set-locale",
2731
+ "order-history-full"
2407
2732
  ]).describe("The SDK operation to get a snippet for.")
2408
2733
  };
2409
2734
  var SNIPPETS = {
@@ -2498,23 +2823,51 @@ const checkout = await client.getCheckout(checkoutId);
2498
2823
  "checkout-payment-providers": `// Fetch configured payment providers for this checkout.
2499
2824
  import { client } from './brainerce';
2500
2825
 
2501
- const providers = await client.getPaymentProviders(checkoutId);
2502
- // providers = [{ provider, name, renderType, config }, ...]
2503
- // renderType tells you how to render:
2504
- // 'stripe-elements' \u2014 render Stripe Elements form and call stripe.confirmCardPayment
2505
- // 'redirect' \u2014 render a button that navigates to provider.authorizationUrl
2506
- // 'paypal' \u2014 render PayPal SDK button
2507
- // 'sandbox' \u2014 render a "complete test order" button (no real charge)
2826
+ const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
2827
+ // each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
2828
+ //
2829
+ // After createPaymentIntent, the response carries a clientSdk.renderType that
2830
+ // tells your UI how to render the payment step. The 5 possible values:
2831
+ //
2832
+ // 'sdk-widget' \u2014 Load the provider's JS SDK (scriptUrl), then mount
2833
+ // into a <div id={containerId}>. Used by Stripe,
2834
+ // PayPal, Grow. The SDK paints its own form inside
2835
+ // your DOM. You control layout/placement.
2836
+ //
2837
+ // 'iframe' \u2014 Render <iframe src={clientSecret}>. Two flavors:
2838
+ // a) URL path contains "/embed/" \u2192 Brainerce-hosted
2839
+ // embed page. Render INLINE in your checkout
2840
+ // flow. Listen for postMessage:
2841
+ // - 'brainerce:resize' \u2192 update iframe height
2842
+ // - 'brainerce:redirect' \u2192 validated top-level
2843
+ // navigation (e.g. Bit express-pay)
2844
+ // b) Any other URL \u2192 provider-hosted page. Render
2845
+ // inside a modal overlay (it carries its own
2846
+ // branding/chrome).
2847
+ //
2848
+ // 'redirect' \u2014 Full-page navigate: window.location.href = URL.
2849
+ // Customer leaves your site, pays on provider page,
2850
+ // returns via SuccessRedirectUrl.
2851
+ //
2852
+ // 'sandbox' \u2014 Test mode. Render a "Complete Test Order" button;
2853
+ // call completeGuestCheckout(checkoutId). Orders are
2854
+ // flagged isTestOrder: true. No real charge.
2855
+ //
2856
+ // 'embedded-fields' \u2014 (Reserved \u2014 not yet shipped in any provider.) PCI
2857
+ // micro-iframes for sensitive fields (card number,
2858
+ // CVV) mounted directly into the merchant form. Full
2859
+ // control of surrounding layout, like Stripe
2860
+ // Elements. Requires the provider to ship a client
2861
+ // SDK that exposes mount points.
2862
+ //
2863
+ // Always branch on clientSdk.renderType \u2014 NEVER hard-code by provider name.
2864
+ // New providers can be added without storefront changes.
2508
2865
 
2509
- // Pick the first configured provider by default (or let the user pick if
2510
- // multiple are configured).
2511
- const active = providers[0];
2512
-
2513
- // If providers is empty, NO payment provider is configured and customers
2514
- // cannot pay. Display a clear error \u2014 do not try to fake a checkout.
2515
2866
  if (providers.length === 0) {
2516
2867
  throw new Error('No payment provider configured for this store');
2517
- }`,
2868
+ }
2869
+
2870
+ const active = providers[0];`,
2518
2871
  "checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js and @stripe/react-stripe-js
2519
2872
  // (or the vanilla Stripe.js browser SDK).
2520
2873
  import { loadStripe } from '@stripe/stripe-js';
@@ -2728,12 +3081,82 @@ const locale = 'he'; // e.g., from the /[locale] route segment
2728
3081
  client.setLocale(locale);
2729
3082
 
2730
3083
  // ALL content comes back translated: products, categories, brands,
2731
- // tags, variants, metafields, cart items, checkout line items,
2732
- // recommendations, search suggestions, discount banners, nudges,
3084
+ // tags, variants (name + attributes keys/values), metafields,
3085
+ // cart items, checkout line items, recommendations, bundles,
3086
+ // order bumps, search suggestions, discount banners, nudges,
2733
3087
  // badges, order history.
2734
3088
 
2735
3089
  // For RTL locales (he, ar), set the document direction.
2736
- // Framework-neutral: document.documentElement.setAttribute('dir', 'rtl');`
3090
+ // Framework-neutral: document.documentElement.setAttribute('dir', 'rtl');`,
3091
+ "order-history-full": `// Full "My Orders" account page. Render every piece of data the server
3092
+ // already returns \u2014 not just order number / total / status.
3093
+ //
3094
+ // Data you get from client.getMyOrders() (already on the wire today):
3095
+ // - items[].customizations \u2014 buyer's custom-field entries
3096
+ // - statusHistory \u2014 status timeline
3097
+ // - shippingAddress \u2014 name + address lines + country
3098
+ // - trackingNumber / trackingUrl / carrier / shippedAt / deliveredAt
3099
+ // - paymentMethod / financialStatus / fulfillmentStatus
3100
+ // - appliedDiscounts \u2014 shaped buyer-facing view
3101
+ // - hasDownloads \u2014 call /orders/:id/downloads to fetch
3102
+ //
3103
+ // Do NOT render: accountId, storeId, customerId, notes, customFieldValues
3104
+ // (merchant-internal, order-level), appliedRuleIds, appliedSurcharges,
3105
+ // surchargeAmount, downloadMeta (raw), pickupLocationData. The backend
3106
+ // does not return these to buyers \u2014 if you see them in a type, they're
3107
+ // an oversight; still skip them.
3108
+ import { client } from './brainerce';
3109
+ import type { Order, OrderItem, OrderStatusChange } from 'brainerce';
3110
+
3111
+ const { data: orders } = await client.getMyOrders({ page: 1, limit: 10 });
3112
+
3113
+ for (const order of orders) {
3114
+ // Line items + per-item customizations
3115
+ for (const item of order.items) {
3116
+ // item.image, item.productName, item.quantity, item.price
3117
+ if (item.customizations) {
3118
+ for (const [fieldId, entry] of Object.entries(item.customizations)) {
3119
+ // entry = { label, value, type }
3120
+ // Type-aware render (see account-page section in get-sdk-docs):
3121
+ // TEXT / TEXTAREA / URL / NUMBER / SELECT \u2014 plain text
3122
+ // BOOLEAN \u2014 \u2713 / \u2717
3123
+ // MULTI_SELECT \u2014 comma-separated
3124
+ // IMAGE \u2014 <img src={value}>
3125
+ // GALLERY \u2014 wrap grid of <img>
3126
+ // COLOR \u2014 swatch + hex
3127
+ // DATE / DATETIME \u2014 localized format
3128
+ }
3129
+ }
3130
+ }
3131
+
3132
+ // Status timeline \u2014 skip silently when null/empty
3133
+ if (order.statusHistory?.length) {
3134
+ for (const entry of order.statusHistory as OrderStatusChange[]) {
3135
+ // entry = { status, at, note? }
3136
+ // Render a dot + localized status label + new Date(entry.at).toLocaleString()
3137
+ }
3138
+ }
3139
+
3140
+ // Shipping + tracking
3141
+ if (order.shippingAddress) {
3142
+ // Render firstName lastName \xB7 line1 \xB7 line2 \xB7 city, region \xB7 postalCode \xB7 country
3143
+ }
3144
+ if (order.trackingNumber) {
3145
+ // Render carrier \xB7 trackingNumber, shippedAt/deliveredAt dates,
3146
+ // and an anchor to order.trackingUrl when present.
3147
+ }
3148
+
3149
+ // Payment
3150
+ if (order.paymentMethod || order.financialStatus) {
3151
+ // paymentMethod: 'card' | 'paypal' | 'bank_transfer' | 'cash_on_delivery' | ...
3152
+ // financialStatus: 'pending' | 'paid' | 'refunded' | 'partially_refunded' | 'voided'
3153
+ // Map financialStatus \u2192 a colored badge.
3154
+ }
3155
+ }
3156
+
3157
+ // All sections above are conditional. Absent data = section not rendered \u2014
3158
+ // never show empty placeholders. The create-brainerce-store template's
3159
+ // src/components/account/order-history.tsx is the reference implementation.`
2737
3160
  };
2738
3161
  async function handleGetCodeExample(args) {
2739
3162
  const snippet = SNIPPETS[args.operation];
@@ -3199,7 +3622,7 @@ var RULES = {
3199
3622
  - All prices are STRINGS in the SDK. \`parseFloat\` them before math or comparisons.
3200
3623
  - CartItem and CheckoutLineItem are NESTED (\`item.product.name\`, \`item.unitPrice\`). OrderItem is FLAT (\`item.name\`, \`item.price\`). They are not interchangeable.
3201
3624
  - Cart has no \`.total\` field \u2014 call \`getCartTotals(cart)\` to get \`{ subtotal, tax, shipping, discount, total }\`.
3202
- - \`smartGetCart()\` returns a discriminated union (\`Cart | LocalCart\`). Check \`'id' in cart\` before using server-only fields.
3625
+ - \`smartGetCart()\` returns \`CartWithIncludes\` (extends \`Cart\`). Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
3203
3626
  - \`startGuestCheckout()\` is also a discriminated union. Check \`result.tracked\` before reading \`result.checkoutId\`.`
3204
3627
  }
3205
3628
  };
@@ -3237,6 +3660,7 @@ var GET_BUSINESS_FLOWS_SCHEMA = {
3237
3660
  "order-confirmation",
3238
3661
  "cart-persistence",
3239
3662
  "inventory-reservation",
3663
+ "product-customization",
3240
3664
  "all"
3241
3665
  ]).describe('Which flow to retrieve. Use "all" to get every flow.')
3242
3666
  };
@@ -3356,7 +3780,7 @@ Build the OAuth button region AND the callback handler even when no providers ar
3356
3780
  - **Reads:** \`client.getCart()\` returns the current cart. Call it on mount in your cart UI and on any page that shows a cart count (header).
3357
3781
  - **Writes:** use \`client.addToCart\`, \`client.updateCartItem\`, \`client.removeCartItem\`, \`client.applyCoupon\`, \`client.removeCoupon\`. After each mutation the SDK returns the updated cart.
3358
3782
  - **Totals:** call \`getCartTotals(cart)\` \u2014 do NOT read \`cart.total\`. The helper understands taxes, shipping, and discounts.
3359
- - **\`smartGetCart()\`** returns \`Cart | LocalCart\` (guest users have no server cart until they add an item). Check \`'id' in cart\` before using server-only fields.
3783
+ - **\`smartGetCart()\`** returns \`CartWithIncludes\` (extends \`Cart\`). All carts are server-side. Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
3360
3784
  - **NEVER mutate cart state outside SDK helpers.** Any hand-rolled cart update risks desync with the reservation timer and the checkout flow.`
3361
3785
  },
3362
3786
  "inventory-reservation": {
@@ -3370,6 +3794,54 @@ Build the OAuth button region AND the callback handler even when no providers ar
3370
3794
  - **On the cart page:** show per-item availability \u2014 items that can no longer be purchased (because their reservation expired or the stock dropped) should display an "out of stock" badge and the "proceed to checkout" action should be disabled until they are removed.
3371
3795
 
3372
3796
  The reservation strategy (\`HARD\` vs \`SOFT\`) is exposed via \`get-store-capabilities\`. A HARD reservation means the stock is physically held; a SOFT reservation just tracks intent. Your UI behaves identically either way \u2014 the SDK hides the difference.`
3797
+ },
3798
+ "product-customization": {
3799
+ title: "Product customization (buyer input)",
3800
+ body: `Products can ship with \`customizationFields: ProductCustomizationField[]\` \u2014 buyer-filled inputs (engraving text, uploaded photo, pick-a-color, etc.). If you ignore them, merchants lose the data they need to fulfill the order.
3801
+
3802
+ 1. **Read the fields** from the product payload. Sort by \`position\`. If the array is empty, nothing to render.
3803
+ 2. **Render one control per field** based on \`type\`:
3804
+ - \`TEXT\` / \`URL\` / \`COLOR\` / \`DIMENSION\` / \`WEIGHT\` \u2192 \`<input type="text">\` (honour \`minLength\`/\`maxLength\`)
3805
+ - \`TEXTAREA\` \u2192 \`<textarea>\`
3806
+ - \`NUMBER\` \u2192 \`<input type="number">\` (honour \`minValue\`/\`maxValue\`)
3807
+ - \`BOOLEAN\` \u2192 checkbox (value \`true\`/\`false\`)
3808
+ - \`DATE\` \u2192 \`<input type="date">\` (ISO \`YYYY-MM-DD\`)
3809
+ - \`DATETIME\` \u2192 \`<input type="datetime-local">\` (ISO timestamp)
3810
+ - \`SELECT\` \u2192 \`<select>\` populated from \`enumValues\` (value = one string)
3811
+ - \`MULTI_SELECT\` \u2192 checkbox group from \`enumValues\` (value = \`string[]\`)
3812
+ - \`IMAGE\` \u2192 file input + preview (value = one URL string)
3813
+ - \`GALLERY\` \u2192 multi-file input (value = \`string[]\` of URLs)
3814
+ - \`JSON\` \u2192 advanced; render an admin-style editor or skip unless you control the data
3815
+ 3. **For IMAGE / GALLERY types: upload FIRST, then attach the URL.** Call \`client.uploadCustomizationFile(file)\` \u2014 it returns \`{ url }\`. Put that \`url\` string into the field value. NEVER put a \`File\` object in cart metadata.
3816
+ 4. **Enforce \`required: true\` client-side** before add-to-cart \u2014 show a red hint, block submit. The server re-validates; you want the user to fix it before the request fails.
3817
+ 5. **Add to cart with metadata keyed by \`field.key\`:**
3818
+ \`\`\`ts
3819
+ await client.addToCart({
3820
+ productId,
3821
+ quantity: 1,
3822
+ metadata: {
3823
+ engraving_text: 'For Mom',
3824
+ frame_color: 'Gold',
3825
+ upload_photo: photoUrl, // from uploadCustomizationFile()
3826
+ addons: ['Gift wrap'], // MULTI_SELECT
3827
+ },
3828
+ });
3829
+ \`\`\`
3830
+ 6. **On cart / checkout UIs, surface the metadata.** \`CartItem.metadata\` and \`CheckoutLineItem.metadata\` carry the values the buyer submitted. Show them in the line-item row so the buyer can verify before paying (especially uploaded image thumbnails).
3831
+ 7. **After the order is placed, values live on \`OrderItem.customizations\`** \u2014 a \`Record<string, { label, value, type }>\` keyed by field key. Definitions may be renamed/deleted after the order; the snapshot preserves what the buyer saw at purchase time.
3832
+
3833
+ **Apply-to-all fields.** A merchant can flag a \`MetafieldDefinition\` with \`appliesToAllProducts: true\` \u2014 the backend then includes it in every product's \`customizationFields\` array automatically, including products created after the flag was set. Your client code reads \`product.customizationFields\` as-is and never merges or unions anything \u2014 just render what's there.
3834
+
3835
+ Server-side guardrails that WILL reject bad requests (so validate client-side to avoid round-trips):
3836
+ - Unknown keys \u2192 rejected. Only use keys that appear in \`product.customizationFields\`.
3837
+ - Missing \`required\` fields \u2192 rejected.
3838
+ - \`SELECT\` value not in \`enumValues\` \u2192 rejected.
3839
+ - \`MULTI_SELECT\` value not \`string[]\` OR containing values outside \`enumValues\` \u2192 rejected.
3840
+ - \`IMAGE\` / \`GALLERY\` values must be URLs returned from \`/customization-upload\` on this store \u2014 pasting an external URL is rejected.
3841
+ - Upload > 5MB or non-image MIME \u2192 rejected by upload endpoint.
3842
+ - More than 10 uploads per IP per minute \u2192 429.
3843
+
3844
+ Never render customization fields without also wiring the upload + metadata flow \u2014 a form that submits nothing is worse than no form at all.`
3373
3845
  }
3374
3846
  };
3375
3847
  var FLOW_ORDER = [
@@ -3380,7 +3852,8 @@ var FLOW_ORDER = [
3380
3852
  "oauth",
3381
3853
  "order-confirmation",
3382
3854
  "cart-persistence",
3383
- "inventory-reservation"
3855
+ "inventory-reservation",
3856
+ "product-customization"
3384
3857
  ];
3385
3858
  async function handleGetBusinessFlows(args) {
3386
3859
  const header = "# Brainerce Business Flows\n\nThese sequences are framework-neutral and non-negotiable. Your framework and file layout are your choice \u2014 the order of SDK calls and the error handling is not.";
@@ -3445,6 +3918,14 @@ var FEATURES = [
3445
3918
  sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
3446
3919
  mandatory: "mandatory"
3447
3920
  },
3921
+ {
3922
+ id: "product-customization-fields",
3923
+ title: "Render buyer-input customization fields on the product page",
3924
+ description: "When a product ships with `customizationFields` (engraving text, uploaded photo, picked color, date, select / multi-select), render one control per field sorted by `position`. Enforce required + enumValues + min/max client-side. For IMAGE / GALLERY, upload via client.uploadCustomizationFile() and place the returned URL in the value. Submit all values keyed by `field.key` inside addToCart({ metadata }). Show the metadata on cart / checkout rows so the buyer can verify. Build the UI anyway \u2014 it auto-hides when a product has no customization fields. NOTE: merchants can flag a definition with `appliesToAllProducts: true`; the backend folds those into every product's `customizationFields` array automatically \u2014 the client never merges or unions anything, just reads `product.customizationFields` as-is.",
3925
+ sdk: "product.customizationFields, client.uploadCustomizationFile(file), client.addToCart({ productId, quantity, metadata })",
3926
+ flowRef: "product-customization",
3927
+ mandatory: "mandatory"
3928
+ },
3448
3929
  {
3449
3930
  id: "cart",
3450
3931
  title: "Manage a cart with quantity, removal, and totals",