@decocms/apps 1.4.0 → 1.5.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.
@@ -1096,3 +1096,133 @@ export type AnalyticsEvent =
1096
1096
  | ViewItemListEvent
1097
1097
  | ViewPromotionEvent
1098
1098
  | DecoEvent;
1099
+
1100
+ // ---------------------------------------------------------------------------
1101
+ // Minicart — platform-agnostic storefront cart contract
1102
+ // ---------------------------------------------------------------------------
1103
+ //
1104
+ // Reconciled superset of presentational shapes used across Deco storefronts.
1105
+ // Platform-specific extras (VTEX `attachments`, `seller`, etc.) live as
1106
+ // **optional** fields so future platform mappers (Shopify, VNDA, Wake, Linx,
1107
+ // Nuvemshop, ...) can populate-or-skip without breaking the contract.
1108
+ //
1109
+ // Pricing is in **major units** (e.g. `19.90` for R$19.90), not cents. This
1110
+ // matches `Intl.NumberFormat` (used by `commerce/sdk/formatPrice`) and lets
1111
+ // platform transforms convert from native units once at the boundary.
1112
+ //
1113
+ // Currently shipped with: VTEX (`vtex/utils/minicart.ts`, `vtex/inline-loaders/minicart.ts`).
1114
+
1115
+ /** Free-form attachment payload. Platforms attach customizations or services. */
1116
+ export interface MinicartItemAttachment {
1117
+ name: string;
1118
+ content: unknown;
1119
+ }
1120
+
1121
+ /** Description of an attachment slot offered by the platform for an item. */
1122
+ export interface MinicartItemAttachmentOffering {
1123
+ name: string;
1124
+ required: boolean;
1125
+ // biome-ignore lint/suspicious/noExplicitAny: schema is platform-specific
1126
+ schema?: any;
1127
+ }
1128
+
1129
+ /**
1130
+ * A single line in the minicart.
1131
+ *
1132
+ * Mirrors the fields of `AnalyticsItem` (GA4 contract) so a `MinicartItem` can
1133
+ * be forwarded directly to `add_to_cart` / `view_cart` events. We deliberately
1134
+ * make both `item_id` and `item_name` optional — `AnalyticsItem` enforces
1135
+ * "one or the other" via a discriminated union, which forces consumers to
1136
+ * narrow on every read. Storefront platforms typically populate both, and
1137
+ * sites cast at the GA4 boundary.
1138
+ *
1139
+ * Required:
1140
+ * - `image` — product image URL (storefront-hosted, https).
1141
+ * - `listPrice` — original list price per unit (compare-at), in major units.
1142
+ * - `price` — current selling price per unit, in major units.
1143
+ * - `quantity` — line quantity.
1144
+ *
1145
+ * Optional (platform-specific):
1146
+ * - `seller` — vendor / seller identifier (VTEX, Wake).
1147
+ * - `attachments` — applied customizations (VTEX).
1148
+ * - `attachmentOfferings` — offered customization slots (VTEX).
1149
+ */
1150
+ export interface MinicartItem {
1151
+ // --- GA4 analytics fields (compatible with `AnalyticsItem`) ---
1152
+ item_id?: string;
1153
+ item_name?: string;
1154
+ item_brand?: string;
1155
+ item_category?: string;
1156
+ item_category2?: string;
1157
+ item_category3?: string;
1158
+ item_category4?: string;
1159
+ item_category5?: string;
1160
+ item_group_id?: string;
1161
+ item_list_id?: string;
1162
+ item_list_name?: string;
1163
+ item_url?: string;
1164
+ item_variant?: string;
1165
+ affiliation?: string;
1166
+ coupon?: string;
1167
+ discount?: number;
1168
+ index?: number;
1169
+ location_id?: string;
1170
+
1171
+ // --- Cart-required fields ---
1172
+ /** Line quantity. */
1173
+ quantity: number;
1174
+ /** Product image URL (https). */
1175
+ image: string;
1176
+ /** Original list price per unit, in major units. */
1177
+ listPrice: number;
1178
+ /** Selling price per unit, in major units. */
1179
+ price: number;
1180
+
1181
+ // --- Platform-specific (optional) ---
1182
+ /** Vendor / seller identifier (VTEX, Wake). Optional for platforms without sellers. */
1183
+ seller?: string;
1184
+ /** Applied customizations on this line (VTEX attachments). */
1185
+ attachments?: MinicartItemAttachment[];
1186
+ /** Customization slots offered for this line (VTEX). */
1187
+ attachmentOfferings?: MinicartItemAttachmentOffering[];
1188
+ }
1189
+
1190
+ /**
1191
+ * Storefront-facing minicart. Two views:
1192
+ * - `original` — raw platform cart (VTEX OrderForm, Shopify Cart, ...). Sites use
1193
+ * this as an escape hatch for platform-specific reads (GTM, pixels, custom
1194
+ * integrations). Generic param `TRaw` lets callers narrow the type.
1195
+ * - `storefront` — normalized contract every UI consumes.
1196
+ *
1197
+ * All monetary values are in **major units** (decimal), not cents.
1198
+ */
1199
+ export interface Minicart<TRaw = unknown> {
1200
+ /** Raw platform cart blob — escape hatch for site-specific reads. */
1201
+ original: TRaw;
1202
+ /** Normalized storefront contract. */
1203
+ storefront: {
1204
+ items: MinicartItem[];
1205
+ /** Total payable, in major units. */
1206
+ total: number;
1207
+ /** Sum of line items before discounts/shipping, in major units. */
1208
+ subtotal: number;
1209
+ /** Total discount applied, in major units. Always non-negative. */
1210
+ discounts: number;
1211
+ /** Shipping cost, in major units. Undefined when not yet calculated. */
1212
+ shipping?: number;
1213
+ /** Applied coupon code, if any. */
1214
+ coupon?: string;
1215
+ /** BCP-47 locale (e.g. `"pt-BR"`). Drives `formatPrice`. */
1216
+ locale: string;
1217
+ /** ISO-4217 currency code (e.g. `"BRL"`). */
1218
+ currency: string;
1219
+ /** Whether the UI should expose the coupon input. */
1220
+ enableCoupon?: boolean;
1221
+ /** Free-shipping threshold in major units. Drives the progress bar. `0` disables it. */
1222
+ freeShippingTarget: number;
1223
+ /** Where the checkout button sends the user. */
1224
+ checkoutHref: string;
1225
+ /** Postal code used for shipping simulation. */
1226
+ postalCode?: string;
1227
+ };
1228
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/apps",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
6
6
  "exports": {
@@ -40,6 +40,7 @@
40
40
  "./vtex/inline-loaders/productListShelf": "./vtex/inline-loaders/productListShelf.ts",
41
41
  "./vtex/inline-loaders/relatedProducts": "./vtex/inline-loaders/relatedProducts.ts",
42
42
  "./vtex/inline-loaders/suggestions": "./vtex/inline-loaders/suggestions.ts",
43
+ "./vtex/inline-loaders/minicart": "./vtex/inline-loaders/minicart.ts",
43
44
  "./vtex/hooks": "./vtex/hooks/index.ts",
44
45
  "./vtex/hooks/*": "./vtex/hooks/*.ts",
45
46
  "./vtex/inline-loaders/workflowProducts": "./vtex/inline-loaders/workflowProducts.ts",
@@ -95,6 +96,7 @@
95
96
  "vtex/",
96
97
  "resend/",
97
98
  "website/",
99
+ "registry.ts",
98
100
  "!**/__tests__/",
99
101
  "!scripts/"
100
102
  ],
package/registry.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Declarative catalogue of installable apps published under `@decocms/apps`.
3
+ *
4
+ * `@decocms/start`'s `autoconfigApps()` consumes this array to wire CMS
5
+ * commerce loaders and admin invoke handlers for whichever apps the host
6
+ * site has configured in its decofile. New apps are added here — no edit
7
+ * to the framework or the site is required.
8
+ *
9
+ * Import path: `@decocms/apps/registry`
10
+ *
11
+ * NOTE: the type is inlined rather than imported from
12
+ * `@decocms/start/apps` so this file ships in `@decocms/apps@1.4.0`
13
+ * against any installed `@decocms/start` version. Once callers pin a
14
+ * start version that exposes `AppRegistry`, the type can be swapped.
15
+ */
16
+
17
+ interface AppRegistryEntry {
18
+ /** Block key in the decofile, e.g. "deco-shopify". */
19
+ blockKey: string;
20
+ /** Lazy dynamic import of the app's mod module. */
21
+ module: () => Promise<any>;
22
+ /** Human-readable name shown in admin install UI. */
23
+ displayName?: string;
24
+ /** Icon URL (absolute or site-relative) shown in admin install UI. */
25
+ icon?: string;
26
+ /** Grouping label, e.g. "commerce", "email", "analytics". */
27
+ category?: string;
28
+ /** Short summary shown in admin install UI. */
29
+ description?: string;
30
+ }
31
+
32
+ type AppRegistry = readonly AppRegistryEntry[];
33
+
34
+ export const APP_REGISTRY: AppRegistry = [
35
+ {
36
+ blockKey: "deco-shopify",
37
+ module: () => import("./shopify/mod"),
38
+ displayName: "Shopify",
39
+ category: "commerce",
40
+ description: "Shopify Storefront API commerce integration",
41
+ },
42
+ {
43
+ blockKey: "deco-vtex",
44
+ module: () => import("./vtex/mod"),
45
+ displayName: "VTEX",
46
+ category: "commerce",
47
+ description: "VTEX IO commerce integration",
48
+ },
49
+ {
50
+ blockKey: "deco-resend",
51
+ module: () => import("./resend/mod"),
52
+ displayName: "Resend",
53
+ category: "email",
54
+ description: "Transactional email via Resend",
55
+ },
56
+ ];
57
+
58
+ export default APP_REGISTRY;
59
+ export type { AppRegistryEntry, AppRegistry };
@@ -1,36 +1,43 @@
1
1
  /**
2
2
  * Client-side cart hook for VTEX.
3
3
  *
4
- * Uses TanStack Query for SWR, optimistic updates, and cache
5
- * invalidation. Wraps the VTEX orderForm API.
4
+ * Uses TanStack Query for SWR, optimistic updates, and cache invalidation.
5
+ * Returns BOTH the raw `OrderForm` (back-compat for existing consumers) AND
6
+ * the canonical `Minicart` shape (preferred for new code).
6
7
  *
7
- * @example
8
+ * @example Reading the cart
8
9
  * ```tsx
9
10
  * import { useCart } from "@decocms/apps/vtex/hooks/useCart";
10
11
  *
11
12
  * function CartButton() {
12
- * const { cart, addItems, isLoading } = useCart();
13
- * const count = cart?.items?.length ?? 0;
13
+ * const { minicart, isLoading } = useCart({ freeShippingTarget: 200 });
14
+ * const count = minicart?.storefront.items.length ?? 0;
14
15
  * return <button disabled={isLoading}>{count} items</button>;
15
16
  * }
16
17
  * ```
18
+ *
19
+ * @example Mutations
20
+ * ```tsx
21
+ * const { addItems, removeItem, addCoupons } = useCart();
22
+ * addItems.mutate([{ id: "123", seller: "1", quantity: 1 }]);
23
+ * ```
17
24
  */
18
25
 
19
26
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
27
+ import { useMemo } from "react";
28
+ import type { Minicart } from "../../commerce/types/commerce";
29
+ import { vtexOrderFormToMinicart } from "../utils/minicart";
30
+ import type { OrderForm, OrderFormItem } from "../types";
20
31
 
21
- export interface CartItem {
22
- id: string;
23
- quantity: number;
24
- seller: string;
25
- }
32
+ /** Re-exported from `vtex/types` for back-compat. New code should import directly. */
33
+ export type { OrderForm } from "../types";
26
34
 
27
- export interface OrderForm {
28
- orderFormId: string;
29
- items: CartItem[];
30
- totalizers: Array<{ id: string; name: string; value: number }>;
31
- value: number;
32
- [key: string]: unknown;
33
- }
35
+ /**
36
+ * Slim cart-item shape used by mutations.
37
+ * @deprecated Use `OrderFormItem` from `@decocms/apps/vtex/types` for full fidelity,
38
+ * or `MinicartItem` from `@decocms/apps/commerce/types` for the canonical contract.
39
+ */
40
+ export type CartItem = Pick<OrderFormItem, "id" | "quantity" | "seller">;
34
41
 
35
42
  const CART_QUERY_KEY = ["vtex", "cart"] as const;
36
43
 
@@ -136,6 +143,14 @@ export interface UseCartOptions {
136
143
  enabled?: boolean;
137
144
  /** Stale time in ms. @default 30000 */
138
145
  staleTime?: number;
146
+ /** Free-shipping threshold in major units, surfaced on `minicart.storefront`. @default 0 */
147
+ freeShippingTarget?: number;
148
+ /** Override the OrderForm's locale (BCP-47, e.g. `"pt-BR"`). */
149
+ locale?: string;
150
+ /** Where the checkout button sends the user. @default "/checkout" */
151
+ checkoutHref?: string;
152
+ /** Whether to surface the coupon input. @default true */
153
+ enableCoupon?: boolean;
139
154
  }
140
155
 
141
156
  export function useCart(options?: UseCartOptions) {
@@ -148,6 +163,24 @@ export function useCart(options?: UseCartOptions) {
148
163
  enabled: options?.enabled !== false,
149
164
  });
150
165
 
166
+ const cart = query.data ?? null;
167
+
168
+ const minicart: Minicart<OrderForm> | null = useMemo(() => {
169
+ if (!cart) return null;
170
+ return vtexOrderFormToMinicart(cart, {
171
+ freeShippingTarget: options?.freeShippingTarget,
172
+ locale: options?.locale,
173
+ checkoutHref: options?.checkoutHref,
174
+ enableCoupon: options?.enableCoupon,
175
+ });
176
+ }, [
177
+ cart,
178
+ options?.freeShippingTarget,
179
+ options?.locale,
180
+ options?.checkoutHref,
181
+ options?.enableCoupon,
182
+ ]);
183
+
151
184
  const addItems = useMutation({
152
185
  mutationFn: (items: Array<{ id: string; quantity: number; seller: string }>) => {
153
186
  const orderFormId = query.data?.orderFormId;
@@ -193,7 +226,10 @@ export function useCart(options?: UseCartOptions) {
193
226
  });
194
227
 
195
228
  return {
196
- cart: query.data ?? null,
229
+ /** Raw VTEX OrderForm — escape hatch for platform-specific reads. */
230
+ cart,
231
+ /** Canonical platform-agnostic minicart. Prefer this in new UI code. */
232
+ minicart,
197
233
  isLoading: query.isLoading,
198
234
  isError: query.isError,
199
235
  error: query.error,
@@ -202,6 +238,6 @@ export function useCart(options?: UseCartOptions) {
202
238
  addCoupons,
203
239
  updateQuantity,
204
240
  removeItem,
205
- itemCount: query.data?.items?.length ?? 0,
241
+ itemCount: cart?.items?.length ?? 0,
206
242
  };
207
243
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * SSR loader returning the canonical `Minicart` for the current request.
3
+ *
4
+ * Reads `orderFormId` from the `checkout.vtex.com__orderFormId` cookie and
5
+ * fetches the corresponding OrderForm via `getOrCreateCart`. When no
6
+ * orderFormId cookie exists (first-time visitor, no items added yet), returns
7
+ * an empty `Minicart` shell — we deliberately avoid creating a new OrderForm
8
+ * server-side to prevent zero-item carts on every page view.
9
+ *
10
+ * Configurable via Props (free-shipping target, locale, checkout URL).
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // setup/commerce-loaders.ts
15
+ * import minicart from "@decocms/apps/vtex/inline-loaders/minicart";
16
+ * registerInlineLoader("vtex/inline-loaders/minicart", minicart);
17
+ * ```
18
+ */
19
+
20
+ import { RequestContext } from "@decocms/start/sdk/requestContext";
21
+ import type { Minicart } from "../../commerce/types/commerce";
22
+ import { getOrCreateCart } from "../actions/checkout";
23
+ import type { OrderForm } from "../types";
24
+ import { vtexOrderFormToMinicart } from "../utils/minicart";
25
+
26
+ const ORDER_FORM_COOKIE = "checkout.vtex.com__orderFormId";
27
+
28
+ export interface MinicartProps {
29
+ /** Free-shipping threshold in major units. `0` disables the progress bar. */
30
+ freeShippingTarget?: number;
31
+ /** Override the OrderForm's locale (BCP-47, e.g. `"pt-BR"`). */
32
+ locale?: string;
33
+ /** Where the checkout button sends the user. Default: `/checkout`. */
34
+ checkoutHref?: string;
35
+ /** Whether the UI should expose the coupon input. Default: `true`. */
36
+ enableCoupon?: boolean;
37
+ }
38
+
39
+ function readOrderFormIdFromRequest(): string | undefined {
40
+ const ctx = RequestContext.current;
41
+ const cookieHeader = ctx?.request.headers.get("cookie");
42
+ if (!cookieHeader) return undefined;
43
+ const match = cookieHeader.match(
44
+ new RegExp(`(?:^|;\\s*)${ORDER_FORM_COOKIE}=([^;]+)`),
45
+ );
46
+ return match?.[1] ? decodeURIComponent(match[1]) : undefined;
47
+ }
48
+
49
+ /** Empty cart shell returned when no orderFormId is yet associated with the visitor. */
50
+ function emptyMinicart(opts: MinicartProps): Minicart<OrderForm | null> {
51
+ return {
52
+ original: null,
53
+ storefront: {
54
+ items: [],
55
+ subtotal: 0,
56
+ discounts: 0,
57
+ total: 0,
58
+ locale: opts.locale ?? "pt-BR",
59
+ currency: "BRL",
60
+ enableCoupon: opts.enableCoupon ?? true,
61
+ freeShippingTarget: opts.freeShippingTarget ?? 0,
62
+ checkoutHref: opts.checkoutHref ?? "/checkout",
63
+ },
64
+ };
65
+ }
66
+
67
+ export default async function vtexMinicart(
68
+ props: MinicartProps = {},
69
+ ): Promise<Minicart<OrderForm | null>> {
70
+ const orderFormId = readOrderFormIdFromRequest();
71
+ if (!orderFormId) return emptyMinicart(props);
72
+
73
+ const orderForm = await getOrCreateCart({ orderFormId });
74
+ return vtexOrderFormToMinicart(orderForm, {
75
+ freeShippingTarget: props.freeShippingTarget,
76
+ locale: props.locale,
77
+ checkoutHref: props.checkoutHref,
78
+ enableCoupon: props.enableCoupon,
79
+ });
80
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Map a VTEX OrderForm to the canonical `Minicart` contract.
3
+ *
4
+ * Pure function — no I/O, fully unit-testable. Pricing is converted from
5
+ * VTEX's native cents to major units (the canonical unit for `Minicart`).
6
+ *
7
+ * Locale and currency come from `orderForm.storePreferencesData` and follow
8
+ * VTEX's `storePreferencesData.countryCode` / `currencyCode` semantics.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { vtexOrderFormToMinicart } from "@decocms/apps/vtex/utils/minicart";
13
+ * import { getCart } from "@decocms/apps/vtex/loaders/cart";
14
+ *
15
+ * const orderForm = await getCart(orderFormId);
16
+ * const minicart = vtexOrderFormToMinicart(orderForm, {
17
+ * freeShippingTarget: 0,
18
+ * checkoutHref: "/checkout",
19
+ * });
20
+ * ```
21
+ */
22
+
23
+ import type { Minicart, MinicartItem } from "../../commerce/types/commerce";
24
+ import type { OrderForm, OrderFormItem, Totalizer } from "../types";
25
+
26
+ export interface VtexOrderFormToMinicartOptions {
27
+ /** Free-shipping threshold in major units. `0` disables the progress bar. */
28
+ freeShippingTarget?: number;
29
+ /** Override the OrderForm's `clientPreferencesData.locale` (BCP-47, e.g. `"pt-BR"`). */
30
+ locale?: string;
31
+ /** Where the checkout button sends the user. Default: `/checkout`. */
32
+ checkoutHref?: string;
33
+ /** Whether the UI should expose the coupon input. Default: `true`. */
34
+ enableCoupon?: boolean;
35
+ }
36
+
37
+ const CENTS_PER_MAJOR = 100;
38
+
39
+ /** Convert VTEX cents to major units. Always returns a finite number. */
40
+ function fromCents(cents: number | undefined | null): number {
41
+ if (cents == null || !Number.isFinite(cents)) return 0;
42
+ return cents / CENTS_PER_MAJOR;
43
+ }
44
+
45
+ function findTotalizer(totalizers: Totalizer[] | undefined, id: string): number {
46
+ if (!totalizers) return 0;
47
+ const t = totalizers.find((x) => x.id === id);
48
+ return t?.value ?? 0;
49
+ }
50
+
51
+ /**
52
+ * Locale heuristic. VTEX exposes `clientPreferencesData.locale` when set, but
53
+ * otherwise we synthesize one from `storePreferencesData.countryCode` so the UI
54
+ * always has a usable value for `Intl.NumberFormat`.
55
+ */
56
+ function inferLocale(orderForm: OrderForm, override?: string): string {
57
+ if (override) return override;
58
+ const explicit = orderForm.clientPreferencesData?.locale;
59
+ if (explicit) return explicit;
60
+
61
+ const country = orderForm.storePreferencesData?.countryCode;
62
+ if (country === "BRA" || country === "BR") return "pt-BR";
63
+ if (country === "USA" || country === "US") return "en-US";
64
+ return "en-US";
65
+ }
66
+
67
+ function vtexItemToMinicartItem(item: OrderFormItem, index: number, coupon?: string): MinicartItem {
68
+ const sellingPrice = fromCents(item.sellingPrice ?? item.price);
69
+ const listPrice = fromCents(item.listPrice ?? item.price);
70
+ const discount = Math.max(0, listPrice - sellingPrice);
71
+
72
+ return {
73
+ // AnalyticsItem identifier — VTEX uses productId; sites map to numeric SKU
74
+ // when needed via `Number(item.item_id)` (see bagaggio Minicart).
75
+ item_id: item.id,
76
+ item_group_id: item.productId,
77
+ item_name: item.name ?? item.skuName ?? "",
78
+ item_variant: item.skuName,
79
+ item_brand: item.additionalInfo?.brandName ?? undefined,
80
+ item_url: item.detailUrl,
81
+ coupon,
82
+ affiliation: item.seller,
83
+ index,
84
+ // Cart-required fields
85
+ image: item.imageUrl?.replace(/^http:/, "https:") ?? "",
86
+ listPrice,
87
+ price: sellingPrice,
88
+ quantity: item.quantity,
89
+ discount: Number(discount.toFixed(2)),
90
+ // Platform-specific
91
+ seller: item.seller,
92
+ attachments: item.attachments as MinicartItem["attachments"],
93
+ attachmentOfferings: item.attachmentOfferings as MinicartItem["attachmentOfferings"],
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Map a VTEX `OrderForm` to the canonical platform-agnostic `Minicart`.
99
+ *
100
+ * @param orderForm - Result from `getCart()` or `getOrCreateCart()`.
101
+ * @param opts - Storefront-level overrides (free-shipping target, checkout href, ...).
102
+ */
103
+ export function vtexOrderFormToMinicart(
104
+ orderForm: OrderForm,
105
+ opts: VtexOrderFormToMinicartOptions = {},
106
+ ): Minicart<OrderForm> {
107
+ const totalizers = orderForm.totalizers;
108
+ const subtotal = fromCents(findTotalizer(totalizers, "Items"));
109
+ const discountsRaw = findTotalizer(totalizers, "Discounts");
110
+ const discounts = Math.abs(fromCents(discountsRaw));
111
+ const shippingRaw = findTotalizer(totalizers, "Shipping");
112
+ const shipping = totalizers?.some((t) => t.id === "Shipping") ? fromCents(shippingRaw) : undefined;
113
+ const total = fromCents(orderForm.value);
114
+
115
+ const coupon = orderForm.marketingData?.coupon;
116
+ const items = (orderForm.items ?? []).map((item, index) =>
117
+ vtexItemToMinicartItem(item, index, coupon),
118
+ );
119
+
120
+ return {
121
+ original: orderForm,
122
+ storefront: {
123
+ items,
124
+ subtotal,
125
+ discounts,
126
+ shipping,
127
+ total,
128
+ coupon,
129
+ locale: inferLocale(orderForm, opts.locale),
130
+ currency: orderForm.storePreferencesData?.currencyCode ?? "BRL",
131
+ enableCoupon: opts.enableCoupon ?? true,
132
+ freeShippingTarget: opts.freeShippingTarget ?? 0,
133
+ checkoutHref: opts.checkoutHref ?? "/checkout",
134
+ postalCode: orderForm.shippingData?.address?.postalCode ?? undefined,
135
+ },
136
+ };
137
+ }