diff --git a/packages/core-components/package.json b/packages/core-components/package.json index d763973b..3d1cfead 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -56,6 +56,6 @@ }, "dependencies": { "@commercelayer/js-auth": "^8.0.0", - "@commercelayer/sdk": "https://pkg.pr.new/@commercelayer/sdk@57f8b0a" + "@commercelayer/sdk": "^8.0.0" } } diff --git a/packages/core-components/src/payment_sessions/createdWithVersion.spec.ts b/packages/core-components/src/payment_sessions/createdWithVersion.spec.ts new file mode 100644 index 00000000..2a3afbf0 --- /dev/null +++ b/packages/core-components/src/payment_sessions/createdWithVersion.spec.ts @@ -0,0 +1,33 @@ +import type { Order } from "@commercelayer/sdk" +import { describe, expect, it } from "vitest" +import { isCreatedWithVersion } from "./createdWithVersion" + +// The version as the SDK returns it: under the resource's `meta`. +const order = (createdWithVersion?: string): Order => + ({ + id: "order-1", + ...(createdWithVersion != null ? { meta: { created_with_version: createdWithVersion } } : {}), + }) as unknown as Order + +describe("isCreatedWithVersion", () => { + it("compares the version the API reports in the order's meta", () => { + expect(isCreatedWithVersion(order("2026-05"), "2026-05")).toBe(true) + expect(isCreatedWithVersion(order("2017-08"), "2026-05")).toBe(false) + }) + + it("matches the version exactly", () => { + expect(isCreatedWithVersion(order("2027-01"), "2026-05")).toBe(false) + }) + + it("takes only a version the SDK supports", () => { + // @ts-expect-error a mistyped version is a compile error, not a silent false + expect(isCreatedWithVersion(order("2026-05"), "2026-5")).toBe(false) + }) + + it("is undefined when the order does not say which version created it", () => { + expect(isCreatedWithVersion(order(), "2026-05")).toBeUndefined() + expect(isCreatedWithVersion(order(""), "2026-05")).toBeUndefined() + expect(isCreatedWithVersion(undefined, "2026-05")).toBeUndefined() + expect(isCreatedWithVersion(null, "2026-05")).toBeUndefined() + }) +}) diff --git a/packages/core-components/src/payment_sessions/createdWithVersion.ts b/packages/core-components/src/payment_sessions/createdWithVersion.ts new file mode 100644 index 00000000..c01bd583 --- /dev/null +++ b/packages/core-components/src/payment_sessions/createdWithVersion.ts @@ -0,0 +1,27 @@ +import type { ApiVersion, Order } from "@commercelayer/sdk" + +/** + * Whether the order was created with exactly this API version, e.g. `2026-05`. + * + * It matters because Core API fixes some of an order's behaviour once, at + * creation, whatever version later requests use: an order created with + * `2026-05` is on the payment engine where the older order-level gift card is + * neither validated nor applied, and one created with `2017-08` keeps it even + * when read with `2026-05`. + * + * The version is the one the API reports in the order's `meta`. `version` is + * one the SDK supports, so a mistyped one fails to compile rather than silently + * never matching. + * + * Undefined when the order does not say which version created it — the API did + * not report it, or the order was not fetched from the API — so callers can + * keep deciding the way they would without it. + */ +export function isCreatedWithVersion( + order: Order | null | undefined, + version: ApiVersion +): boolean | undefined { + const createdWith = order?.meta?.created_with_version + if (createdWith == null || createdWith === "") return undefined + return createdWith === version +} diff --git a/packages/core-components/src/payment_sessions/index.ts b/packages/core-components/src/payment_sessions/index.ts index acce729e..0ed45038 100644 --- a/packages/core-components/src/payment_sessions/index.ts +++ b/packages/core-components/src/payment_sessions/index.ts @@ -2,6 +2,7 @@ export { applyGiftCard } from "./applyGiftCard" export type { AuthorizeGiftCardSessionsResult } from "./authorizeGiftCardSessions" export { authorizeGiftCardSessions } from "./authorizeGiftCardSessions" export { buildGatewayReturnUrl } from "./buildGatewayReturnUrl" +export { isCreatedWithVersion } from "./createdWithVersion" export { createPaymentSession } from "./createPaymentSession" export type { PaymentSessionsState } from "./derivePaymentSessionsState" export { derivePaymentSessionsState } from "./derivePaymentSessionsState" diff --git a/packages/docs/package.json b/packages/docs/package.json index f55b947c..596ad2d3 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -21,7 +21,7 @@ "@babel/core": "^7.29.7", "@babel/preset-env": "^7.29.7", "@commercelayer/js-auth": "^8.0.0", - "@commercelayer/sdk": "https://pkg.pr.new/@commercelayer/sdk@57f8b0a", + "@commercelayer/sdk": "^8.0.0", "@mdx-js/react": "^3.1.1", "@storybook/addon-actions": "^9.0.8", "@storybook/addon-backgrounds": "^9.0.8", diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 39cccaeb..504aee14 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -64,7 +64,7 @@ "@commercelayer/core-components": "workspace:*", "@commercelayer/organization-config": "^2.8.4", "@commercelayer/react-hooks-components": "workspace:*", - "@commercelayer/sdk": "https://pkg.pr.new/@commercelayer/sdk@57f8b0a", + "@commercelayer/sdk": "^8.0.0", "@stripe/react-stripe-js": "^6.8.1", "@stripe/stripe-js": "^9.10.0", "@tanstack/react-table": "^8.21.3", diff --git a/packages/react-components/specs/gift_cards/GiftCardOrCouponForm.paymentsModel.spec.tsx b/packages/react-components/specs/gift_cards/GiftCardOrCouponForm.paymentsModel.spec.tsx index 8f09d2bd..1e3bdb67 100644 --- a/packages/react-components/specs/gift_cards/GiftCardOrCouponForm.paymentsModel.spec.tsx +++ b/packages/react-components/specs/gift_cards/GiftCardOrCouponForm.paymentsModel.spec.tsx @@ -65,3 +65,50 @@ describe("GiftCardOrCouponForm on the payment_sessions model", () => { expect(inputName()).toBe("gift_card_or_coupon_code") }) }) + +describe("GiftCardOrCouponForm by the API version the order was created with", () => { + // The API fixes an order's payment engine when it is created. On one created + // with 2026-05 the order-level gift card is neither validated nor applied, so + // the form offers the coupon alone — even with the older payment methods on + // offer, which the same order still accepts. + it("offers only the coupon on an order created with 2026-05", () => { + renderForm({ + id: "order-1", + available_payment_methods: [{ id: "pm-1" }], + meta: { created_with_version: "2026-05" }, + } as never) + expect(inputName()).toBe("coupon_code") + }) + + it("overrides an explicit gift_card_code request on an order created with 2026-05", () => { + renderForm( + { + id: "order-1", + available_payment_methods: [{ id: "pm-1" }], + meta: { created_with_version: "2026-05" }, + } as never, + "gift_card_code" + ) + expect(inputName()).toBe("coupon_code") + }) + + it("offers the combined field on an order created with 2017-08", () => { + renderForm({ + id: "order-1", + available_payment_methods: [{ id: "pm-1" }], + meta: { created_with_version: "2017-08" }, + } as never) + expect(inputName()).toBe("gift_card_or_coupon_code") + }) + + // The version is what the API decides on, so it wins over the payments model + // the order is on. + it("follows the version rather than the payments model", () => { + renderForm({ + id: "order-1", + available_payment_settings: [MANUAL], + meta: { created_with_version: "2017-08" }, + } as never) + expect(inputName()).toBe("gift_card_or_coupon_code") + }) +}) diff --git a/packages/react-components/src/components/gift_cards/GiftCardOrCouponForm.tsx b/packages/react-components/src/components/gift_cards/GiftCardOrCouponForm.tsx index 0021b26d..a8e4c422 100644 --- a/packages/react-components/src/components/gift_cards/GiftCardOrCouponForm.tsx +++ b/packages/react-components/src/components/gift_cards/GiftCardOrCouponForm.tsx @@ -1,3 +1,4 @@ +import { isCreatedWithVersion } from "@commercelayer/core-components" import type { Order } from "@commercelayer/sdk" import { useRapidForm } from "rapid-form" import { type JSX, useCallback, useContext, useEffect, useState } from "react" @@ -41,13 +42,21 @@ export function GiftCardOrCouponForm(props: Props): JSX.Element | null { // Derive the active code type from the current order state useEffect(() => { - // On the `payment_sessions` model a gift card is not an order-level code: - // it is spent by creating a Payment Session against a gift-card Payment - // Setting, so it appears among the payment methods instead. Writing - // `gift_card_code` on the order there is meaningless, and letting it - // through would silently apply a gift card that no session reflects — so - // this overrides an explicit `codeType` too, rather than trusting it. - if (paymentsModel === "payment_sessions") { + // An order created with API version 2026-05 takes only the coupon here. + // Its gift cards are spent by creating a Payment Session against a + // gift-card Payment Setting, and the API no longer validates nor applies + // the order-level one: `gift_card_code` is stored unchecked, and so is + // anything sent as `gift_card_or_coupon_code`, which tries the gift card + // first — a coupon typed there is kept as a gift card code and never + // discounts. This holds even when the order also offers the older + // payment methods, and it overrides an explicit `codeType` too. + // + // What decides it is the version the order was created with, since the API + // fixes the payment engine then. When the order does not say — the API did + // not report it — the payments model stands in for it. + const couponOnly = + isCreatedWithVersion(order, "2026-05") ?? paymentsModel === "payment_sessions" + if (couponOnly) { setType("coupon_code") return } diff --git a/packages/react-hooks-components/package.json b/packages/react-hooks-components/package.json index 21ca0771..cec2b9ad 100644 --- a/packages/react-hooks-components/package.json +++ b/packages/react-hooks-components/package.json @@ -50,7 +50,7 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", "@babel/core": "^8.0.1", - "@commercelayer/sdk": "https://pkg.pr.new/@commercelayer/sdk@57f8b0a", + "@commercelayer/sdk": "^8.0.0", "@rolldown/plugin-babel": "^0.2.3", "@testing-library/react": "^16.3.2", "@types/react": "^19.2.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a77d7503..f9515716 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,8 +63,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 '@commercelayer/sdk': - specifier: https://pkg.pr.new/@commercelayer/sdk@57f8b0a - version: https://pkg.pr.new/@commercelayer/sdk@57f8b0a + specifier: ^8.0.0 + version: 8.0.0 devDependencies: '@arethetypeswrong/cli': specifier: ^0.18.5 @@ -110,8 +110,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 '@commercelayer/sdk': - specifier: https://pkg.pr.new/@commercelayer/sdk@57f8b0a - version: https://pkg.pr.new/@commercelayer/sdk@57f8b0a + specifier: ^8.0.0 + version: 8.0.0 '@mdx-js/react': specifier: ^3.1.1 version: 3.1.1(@types/react@19.2.18)(react@19.2.8) @@ -188,8 +188,8 @@ importers: specifier: workspace:* version: link:../react-hooks-components '@commercelayer/sdk': - specifier: https://pkg.pr.new/@commercelayer/sdk@57f8b0a - version: https://pkg.pr.new/@commercelayer/sdk@57f8b0a + specifier: ^8.0.0 + version: 8.0.0 '@stripe/react-stripe-js': specifier: ^6.8.1 version: 6.8.1(@stripe/stripe-js@9.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -316,8 +316,8 @@ importers: specifier: ^8.0.1 version: 8.0.1 '@commercelayer/sdk': - specifier: https://pkg.pr.new/@commercelayer/sdk@57f8b0a - version: https://pkg.pr.new/@commercelayer/sdk@57f8b0a + specifier: ^8.0.0 + version: 8.0.0 '@rolldown/plugin-babel': specifier: ^0.2.3 version: 0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.7)(rolldown@1.2.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(yaml@2.9.0)) @@ -1113,9 +1113,8 @@ packages: resolution: {integrity: sha512-ZlgIhQx7vu89ZD3eOCSkCe7kNb5LgEGy0p4Gayi75sxfHfJRXKkoX2NC+nkp2pFosi4SBYhlQFcd/EtzStAQGw==} engines: {node: '>=18', pnpm: '>=7'} - '@commercelayer/sdk@https://pkg.pr.new/@commercelayer/sdk@57f8b0a': - resolution: {integrity: sha512-cWoOVjEpvet39vKAz5ZDKc04B7+fOzJjSEj9ryQ+5M4Tvylj6ssiJ7Mi6q2yh/TORlaqUj8IZv52SgvH7oCuXw==, tarball: https://pkg.pr.new/@commercelayer/sdk@57f8b0a} - version: 8.0.0-beta.13 + '@commercelayer/sdk@8.0.0': + resolution: {integrity: sha512-cdlMwi3wvK79hs9LTlE8nLsEx7JM1JNPJmtOCDRNzjG/sUiezfO96qDbQmP1Qd2PtUtapcAEqUgDmj59rtGlVw==} engines: {node: '>=20'} '@csstools/color-helpers@6.1.0': @@ -4853,7 +4852,7 @@ snapshots: '@commercelayer/js-auth': 7.4.2 merge-anything: 5.1.7 - '@commercelayer/sdk@https://pkg.pr.new/@commercelayer/sdk@57f8b0a': {} + '@commercelayer/sdk@8.0.0': {} '@csstools/color-helpers@6.1.0': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e90265d6..d522e25c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -35,6 +35,7 @@ minimumReleaseAgeExclude: - valibot@1.4.2 - nx@22.7.2 || 22.7.7 - nanoid@3.3.16 || 3.3.17 + - '@commercelayer/sdk' onlyBuiltDependencies: - esbuild