@decocms/apps-vtex 7.43.0 → 7.44.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/apps-vtex",
3
- "version": "7.43.0",
3
+ "version": "7.44.0",
4
4
  "type": "module",
5
5
  "description": "Deco commerce app: VTEX integration",
6
6
  "repository": {
@@ -51,10 +51,10 @@
51
51
  "lint:unused": "knip"
52
52
  },
53
53
  "dependencies": {
54
- "@decocms/blocks": "7.43.0",
55
- "@decocms/apps-commerce": "7.43.0",
56
- "@decocms/apps-website": "7.43.0",
57
- "@decocms/tanstack": "7.43.0"
54
+ "@decocms/blocks": "7.44.0",
55
+ "@decocms/apps-commerce": "7.44.0",
56
+ "@decocms/apps-website": "7.44.0",
57
+ "@decocms/tanstack": "7.44.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@tanstack/react-query": ">=5.0.0",
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AggregateOffer, Offer, UnitPriceSpecification } from "@decocms/apps-commerce/types";
3
+ import { useOffer } from "@decocms/apps-commerce/sdk/useOffer";
4
+ import { buildOfferShelf } from "./transform";
5
+
6
+ const installment = (
7
+ over: Partial<UnitPriceSpecification>,
8
+ ): UnitPriceSpecification =>
9
+ ({
10
+ "@type": "UnitPriceSpecification",
11
+ priceType: "https://schema.org/SalePrice",
12
+ priceComponentType: "https://schema.org/Installment",
13
+ ...over,
14
+ }) as UnitPriceSpecification;
15
+
16
+ const offerWith = (specs: UnitPriceSpecification[]): Offer =>
17
+ ({
18
+ "@type": "Offer",
19
+ price: 100,
20
+ priceSpecification: [
21
+ { "@type": "UnitPriceSpecification", priceType: "https://schema.org/ListPrice", price: 120 },
22
+ { "@type": "UnitPriceSpecification", priceType: "https://schema.org/SalePrice", price: 100 },
23
+ ...specs,
24
+ ],
25
+ }) as Offer;
26
+
27
+ const pickInstallment = (offer: Offer) =>
28
+ useOffer({ "@type": "AggregateOffer", offers: [offer] } as AggregateOffer).installment;
29
+
30
+ describe("buildOfferShelf installment selection", () => {
31
+ it("keeps the same installment useOffer picks on PLP/PDP (shelf == detail)", () => {
32
+ // 12x whose stated total (100) is the lowest and duration the highest, but VTEX
33
+ // rounding makes 12 * 8.34 = 100.08 — the old 1-cent isNoInterest check rejected
34
+ // it and the shelf fell back to 10x, diverging from the detail page's 12x.
35
+ const full = offerWith([
36
+ installment({ name: "PIX", billingDuration: 1, billingIncrement: 100, price: 100 }),
37
+ installment({ name: "Visa", billingDuration: 10, billingIncrement: 10, price: 100 }),
38
+ installment({ name: "Visa", billingDuration: 12, billingIncrement: 8.34, price: 100 }),
39
+ installment({ name: "Visa", billingDuration: 6, billingIncrement: 17.67, price: 106 }),
40
+ ]);
41
+
42
+ const lean = buildOfferShelf(full);
43
+
44
+ expect(pickInstallment(lean)).toEqual(pickInstallment(full));
45
+ expect(pickInstallment(lean)?.billingDuration).toBe(12);
46
+ });
47
+
48
+ it("keeps the lowest-total plan even when every installment has interest", () => {
49
+ const full = offerWith([
50
+ installment({ name: "Visa", billingDuration: 3, billingIncrement: 34, price: 102 }),
51
+ installment({ name: "Visa", billingDuration: 6, billingIncrement: 18, price: 108 }),
52
+ ]);
53
+
54
+ const lean = buildOfferShelf(full);
55
+
56
+ expect(pickInstallment(lean)).toEqual(pickInstallment(full));
57
+ expect(pickInstallment(lean)?.price).toBe(102);
58
+ });
59
+ });
@@ -20,6 +20,7 @@ import type {
20
20
  SiteNavigationElement,
21
21
  UnitPriceSpecification,
22
22
  } from "@decocms/apps-commerce/types";
23
+ import { bestInstallment } from "@decocms/apps-commerce/sdk/useOffer";
23
24
  import { DEFAULT_IMAGE } from "@decocms/apps-commerce/utils/constants";
24
25
  import { formatRange } from "@decocms/apps-commerce/utils/filters";
25
26
  import { pick } from "./pickAndOmit";
@@ -509,61 +510,48 @@ export const toProduct = <P extends LegacyProductVTEX | ProductVTEX>(
509
510
  };
510
511
  };
511
512
 
512
- /**
513
- * Determines if an installment has no interest by checking if
514
- * billingDuration * billingIncrement ≈ total price (within 1 cent tolerance).
515
- */
516
- const isNoInterest = (spec: UnitPriceSpecification): boolean => {
517
- if (spec.billingDuration == null || spec.billingIncrement == null || spec.price == null) {
518
- return false;
519
- }
520
- return Math.abs(spec.billingDuration * spec.billingIncrement - spec.price) < 0.01;
521
- };
522
-
523
513
  /**
524
514
  * Build a lean offer for shelf display. Keeps only:
525
515
  * - ListPrice, SalePrice, SRP price types
526
516
  * - PIX installment (name?.toUpperCase() === "PIX")
527
- * - Best no-interest installment (highest billingDuration)
517
+ * - Best installment, chosen with the SAME `bestInstallment` heuristic `useOffer`
518
+ * applies on PLP/PDP (lowest total price, tie-broken by highest billingDuration).
519
+ * Reusing it means shelf cards and detail pages always show the same installment.
528
520
  * Drops: inventoryLevel, giftSkuIds, priceValidUntil
529
521
  */
530
- const buildOfferShelf = (offer: Offer): Offer => {
522
+ export const buildOfferShelf = (offer: Offer): Offer => {
531
523
  const leanSpecs: UnitPriceSpecification[] = [];
532
-
533
- let bestNoInterest: UnitPriceSpecification | null = null;
524
+ const installmentSpecs: UnitPriceSpecification[] = [];
534
525
 
535
526
  for (const spec of offer.priceSpecification ?? []) {
536
- // Keep base price types
527
+ // Keep base (non-installment) price types
537
528
  if (
538
- spec.priceType === SCHEMA_LIST_PRICE ||
539
- spec.priceType === SCHEMA_SALE_PRICE ||
540
- spec.priceType === SCHEMA_SRP
529
+ (spec.priceType === SCHEMA_LIST_PRICE ||
530
+ spec.priceType === SCHEMA_SALE_PRICE ||
531
+ spec.priceType === SCHEMA_SRP) &&
532
+ spec.priceComponentType !== SCHEMA_INSTALLMENT
541
533
  ) {
542
- if (spec.priceComponentType !== SCHEMA_INSTALLMENT) {
543
- leanSpecs.push(spec);
544
- continue;
545
- }
534
+ leanSpecs.push(spec);
535
+ continue;
546
536
  }
547
537
 
548
- // Keep PIX installment
538
+ // Keep PIX installment (also feeds the bestInstallment reducer below via leanSpecs)
549
539
  if (spec.priceComponentType === SCHEMA_INSTALLMENT && spec.name?.toUpperCase() === "PIX") {
550
540
  leanSpecs.push(spec);
551
541
  continue;
552
542
  }
553
543
 
554
- // Track best no-interest installment (highest billingDuration)
555
- if (
556
- spec.priceComponentType === SCHEMA_INSTALLMENT &&
557
- isNoInterest(spec) &&
558
- (bestNoInterest == null ||
559
- (spec.billingDuration ?? 0) > (bestNoInterest.billingDuration ?? 0))
560
- ) {
561
- bestNoInterest = spec;
544
+ if (spec.priceComponentType === SCHEMA_INSTALLMENT) {
545
+ installmentSpecs.push(spec);
562
546
  }
563
547
  }
564
548
 
565
- if (bestNoInterest) {
566
- leanSpecs.push(bestNoInterest);
549
+ // Keep the single installment useOffer would pick from the full list. PIX is kept
550
+ // separately above, so useOffer on the lean offer still reduces over {PIX, best}
551
+ // and matches the detail page's reduce over the full set.
552
+ const best = installmentSpecs.reduce(bestInstallment, null as UnitPriceSpecification | null);
553
+ if (best) {
554
+ leanSpecs.push(best);
567
555
  }
568
556
 
569
557
  return {