@decocms/apps-vtex 7.24.0 → 7.25.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.
@@ -17,6 +17,11 @@ import * as loaders_address from "./loaders/address";
17
17
  import * as loaders_autocomplete from "./loaders/autocomplete";
18
18
  import * as loaders_brands from "./loaders/brands";
19
19
  import * as loaders_cart from "./loaders/cart";
20
+ import * as loaders_cart_attachments from "./loaders/cart/attachments";
21
+ import * as loaders_cart_full from "./loaders/cart/full";
22
+ import * as loaders_cart_gifts from "./loaders/cart/gifts";
23
+ import * as loaders_cart_shipping from "./loaders/cart/shipping";
24
+ import * as loaders_cart_summary from "./loaders/cart/summary";
20
25
  import * as loaders_catalog from "./loaders/catalog";
21
26
  import * as loaders_collections from "./loaders/collections";
22
27
  import * as loaders_legacy from "./loaders/legacy";
@@ -35,44 +40,49 @@ import * as loaders_wishlistProducts from "./loaders/wishlistProducts";
35
40
  import * as loaders_workflow from "./loaders/workflow";
36
41
 
37
42
  const manifest = {
38
- name: "vtex",
39
- loaders: {
40
- "vtex/loaders/address": loaders_address,
41
- "vtex/loaders/autocomplete": loaders_autocomplete,
42
- "vtex/loaders/brands": loaders_brands,
43
- "vtex/loaders/cart": loaders_cart,
44
- "vtex/loaders/catalog": loaders_catalog,
45
- "vtex/loaders/collections": loaders_collections,
46
- "vtex/loaders/legacy": loaders_legacy,
47
- "vtex/loaders/logistics": loaders_logistics,
48
- "vtex/loaders/navbar": loaders_navbar,
49
- "vtex/loaders/orders": loaders_orders,
50
- "vtex/loaders/pageType": loaders_pageType,
51
- "vtex/loaders/payment": loaders_payment,
52
- "vtex/loaders/profile": loaders_profile,
53
- "vtex/loaders/promotion": loaders_promotion,
54
- "vtex/loaders/search": loaders_search,
55
- "vtex/loaders/session": loaders_session,
56
- "vtex/loaders/user": loaders_user,
57
- "vtex/loaders/wishlist": loaders_wishlist,
58
- "vtex/loaders/wishlistProducts": loaders_wishlistProducts,
59
- "vtex/loaders/workflow": loaders_workflow,
60
- },
61
- actions: {
62
- "vtex/actions/address": actions_address,
63
- "vtex/actions/analytics/sendEvent": actions_analytics_sendEvent,
64
- "vtex/actions/auth": actions_auth,
65
- "vtex/actions/checkout": actions_checkout,
66
- "vtex/actions/masterData": actions_masterData,
67
- "vtex/actions/misc": actions_misc,
68
- "vtex/actions/newsletter": actions_newsletter,
69
- "vtex/actions/orders": actions_orders,
70
- "vtex/actions/profile": actions_profile,
71
- "vtex/actions/session": actions_session,
72
- "vtex/actions/trigger": actions_trigger,
73
- "vtex/actions/wishlist": actions_wishlist,
74
- },
75
- sections: {},
43
+ name: "vtex",
44
+ loaders: {
45
+ "vtex/loaders/address": loaders_address,
46
+ "vtex/loaders/autocomplete": loaders_autocomplete,
47
+ "vtex/loaders/brands": loaders_brands,
48
+ "vtex/loaders/cart": loaders_cart,
49
+ "vtex/loaders/cart/summary": loaders_cart_summary,
50
+ "vtex/loaders/cart/full": loaders_cart_full,
51
+ "vtex/loaders/cart/shipping": loaders_cart_shipping,
52
+ "vtex/loaders/cart/gifts": loaders_cart_gifts,
53
+ "vtex/loaders/cart/attachments": loaders_cart_attachments,
54
+ "vtex/loaders/catalog": loaders_catalog,
55
+ "vtex/loaders/collections": loaders_collections,
56
+ "vtex/loaders/legacy": loaders_legacy,
57
+ "vtex/loaders/logistics": loaders_logistics,
58
+ "vtex/loaders/navbar": loaders_navbar,
59
+ "vtex/loaders/orders": loaders_orders,
60
+ "vtex/loaders/pageType": loaders_pageType,
61
+ "vtex/loaders/payment": loaders_payment,
62
+ "vtex/loaders/profile": loaders_profile,
63
+ "vtex/loaders/promotion": loaders_promotion,
64
+ "vtex/loaders/search": loaders_search,
65
+ "vtex/loaders/session": loaders_session,
66
+ "vtex/loaders/user": loaders_user,
67
+ "vtex/loaders/wishlist": loaders_wishlist,
68
+ "vtex/loaders/wishlistProducts": loaders_wishlistProducts,
69
+ "vtex/loaders/workflow": loaders_workflow,
70
+ },
71
+ actions: {
72
+ "vtex/actions/address": actions_address,
73
+ "vtex/actions/analytics/sendEvent": actions_analytics_sendEvent,
74
+ "vtex/actions/auth": actions_auth,
75
+ "vtex/actions/checkout": actions_checkout,
76
+ "vtex/actions/masterData": actions_masterData,
77
+ "vtex/actions/misc": actions_misc,
78
+ "vtex/actions/newsletter": actions_newsletter,
79
+ "vtex/actions/orders": actions_orders,
80
+ "vtex/actions/profile": actions_profile,
81
+ "vtex/actions/session": actions_session,
82
+ "vtex/actions/trigger": actions_trigger,
83
+ "vtex/actions/wishlist": actions_wishlist,
84
+ },
85
+ sections: {},
76
86
  } as const;
77
87
 
78
88
  export type Manifest = typeof manifest;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Tests for `projectOrderForm` — the server-side fragmentation of a VTEX
3
+ * OrderForm into the requested `CartProjection`. Pure function, no I/O.
4
+ */
5
+
6
+ import type { Minicart } from "@decocms/apps-commerce/types";
7
+ import { describe, expect, it } from "vitest";
8
+ import type { OrderForm } from "../../types";
9
+ import { projectOrderForm } from "../cartProjection";
10
+
11
+ function makeOrderForm(overrides: Partial<OrderForm> = {}): OrderForm {
12
+ return {
13
+ orderFormId: "of-123",
14
+ salesChannel: "1",
15
+ loggedIn: false,
16
+ isCheckedIn: false,
17
+ allowManualPrice: false,
18
+ canEditData: true,
19
+ ignoreProfileData: false,
20
+ value: 15000, // R$150.00 in cents
21
+ messages: [],
22
+ items: [
23
+ {
24
+ id: "sku-1",
25
+ productId: "prod-1",
26
+ name: "Camiseta",
27
+ skuName: "Camiseta P Azul",
28
+ imageUrl: "http://img.example/x.jpg",
29
+ detailUrl: "/camiseta/p",
30
+ price: 10000,
31
+ listPrice: 12000,
32
+ sellingPrice: 10000,
33
+ quantity: 1,
34
+ seller: "1",
35
+ uniqueId: "u1",
36
+ },
37
+ {
38
+ id: "sku-2",
39
+ productId: "prod-2",
40
+ name: "Boné",
41
+ skuName: "Boné U Preto",
42
+ imageUrl: "https://img.example/y.jpg",
43
+ detailUrl: "/bone/u",
44
+ price: 5000,
45
+ listPrice: 5000,
46
+ sellingPrice: 5000,
47
+ quantity: 2,
48
+ seller: "1",
49
+ uniqueId: "u2",
50
+ },
51
+ ],
52
+ totalizers: [
53
+ { id: "Items", name: "Items", value: 20000 },
54
+ { id: "Discounts", name: "Discounts", value: -5000 },
55
+ ],
56
+ shippingData: null,
57
+ clientProfileData: null,
58
+ paymentData: null,
59
+ marketingData: null,
60
+ ...overrides,
61
+ } as OrderForm;
62
+ }
63
+
64
+ describe("projectOrderForm", () => {
65
+ it("none → { ok: true } and discards the payload", () => {
66
+ expect(projectOrderForm(makeOrderForm(), "none")).toEqual({ ok: true });
67
+ });
68
+
69
+ it("summary → orderFormId, summed quantities, total in major units", () => {
70
+ const r = projectOrderForm(makeOrderForm(), "summary") as {
71
+ orderFormId: string;
72
+ totalItems: number;
73
+ total: number;
74
+ };
75
+ expect(r.orderFormId).toBe("of-123");
76
+ expect(r.totalItems).toBe(3); // 1 + 2
77
+ expect(r.total).toBe(150); // 15000 cents → 150
78
+ });
79
+
80
+ it("summary+items → summary plus slim, https-normalized items", () => {
81
+ const r = projectOrderForm(makeOrderForm(), "summary+items") as {
82
+ totalItems: number;
83
+ items: Array<{ item_name: string; image: string; price: number; quantity: number }>;
84
+ };
85
+ expect(r.totalItems).toBe(3);
86
+ expect(r.items).toHaveLength(2);
87
+ expect(r.items[0]).toMatchObject({
88
+ item_name: "Camiseta",
89
+ item_variant: "Camiseta P Azul",
90
+ image: "https://img.example/x.jpg", // http → https
91
+ price: 100, // 10000 cents
92
+ quantity: 1,
93
+ });
94
+ // slim projection must NOT leak the full VTEX item fields
95
+ expect(r.items[0]).not.toHaveProperty("uniqueId");
96
+ });
97
+
98
+ it("minicart → canonical Minicart with major-unit totals", () => {
99
+ const r = projectOrderForm(makeOrderForm(), "minicart") as Minicart<OrderForm>;
100
+ expect(r.storefront.items).toHaveLength(2);
101
+ expect(r.storefront.total).toBe(150);
102
+ expect(r.original.orderFormId).toBe("of-123");
103
+ });
104
+
105
+ it("raw → the untouched OrderForm", () => {
106
+ const of = makeOrderForm();
107
+ expect(projectOrderForm(of, "raw")).toBe(of);
108
+ });
109
+
110
+ it("summary handles an empty cart without NaN", () => {
111
+ const r = projectOrderForm(
112
+ makeOrderForm({ items: [], value: 0, totalizers: [] }),
113
+ "summary",
114
+ ) as { totalItems: number; total: number };
115
+ expect(r.totalItems).toBe(0);
116
+ expect(r.total).toBe(0);
117
+ });
118
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Project a VTEX OrderForm down to a requested `CartProjection`.
3
+ *
4
+ * The VTEX checkout API always returns the whole OrderForm from mutation
5
+ * endpoints (bounded only by `expectedOrderFormSections`) — there is no
6
+ * delta/patch response. So "return the minimum" is enforced **server-side**:
7
+ * we take whatever VTEX gave us and shape it into `none` / `summary` /
8
+ * `summary+items` / `minicart` / `raw` before it reaches the browser.
9
+ *
10
+ * Pure function — no I/O, fully unit-testable. All monetary values in the
11
+ * projected output are in major units (VTEX is cents).
12
+ *
13
+ * @see `@decocms/apps-commerce/types/cart` for the agnostic contract.
14
+ */
15
+
16
+ import type {
17
+ CartItemSlim,
18
+ CartOk,
19
+ CartProjection,
20
+ CartSummary,
21
+ CartSummaryWithItems,
22
+ Minicart,
23
+ } from "@decocms/apps-commerce/types";
24
+ import type { OrderForm, OrderFormItem } from "../types";
25
+ import { type VtexOrderFormToMinicartOptions, vtexOrderFormToMinicart } from "./minicart";
26
+
27
+ const CENTS_PER_MAJOR = 100;
28
+
29
+ function fromCents(cents: number | undefined | null): number {
30
+ if (cents == null || !Number.isFinite(cents)) return 0;
31
+ return cents / CENTS_PER_MAJOR;
32
+ }
33
+
34
+ /** Sum of line quantities. */
35
+ function countItems(orderForm: OrderForm): number {
36
+ return (orderForm.items ?? []).reduce((sum, i) => sum + (i.quantity ?? 0), 0);
37
+ }
38
+
39
+ function toSummary(orderForm: OrderForm): CartSummary {
40
+ return {
41
+ orderFormId: orderForm.orderFormId ?? null,
42
+ totalItems: countItems(orderForm),
43
+ total: fromCents(orderForm.value),
44
+ };
45
+ }
46
+
47
+ /** Map a VTEX line to the slim, analytics-compatible `CartItemSlim`. */
48
+ function toSlimItem(item: OrderFormItem): CartItemSlim {
49
+ return {
50
+ item_id: item.id,
51
+ item_name: item.name ?? item.skuName ?? "",
52
+ item_variant: item.skuName,
53
+ image: item.imageUrl?.replace(/^http:/, "https:") ?? "",
54
+ price: fromCents(item.sellingPrice ?? item.price),
55
+ quantity: item.quantity,
56
+ };
57
+ }
58
+
59
+ export type ProjectOrderFormOptions = VtexOrderFormToMinicartOptions;
60
+
61
+ /** The projected result type, with VTEX-concrete `minicart`/`raw` payloads. */
62
+ export type VtexCartProjectionResult =
63
+ | CartOk
64
+ | CartSummary
65
+ | CartSummaryWithItems
66
+ | Minicart<OrderForm>
67
+ | OrderForm;
68
+
69
+ /**
70
+ * Shape a VTEX OrderForm into the requested projection.
71
+ *
72
+ * @param orderForm - Raw OrderForm from a VTEX checkout call.
73
+ * @param projection - Desired client-facing shape.
74
+ * @param opts - Storefront overrides forwarded to the `minicart` projection.
75
+ */
76
+ export function projectOrderForm(
77
+ orderForm: OrderForm,
78
+ projection: CartProjection,
79
+ opts: ProjectOrderFormOptions = {},
80
+ ): VtexCartProjectionResult {
81
+ switch (projection) {
82
+ case "none":
83
+ return { ok: true };
84
+ case "summary":
85
+ return toSummary(orderForm);
86
+ case "summary+items":
87
+ return {
88
+ ...toSummary(orderForm),
89
+ items: (orderForm.items ?? []).map(toSlimItem),
90
+ };
91
+ case "minicart":
92
+ return vtexOrderFormToMinicart(orderForm, opts);
93
+ case "raw":
94
+ return orderForm;
95
+ }
96
+ }