@rebuy/rebuy 2.32.0-rc.2 → 2.32.0-rc.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/pricing/index.ts", "../../src/pricing/amountToCents.ts", "../../src/pricing/computeCompareAtPrice.ts", "../../src/pricing/computeDisplayPrice.ts", "../../src/pricing/currencyDecimals.ts", "../../src/pricing/priceVariant.ts"],
4
- "sourcesContent": ["export * from '~/pricing/amountToCents';\nexport * from '~/pricing/computeCompareAtPrice';\nexport * from '~/pricing/computeDisplayPrice';\nexport * from '~/pricing/currencyDecimals';\nexport * from '~/pricing/priceVariant';\nexport * from '~/pricing/types';\n", "const round = (value: number, precision = 0): number => {\n const factor = 10 ** precision;\n\n return Math.round(value * factor) / factor;\n};\n\n/**\n * A monetary amount in integer minor units. `decimals` is the currency's minor-unit\n * exponent (2 for USD/EUR, 0 for JPY, 3 for IQD/KWD) and defaults to 2 \u2014 resolve it\n * with `currencyDecimals(code)`. Rounds to that precision first so float drift\n * (19.999 \u2192 20.00 \u2192 2000 at 2 decimals) doesn't leak in; non-finite input \u2192 0. Ported\n * from the React checkout extension's `amountToCents`, generalized off its hard 2-decimal\n * assumption so zero- and three-decimal currencies are exact.\n */\nexport const amountToCents = (amount: number | string | null | undefined, decimals = 2): number => {\n const value = Number(amount);\n\n return Number.isFinite(value) ? round(round(value, decimals) * 10 ** decimals) : 0;\n};\n", "import { type Discount } from '~/pricing/types';\n\n/**\n * The strikethrough (compare-at) amount on an ALREADY-localized market price \u2014 ported from the React\n * extension's `getVariantCompareAtPrice`. Uses the variant compare-at when there's no discount or the\n * discount is taken from it; otherwise the (pre-discount) price is the strike. Returns the price itself\n * when there's no genuine strike \u2014 callers decide to show it only when it exceeds the display price.\n *\n * `subscribed`/`subscriptionPrice`: ORACLE `getVariantCompareAtPrice` subscription branch \u2014 while\n * subscribed, the strike is the SUBSCRIPTION (allocation) price when a widget discount also stacks (the\n * display price is the further-discounted amount), else the one-time price; NEVER the variant compare-at.\n * Mirrors the subscribed display in `computeDisplayPrice` so the strike represents the real saving.\n */\nexport const computeCompareAtPrice = (\n price: number,\n compareAtPrice: number | null | undefined,\n widgetDiscount?: Discount,\n subscribed?: boolean,\n subscriptionPrice?: number\n): number => {\n const hasWidgetDiscount = !!widgetDiscount?.type && widgetDiscount.type !== 'none';\n\n if (\n subscribed &&\n subscriptionPrice !== undefined &&\n Number.isFinite(subscriptionPrice) &&\n subscriptionPrice < price\n )\n return hasWidgetDiscount ? subscriptionPrice : price;\n\n const compareAt = Number(compareAtPrice);\n const isValidCompareAt = Number.isFinite(compareAt) && Number.isFinite(price) && compareAt > price;\n const useCompareAt = !hasWidgetDiscount || widgetDiscount?.discountedFrom === 'compare_at_price';\n\n return useCompareAt && isValidCompareAt ? compareAt : price;\n};\n", "import { amountToCents } from '~/pricing/amountToCents';\nimport { type Discount } from '~/pricing/types';\n\nconst isActive = (discount?: Discount): boolean => !!discount?.type && discount.type !== 'none';\n\n/**\n * The displayed (discounted) price, computed on an ALREADY-localized market price.\n * Ported from the React checkout extension's `getVariantPrice`, minus the localization\n * multiplier (Shopify localizes upstream): a data-source (product) discount takes\n * precedence over the widget discount; never returns negative.\n *\n * `decimals` is the currency's minor-unit exponent (2 for USD/EUR, 0 for JPY/KRW,\n * 3 for IQD/KWD/BHD) and defaults to 2 \u2014 resolve it with `currencyDecimals(code)`. All\n * discount math runs in that currency's minor units so zero- and three-decimal currencies\n * are exact (the source hard-coded hundredths). The output keeps whichever precision is\n * finer, the currency's or the price's own decimals, so an over-precise localized price\n * isn't truncated.\n *\n * `conversionRate` (buyer price \u00F7 shop price) converts FIXED discount amounts \u2014 stored in\n * shop currency \u2014 into the market currency before subtracting, mirroring both the discount\n * Function (`convertShopFixedAmount`: shop minor \u00F7 10^shopDecimals \u00D7 rate, rounded to buyer\n * precision) and the React source (which subtracted in shop cents, then localized the result).\n * Percentages are scale-free; a missing/zero/non-finite rate falls back to 1 (same-currency).\n *\n * `shopDecimals` is the SHOP currency's minor-unit exponent and matters ONLY for a\n * Functions-applied `fixed` discount, whose `amount` arrives in shop MINOR units: the buyer-minor\n * discount is `amount \u00D7 rate \u00D7 10^(decimals \u2212 shopDecimals)`, so a 500\u00A2 ($5) discount at 147\u00A5/$\n * is \u00A5735, not \u00A573,500. It defaults to `decimals` (same-currency, where the exponent gap is 0), so\n * same-currency callers are unchanged; supply the real shop exponent for a cross-decimal market.\n * `fixed_amount` and non-Functions `fixed` amounts are shop MAJOR units and never need it.\n *\n * `subscriptionPercentage` is the selected selling plan's discount % \u2014 it STACKS additively\n * with the widget/product discount off the same base (the source pushed both into one\n * discounts array over one price). Scale-free like any percentage; 0/omitted = one-time.\n *\n * `subscriptionPrice` is Shopify's OWN per-cycle allocation price (buyer major units, e.g. from an\n * `@inContext` `sellingPlanAllocations` query) and, when supplied, forecasts the selling-plan saving\n * EXACTLY as `price \u2212 subscriptionPrice` instead of the floored percentage \u2014 so a fixed-price/fixed-amount\n * plan lands on the Function's cent, not a percent-derived near-miss (C-45). It takes precedence over\n * `subscriptionPercentage` (which stays the source of the whole-percent LABEL); omit it for percentage\n * plans or when no allocation price is available.\n */\nexport const computeDisplayPrice = (\n price: number,\n compareAtPrice: number | null | undefined,\n widgetDiscount?: Discount,\n productDiscount?: Discount,\n decimals = 2,\n conversionRate = 1,\n subscriptionPercentage = 0,\n shopDecimals = decimals,\n subscriptionPrice: number | undefined = undefined\n): number => {\n const hasWidget = isActive(widgetDiscount);\n const isFunctions = widgetDiscount?.discountedBy === 'functions';\n\n // Discount off the compare-at price when the widget opts in (and isn't a Function).\n const base =\n hasWidget && !isFunctions && widgetDiscount!.discountedFrom === 'compare_at_price' && compareAtPrice\n ? compareAtPrice\n : price;\n\n const discount = isActive(productDiscount) ? productDiscount : hasWidget ? widgetDiscount : undefined;\n const subscription =\n Number.isFinite(subscriptionPercentage) && subscriptionPercentage > 0 ? subscriptionPercentage : 0;\n\n /**\n * The selling-plan saving in minor units: Shopify's exact allocation saving (`price \u2212 subscriptionPrice`)\n * when the caller supplies the allocation price \u2014 clamped at 0 so a non-discounting plan never adds\n * back \u2014 otherwise the floored percentage off the same base as the other discount (legacy summed both,\n * never compounded). The exact amount is why a fixed plan tracks the Function to the cent (C-45).\n */\n const subscriptionMinor =\n subscriptionPrice !== undefined && Number.isFinite(subscriptionPrice)\n ? Math.max(0, amountToCents(price, decimals) - amountToCents(subscriptionPrice, decimals))\n : subscription\n ? Math.floor(amountToCents(base, decimals) * (subscription / 100))\n : 0;\n\n if (!discount && !subscriptionMinor) return base;\n\n const rate = Number.isFinite(conversionRate) && conversionRate > 0 ? conversionRate : 1;\n let minor = 0; // total discount amount in the currency's minor units\n\n if (discount) {\n const amount = Number(discount.amount);\n\n // Malformed amounts (real merchant data carries shapes like \"10%\") must not forecast the\n // item as free via NaN falling through the final clamp \u2014 skip this discount instead (a\n // stacked subscription discount still applies).\n if (Number.isFinite(amount)) {\n if (discount.type === 'percentage')\n /**\n * ORACLE: the product-discount Function emits a `percentage` that Shopify applies to the\n * LINE cost. On a subscribed line that cost is the allocation price, so a stacked % applies\n * to `base \u2212 subscriptionSaving`, NOT the one-time base \u2014 else the card under-quotes vs the\n * cart ($100 plan-20% widget-10%: $72, not $70). `subscriptionMinor` is 0 for a one-time\n * purchase, so this is `base \u00D7 amount%` unchanged there. Fixed amounts are flat \u2192 unaffected.\n */\n minor = Math.floor((amountToCents(base, decimals) - subscriptionMinor) * (amount / 100));\n else if (discount.type === 'fixed' && hasWidget && isFunctions)\n // Functions send the amount in the SHOP currency's MINOR units; rebase to the buyer's minor\n // units via the rate and the shop-vs-buyer decimal gap, so a 500\u00A2 ($5) discount at 147\u00A5/$ is\n // \u00A5735 (500 \u00D7 147 \u00D7 10^(0\u22122)), not \u00A573,500. Same-currency (shopDecimals === decimals) is a no-op.\n minor = Math.round(amount * rate * 10 ** (decimals - shopDecimals));\n else if (discount.type === 'fixed' || discount.type === 'fixed_amount')\n // Otherwise the amount is in shop MAJOR units (`fixed_amount` always; `fixed` unless applied\n // by Functions). Apply the rate to the major amount and round ONCE at the buyer's precision:\n // a $4.35 discount at 147\u00A5/$ is \u00A5639 (round(639.45)), never \u00A5588 (rounding $4.35 \u2192 $4 first).\n minor = Math.round(amount * rate * 10 ** decimals);\n }\n }\n\n minor += subscriptionMinor;\n\n // Scale into whichever precision is finer \u2014 the currency's or the price's own \u2014 so the\n // minor-unit discount lines up, then clamp at zero.\n const priceDecimals = (base.toString().split('.')[1] ?? '').length;\n const outDecimals = Math.max(decimals, priceDecimals);\n const scale = 10 ** outDecimals;\n const result = (Math.round(base * scale) - minor * 10 ** (outDecimals - decimals)) / scale;\n\n return result > 0 ? result : 0;\n};\n", "/**\n * ISO 4217 minor-unit exponents that differ from the *display* digits CLDR reports via\n * `Intl`. CLDR reflects practical display habits \u2014 e.g. it renders IQD with 0 decimals\n * because Iraqi fils are effectively unused \u2014 but pricing math needs the currency's actual\n * minor unit. Seed this map with those divergences.\n *\n * IQD is ISO 4217 exponent 3 (matching onsite-js's table); `Intl` reports 0. Confirm how\n * Shopify actually denominates the amount for a given market before trusting a value here.\n */\nconst MINOR_UNIT_OVERRIDES: Record<string, number> = {\n IQD: 3,\n};\n\n/** Resolved exponents by (upper-cased) code \u2014 this runs per variant per render, and `Intl.NumberFormat` is not free. */\nconst CACHE = new Map<string, number>();\n\n/**\n * The minor-unit exponent (number of decimal places) for an ISO 4217 currency code \u2014\n * 2 for USD/EUR, 0 for JPY/KRW/CLP, 3 for KWD/BHD/OMR (and IQD via override). Unknown,\n * empty, or invalid codes fall back to 2.\n *\n * We derive this from `Intl` (Unicode CLDR) rather than a hand-maintained table on purpose:\n * the currency tables scattered across the other repos carry data bugs (KRW marked\n * 2-decimal, CLP marked both 0 and 3, no Gulf 3-decimal currencies at all). CLDR is the\n * baseline; MINOR_UNIT_OVERRIDES patches only the ISO-4217-vs-display divergences.\n */\nexport const currencyDecimals = (currencyCode?: string | null): number => {\n if (!currencyCode) return 2;\n\n const code = currencyCode.toUpperCase();\n const override = MINOR_UNIT_OVERRIDES[code];\n\n if (override !== undefined) return override;\n\n const cached = CACHE.get(code);\n\n if (cached !== undefined) return cached;\n\n let decimals = 2;\n\n try {\n const { maximumFractionDigits } = new Intl.NumberFormat('en', {\n currency: code,\n style: 'currency',\n }).resolvedOptions();\n\n /* v8 ignore next -- maximumFractionDigits is always set for a currency formatter; the fallback is unreachable */\n decimals = maximumFractionDigits ?? 2;\n } catch {\n decimals = 2;\n }\n\n CACHE.set(code, decimals);\n\n return decimals;\n};\n", "import { computeCompareAtPrice } from '~/pricing/computeCompareAtPrice';\nimport { computeDisplayPrice } from '~/pricing/computeDisplayPrice';\nimport { currencyDecimals } from '~/pricing/currencyDecimals';\nimport { type PricedVariant, type PriceVariantInput } from '~/pricing/types';\n\n/**\n * \u2550\u2550\u2550 Offer-card money forecast \u2014 the single source of truth \u2550\u2550\u2550\n *\n * `priceVariant` is the one pure entry point; `computeDisplayPrice` and `computeCompareAtPrice` are its\n * leaves. INVARIANT: **card == cart** \u2014 the forecast mirrors the deployed product-discount Function (the\n * charge) and Shopify's line-cost semantics; it never sets a price, it only predicts one for display.\n *\n * ORACLE HIERARCHY (which source defines \"correct\", cited per branch below):\n * - **Discount math** \u2192 the deployed `product-discount` Shopify Function. A divergence from it is a bug\n * HERE, unless the Function itself contradicts an external spec.\n * - **Subscription / compare-at DISPLAY parity** \u2192 legacy `getVariantPrice` / `getVariantCompareAtPrice`.\n * - **Currency minor units** \u2192 ISO 4217 / CLDR (`currencyDecimals`). Where the Function contradicts ISO\n * (the cross-decimal fixed-discount bug) the fix belongs in the Function \u2014 see the escalation\n * `product-discount/JIRA-fixed-amount-currency-conversion.md`; this forecast already computes the\n * correct shop-decimal math, so `card == cart` follows once the Function ships. Same-currency is aligned\n * today; cross-decimal markets stay gated on the extension side until then.\n */\n\n/**\n * Price one variant from its already-localized market price: returns the display price\n * and the strikethrough compare-at, which is null unless it genuinely exceeds the display\n * price (so a non-discounted, non-sale variant shows no strike). Pass `currencyCode` so\n * discount math uses the right minor unit for zero- and three-decimal currencies; it\n * defaults to two-decimal behavior when omitted. `shopDecimals` (the shop currency's exponent)\n * defaults to the buyer currency's decimals, so same-currency callers are unaffected; supply the\n * real shop exponent so a Functions-applied fixed discount converts correctly in a cross-decimal market.\n * `subscriptionPrice` (Shopify's exact allocation price, buyer major units) forecasts the selling-plan\n * saving to the cent \u2014 see {@link PriceVariantInput}.\n */\nexport const priceVariant = ({\n compareAtPrice,\n conversionRate,\n currencyCode,\n price,\n productDiscount,\n shopDecimals,\n subscriptionPercentage,\n subscriptionPrice,\n widgetDiscount,\n}: PriceVariantInput): PricedVariant => {\n const decimals = currencyDecimals(currencyCode);\n const display = computeDisplayPrice(\n price,\n compareAtPrice,\n widgetDiscount,\n productDiscount,\n decimals,\n conversionRate,\n subscriptionPercentage,\n shopDecimals ?? decimals,\n subscriptionPrice\n );\n\n /**\n * The selling-plan allocation (subscription) price \u2014 Shopify's exact value when supplied, else derived\n * from the whole-percent plan discount \u2014 feeds the strike so it mirrors the subscribed display.\n */\n const subscribed = subscriptionPrice !== undefined || (subscriptionPercentage ?? 0) > 0;\n /** `price` when not subscribed (unused \u2014 the strike's subscription branch is gated on `subscribed`). */\n const allocationPrice = subscriptionPrice ?? price * (1 - (subscriptionPercentage ?? 0) / 100);\n\n const strike = computeCompareAtPrice(price, compareAtPrice, widgetDiscount, subscribed, allocationPrice);\n const compareAt = strike > display ? strike : null;\n\n /** Explicit saving vs the shown strike (0 when nothing is struck) for the token layer, so it isn't re-derived per call site. Rounded to kill subtraction float noise (18.4 \u2212 13.8 = 4.600000000000001). */\n const savings = compareAt !== null ? Math.round((compareAt - display) * 1e6) / 1e6 : 0;\n const savingsPercentage = compareAt ? Math.round((savings / compareAt) * 100) : 0;\n\n return { compareAtPrice: compareAt, price: display, savings, savingsPercentage };\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export * from '~/pricing/amountToCents';\nexport * from '~/pricing/computeCompareAtPrice';\nexport * from '~/pricing/computeDisplayPrice';\nexport * from '~/pricing/currencyDecimals';\nexport * from '~/pricing/priceVariant';\nexport * from '~/pricing/types';\n", "const round = (value: number, precision = 0): number => {\n const factor = 10 ** precision;\n\n return Math.round(value * factor) / factor;\n};\n\n/**\n * A monetary amount in integer minor units. `decimals` is the currency's minor-unit\n * exponent (2 for USD/EUR, 0 for JPY, 3 for IQD/KWD) and defaults to 2 \u2014 resolve it\n * with `currencyDecimals(code)`. Rounds to that precision first so float drift\n * (19.999 \u2192 20.00 \u2192 2000 at 2 decimals) doesn't leak in; non-finite input \u2192 0. Ported\n * from the React checkout extension's `amountToCents`, generalized off its hard 2-decimal\n * assumption so zero- and three-decimal currencies are exact.\n */\nexport const amountToCents = (amount: number | string | null | undefined, decimals = 2): number => {\n const value = Number(amount);\n\n return Number.isFinite(value) ? round(round(value, decimals) * 10 ** decimals) : 0;\n};\n", "import { type Discount } from '~/pricing/types';\n\n/**\n * The strikethrough (compare-at) amount on an ALREADY-localized market price \u2014 ported from the React\n * extension's `getVariantCompareAtPrice`. Uses the variant compare-at when there's no discount or the\n * discount is taken from it; otherwise the (pre-discount) price is the strike. Returns the price itself\n * when there's no genuine strike \u2014 callers decide to show it only when it exceeds the display price.\n *\n * `subscribed`/`subscriptionPrice`: ORACLE `getVariantCompareAtPrice` subscription branch \u2014 while\n * subscribed, the strike is the SUBSCRIPTION (allocation) price when a widget discount also stacks (the\n * display price is the further-discounted amount), else the one-time price; NEVER the variant compare-at.\n * Mirrors the subscribed display in `computeDisplayPrice` so the strike represents the real saving.\n */\nexport const computeCompareAtPrice = (\n price: number,\n compareAtPrice: number | null | undefined,\n widgetDiscount?: Discount,\n subscribed?: boolean,\n subscriptionPrice?: number\n): number => {\n const hasWidgetDiscount = !!widgetDiscount?.type && widgetDiscount.type !== 'none';\n\n if (\n subscribed &&\n subscriptionPrice !== undefined &&\n Number.isFinite(subscriptionPrice) &&\n subscriptionPrice < price\n )\n return hasWidgetDiscount ? subscriptionPrice : price;\n\n const compareAt = Number(compareAtPrice);\n const isValidCompareAt = Number.isFinite(compareAt) && Number.isFinite(price) && compareAt > price;\n const useCompareAt = !hasWidgetDiscount || widgetDiscount?.discountedFrom === 'compare_at_price';\n\n return useCompareAt && isValidCompareAt ? compareAt : price;\n};\n", "import { amountToCents } from '~/pricing/amountToCents';\nimport { type Discount } from '~/pricing/types';\n\nconst isActive = (discount?: Discount): boolean => !!discount?.type && discount.type !== 'none';\n\n/**\n * The displayed (discounted) price, computed on an ALREADY-localized market price.\n * Ported from the React checkout extension's `getVariantPrice`, minus the localization\n * multiplier (Shopify localizes upstream): a data-source (product) discount takes\n * precedence over the widget discount; never returns negative.\n *\n * `decimals` is the currency's minor-unit exponent (2 for USD/EUR, 0 for JPY/KRW,\n * 3 for IQD/KWD/BHD) and defaults to 2 \u2014 resolve it with `currencyDecimals(code)`. All\n * discount math runs in that currency's minor units so zero- and three-decimal currencies\n * are exact (the source hard-coded hundredths). The output keeps whichever precision is\n * finer, the currency's or the price's own decimals, so an over-precise localized price\n * isn't truncated.\n *\n * `conversionRate` (buyer price \u00F7 shop price) converts FIXED discount amounts \u2014 stored in\n * shop currency \u2014 into the market currency before subtracting, mirroring both the discount\n * Function (`convertShopFixedAmount`: shop minor \u00F7 10^shopDecimals \u00D7 rate, rounded to buyer\n * precision) and the React source (which subtracted in shop cents, then localized the result).\n * Percentages are scale-free; a missing/zero/non-finite rate falls back to 1 (same-currency).\n *\n * `shopDecimals` is the SHOP currency's minor-unit exponent and matters ONLY for a\n * Functions-applied `fixed` discount, whose `amount` arrives in shop MINOR units: the buyer-minor\n * discount is `amount \u00D7 rate \u00D7 10^(decimals \u2212 shopDecimals)`, so a 500\u00A2 ($5) discount at 147\u00A5/$\n * is \u00A5735, not \u00A573,500. It defaults to `decimals` (same-currency, where the exponent gap is 0), so\n * same-currency callers are unchanged; supply the real shop exponent for a cross-decimal market.\n * `fixed_amount` and non-Functions `fixed` amounts are shop MAJOR units and never need it.\n *\n * `subscriptionPercentage` is the selected selling plan's discount % \u2014 it STACKS additively\n * with the widget/product discount off the same base (the source pushed both into one\n * discounts array over one price). Scale-free like any percentage; 0/omitted = one-time.\n *\n * `subscriptionPrice` is Shopify's OWN per-cycle allocation price (buyer major units, e.g. from an\n * `@inContext` `sellingPlanAllocations` query) and, when supplied, forecasts the selling-plan saving\n * EXACTLY as `price \u2212 subscriptionPrice` instead of the floored percentage \u2014 so a fixed-price/fixed-amount\n * plan lands on the Function's cent, not a percent-derived near-miss (C-45). It takes precedence over\n * `subscriptionPercentage` (which stays the source of the whole-percent LABEL); omit it for percentage\n * plans or when no allocation price is available.\n */\nexport const computeDisplayPrice = (\n price: number,\n compareAtPrice: number | null | undefined,\n widgetDiscount?: Discount,\n productDiscount?: Discount,\n decimals = 2,\n conversionRate = 1,\n subscriptionPercentage = 0,\n shopDecimals = decimals,\n subscriptionPrice: number | undefined = undefined\n): number => {\n const hasWidget = isActive(widgetDiscount);\n const isFunctions = widgetDiscount?.discountedBy === 'functions';\n\n // Discount off the compare-at price when the widget opts in (and isn't a Function).\n const base =\n hasWidget && !isFunctions && widgetDiscount!.discountedFrom === 'compare_at_price' && compareAtPrice\n ? compareAtPrice\n : price;\n\n const discount = isActive(productDiscount) ? productDiscount : hasWidget ? widgetDiscount : undefined;\n const subscription =\n Number.isFinite(subscriptionPercentage) && subscriptionPercentage > 0 ? subscriptionPercentage : 0;\n\n /**\n * The selling-plan saving in minor units: Shopify's exact allocation saving (`price \u2212 subscriptionPrice`)\n * when the caller supplies the allocation price \u2014 clamped at 0 so a non-discounting plan never adds\n * back \u2014 otherwise the floored percentage off the same base as the other discount (legacy summed both,\n * never compounded). The exact amount is why a fixed plan tracks the Function to the cent (C-45).\n */\n const subscriptionMinor =\n subscriptionPrice !== undefined && Number.isFinite(subscriptionPrice)\n ? Math.max(0, amountToCents(price, decimals) - amountToCents(subscriptionPrice, decimals))\n : subscription\n ? Math.floor(amountToCents(base, decimals) * (subscription / 100))\n : 0;\n\n if (!discount && !subscriptionMinor) return base;\n\n const rate = Number.isFinite(conversionRate) && conversionRate > 0 ? conversionRate : 1;\n let minor = 0; // total discount amount in the currency's minor units\n\n if (discount) {\n const amount = Number(discount.amount);\n\n // Malformed amounts (real merchant data carries shapes like \"10%\") must not forecast the\n // item as free via NaN falling through the final clamp \u2014 skip this discount instead (a\n // stacked subscription discount still applies).\n if (Number.isFinite(amount)) {\n if (discount.type === 'percentage')\n /**\n * ORACLE: the product-discount Function emits a `percentage` that Shopify applies to the\n * LINE cost. On a subscribed line that cost is the allocation price, so a stacked % applies\n * to `base \u2212 subscriptionSaving`, NOT the one-time base \u2014 else the card under-quotes vs the\n * cart ($100 plan-20% widget-10%: $72, not $70). `subscriptionMinor` is 0 for a one-time\n * purchase, so this is `base \u00D7 amount%` unchanged there. Fixed amounts are flat \u2192 unaffected.\n */\n minor = Math.floor((amountToCents(base, decimals) - subscriptionMinor) * (amount / 100));\n else if (discount.type === 'fixed' && hasWidget && isFunctions)\n // Functions send the amount in the SHOP currency's MINOR units; rebase to the buyer's minor\n // units via the rate and the shop-vs-buyer decimal gap, so a 500\u00A2 ($5) discount at 147\u00A5/$ is\n // \u00A5735 (500 \u00D7 147 \u00D7 10^(0\u22122)), not \u00A573,500. Same-currency (shopDecimals === decimals) is a no-op.\n minor = Math.round(amount * rate * 10 ** (decimals - shopDecimals));\n else if (discount.type === 'fixed' || discount.type === 'fixed_amount')\n // Otherwise the amount is in shop MAJOR units (`fixed_amount` always; `fixed` unless applied\n // by Functions). Apply the rate to the major amount and round ONCE at the buyer's precision:\n // a $4.35 discount at 147\u00A5/$ is \u00A5639 (round(639.45)), never \u00A5588 (rounding $4.35 \u2192 $4 first).\n minor = Math.round(amount * rate * 10 ** decimals);\n }\n }\n\n minor += subscriptionMinor;\n\n // Scale into whichever precision is finer \u2014 the currency's or the price's own \u2014 so the\n // minor-unit discount lines up, then clamp at zero.\n const priceDecimals = (base.toString().split('.')[1] ?? '').length;\n const outDecimals = Math.max(decimals, priceDecimals);\n const scale = 10 ** outDecimals;\n const result = (Math.round(base * scale) - minor * 10 ** (outDecimals - decimals)) / scale;\n\n return result > 0 ? result : 0;\n};\n", "/**\n * ISO 4217 minor-unit exponents that differ from the *display* digits CLDR reports via\n * `Intl`. CLDR reflects practical display habits \u2014 e.g. it renders IQD with 0 decimals\n * because Iraqi fils are effectively unused \u2014 but pricing math needs the currency's actual\n * minor unit. Seed this map with those divergences.\n *\n * IQD is ISO 4217 exponent 3 (matching onsite-js's table); `Intl` reports 0. Confirm how\n * Shopify actually denominates the amount for a given market before trusting a value here.\n */\nconst MINOR_UNIT_OVERRIDES: Record<string, number> = {\n IQD: 3,\n};\n\n/** Resolved exponents by (upper-cased) code \u2014 this runs per variant per render, and `Intl.NumberFormat` is not free. */\nconst CACHE = new Map<string, number>();\n\n/**\n * The minor-unit exponent (number of decimal places) for an ISO 4217 currency code \u2014\n * 2 for USD/EUR, 0 for JPY/KRW/CLP, 3 for KWD/BHD/OMR (and IQD via override). Unknown,\n * empty, or invalid codes fall back to 2.\n *\n * We derive this from `Intl` (Unicode CLDR) rather than a hand-maintained table on purpose:\n * the currency tables scattered across the other repos carry data bugs (KRW marked\n * 2-decimal, CLP marked both 0 and 3, no Gulf 3-decimal currencies at all). CLDR is the\n * baseline; MINOR_UNIT_OVERRIDES patches only the ISO-4217-vs-display divergences.\n */\nexport const currencyDecimals = (currencyCode?: string | null): number => {\n if (!currencyCode) return 2;\n\n const code = currencyCode.toUpperCase();\n const override = MINOR_UNIT_OVERRIDES[code];\n\n if (override !== undefined) return override;\n\n const cached = CACHE.get(code);\n\n if (cached !== undefined) return cached;\n\n let decimals = 2;\n\n try {\n const { maximumFractionDigits } = new Intl.NumberFormat('en', {\n currency: code,\n style: 'currency',\n }).resolvedOptions();\n\n /* v8 ignore next -- maximumFractionDigits is always set for a currency formatter; the fallback is unreachable */\n decimals = maximumFractionDigits ?? 2;\n } catch {\n decimals = 2;\n }\n\n CACHE.set(code, decimals);\n\n return decimals;\n};\n", "import { computeCompareAtPrice } from '~/pricing/computeCompareAtPrice';\nimport { computeDisplayPrice } from '~/pricing/computeDisplayPrice';\nimport { currencyDecimals } from '~/pricing/currencyDecimals';\nimport { type PricedVariant, type PriceVariantInput } from '~/pricing/types';\n\n/**\n * \u2550\u2550\u2550 Offer-card money forecast \u2014 the single source of truth \u2550\u2550\u2550\n *\n * `priceVariant` is the one pure entry point; `computeDisplayPrice` and `computeCompareAtPrice` are its\n * leaves. INVARIANT: **card == cart** \u2014 the forecast mirrors the deployed product-discount Function (the\n * charge) and Shopify's line-cost semantics; it never sets a price, it only predicts one for display.\n *\n * ORACLE HIERARCHY (which source defines \"correct\", cited per branch below):\n * - **Discount math** \u2192 the deployed `product-discount` Shopify Function. A divergence from it is a bug\n * HERE, unless the Function itself contradicts an external spec.\n * - **Subscription / compare-at DISPLAY parity** \u2192 legacy `getVariantPrice` / `getVariantCompareAtPrice`.\n * - **Currency minor units** \u2192 ISO 4217 / CLDR (`currencyDecimals`). Where the Function contradicts ISO\n * (the cross-decimal fixed-discount bug) the fix belongs in the Function \u2014 tracked in REB-23607; this\n * forecast already computes the correct shop-decimal math, so `card == cart` follows once the Function\n * ships. Same-currency is aligned today; cross-decimal markets stay gated on the extension side until then.\n */\n\n/**\n * Price one variant from its already-localized market price: returns the display price\n * and the strikethrough compare-at, which is null unless it genuinely exceeds the display\n * price (so a non-discounted, non-sale variant shows no strike). Pass `currencyCode` so\n * discount math uses the right minor unit for zero- and three-decimal currencies; it\n * defaults to two-decimal behavior when omitted. `shopDecimals` (the shop currency's exponent)\n * defaults to the buyer currency's decimals, so same-currency callers are unaffected; supply the\n * real shop exponent so a Functions-applied fixed discount converts correctly in a cross-decimal market.\n * `subscriptionPrice` (Shopify's exact allocation price, buyer major units) forecasts the selling-plan\n * saving to the cent \u2014 see {@link PriceVariantInput}.\n */\nexport const priceVariant = ({\n compareAtPrice,\n conversionRate,\n currencyCode,\n price,\n productDiscount,\n shopDecimals,\n subscriptionPercentage,\n subscriptionPrice,\n widgetDiscount,\n}: PriceVariantInput): PricedVariant => {\n const decimals = currencyDecimals(currencyCode);\n const display = computeDisplayPrice(\n price,\n compareAtPrice,\n widgetDiscount,\n productDiscount,\n decimals,\n conversionRate,\n subscriptionPercentage,\n shopDecimals ?? decimals,\n subscriptionPrice\n );\n\n /**\n * The selling-plan allocation (subscription) price \u2014 Shopify's exact value when supplied, else derived\n * from the whole-percent plan discount \u2014 feeds the strike so it mirrors the subscribed display.\n */\n const subscribed = subscriptionPrice !== undefined || (subscriptionPercentage ?? 0) > 0;\n /** `price` when not subscribed (unused \u2014 the strike's subscription branch is gated on `subscribed`). */\n const allocationPrice = subscriptionPrice ?? price * (1 - (subscriptionPercentage ?? 0) / 100);\n\n const strike = computeCompareAtPrice(price, compareAtPrice, widgetDiscount, subscribed, allocationPrice);\n const compareAt = strike > display ? strike : null;\n\n /** Explicit saving vs the shown strike (0 when nothing is struck) for the token layer, so it isn't re-derived per call site. Rounded to kill subtraction float noise (18.4 \u2212 13.8 = 4.600000000000001). */\n const savings = compareAt !== null ? Math.round((compareAt - display) * 1e6) / 1e6 : 0;\n const savingsPercentage = compareAt ? Math.round((savings / compareAt) * 100) : 0;\n\n return { compareAtPrice: compareAt, price: display, savings, savingsPercentage };\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/pricing/amountToCents.ts", "../../src/pricing/computeCompareAtPrice.ts", "../../src/pricing/computeDisplayPrice.ts", "../../src/pricing/currencyDecimals.ts", "../../src/pricing/priceVariant.ts"],
4
- "sourcesContent": ["const round = (value: number, precision = 0): number => {\n const factor = 10 ** precision;\n\n return Math.round(value * factor) / factor;\n};\n\n/**\n * A monetary amount in integer minor units. `decimals` is the currency's minor-unit\n * exponent (2 for USD/EUR, 0 for JPY, 3 for IQD/KWD) and defaults to 2 \u2014 resolve it\n * with `currencyDecimals(code)`. Rounds to that precision first so float drift\n * (19.999 \u2192 20.00 \u2192 2000 at 2 decimals) doesn't leak in; non-finite input \u2192 0. Ported\n * from the React checkout extension's `amountToCents`, generalized off its hard 2-decimal\n * assumption so zero- and three-decimal currencies are exact.\n */\nexport const amountToCents = (amount: number | string | null | undefined, decimals = 2): number => {\n const value = Number(amount);\n\n return Number.isFinite(value) ? round(round(value, decimals) * 10 ** decimals) : 0;\n};\n", "import { type Discount } from '~/pricing/types';\n\n/**\n * The strikethrough (compare-at) amount on an ALREADY-localized market price \u2014 ported from the React\n * extension's `getVariantCompareAtPrice`. Uses the variant compare-at when there's no discount or the\n * discount is taken from it; otherwise the (pre-discount) price is the strike. Returns the price itself\n * when there's no genuine strike \u2014 callers decide to show it only when it exceeds the display price.\n *\n * `subscribed`/`subscriptionPrice`: ORACLE `getVariantCompareAtPrice` subscription branch \u2014 while\n * subscribed, the strike is the SUBSCRIPTION (allocation) price when a widget discount also stacks (the\n * display price is the further-discounted amount), else the one-time price; NEVER the variant compare-at.\n * Mirrors the subscribed display in `computeDisplayPrice` so the strike represents the real saving.\n */\nexport const computeCompareAtPrice = (\n price: number,\n compareAtPrice: number | null | undefined,\n widgetDiscount?: Discount,\n subscribed?: boolean,\n subscriptionPrice?: number\n): number => {\n const hasWidgetDiscount = !!widgetDiscount?.type && widgetDiscount.type !== 'none';\n\n if (\n subscribed &&\n subscriptionPrice !== undefined &&\n Number.isFinite(subscriptionPrice) &&\n subscriptionPrice < price\n )\n return hasWidgetDiscount ? subscriptionPrice : price;\n\n const compareAt = Number(compareAtPrice);\n const isValidCompareAt = Number.isFinite(compareAt) && Number.isFinite(price) && compareAt > price;\n const useCompareAt = !hasWidgetDiscount || widgetDiscount?.discountedFrom === 'compare_at_price';\n\n return useCompareAt && isValidCompareAt ? compareAt : price;\n};\n", "import { amountToCents } from '~/pricing/amountToCents';\nimport { type Discount } from '~/pricing/types';\n\nconst isActive = (discount?: Discount): boolean => !!discount?.type && discount.type !== 'none';\n\n/**\n * The displayed (discounted) price, computed on an ALREADY-localized market price.\n * Ported from the React checkout extension's `getVariantPrice`, minus the localization\n * multiplier (Shopify localizes upstream): a data-source (product) discount takes\n * precedence over the widget discount; never returns negative.\n *\n * `decimals` is the currency's minor-unit exponent (2 for USD/EUR, 0 for JPY/KRW,\n * 3 for IQD/KWD/BHD) and defaults to 2 \u2014 resolve it with `currencyDecimals(code)`. All\n * discount math runs in that currency's minor units so zero- and three-decimal currencies\n * are exact (the source hard-coded hundredths). The output keeps whichever precision is\n * finer, the currency's or the price's own decimals, so an over-precise localized price\n * isn't truncated.\n *\n * `conversionRate` (buyer price \u00F7 shop price) converts FIXED discount amounts \u2014 stored in\n * shop currency \u2014 into the market currency before subtracting, mirroring both the discount\n * Function (`convertShopFixedAmount`: shop minor \u00F7 10^shopDecimals \u00D7 rate, rounded to buyer\n * precision) and the React source (which subtracted in shop cents, then localized the result).\n * Percentages are scale-free; a missing/zero/non-finite rate falls back to 1 (same-currency).\n *\n * `shopDecimals` is the SHOP currency's minor-unit exponent and matters ONLY for a\n * Functions-applied `fixed` discount, whose `amount` arrives in shop MINOR units: the buyer-minor\n * discount is `amount \u00D7 rate \u00D7 10^(decimals \u2212 shopDecimals)`, so a 500\u00A2 ($5) discount at 147\u00A5/$\n * is \u00A5735, not \u00A573,500. It defaults to `decimals` (same-currency, where the exponent gap is 0), so\n * same-currency callers are unchanged; supply the real shop exponent for a cross-decimal market.\n * `fixed_amount` and non-Functions `fixed` amounts are shop MAJOR units and never need it.\n *\n * `subscriptionPercentage` is the selected selling plan's discount % \u2014 it STACKS additively\n * with the widget/product discount off the same base (the source pushed both into one\n * discounts array over one price). Scale-free like any percentage; 0/omitted = one-time.\n *\n * `subscriptionPrice` is Shopify's OWN per-cycle allocation price (buyer major units, e.g. from an\n * `@inContext` `sellingPlanAllocations` query) and, when supplied, forecasts the selling-plan saving\n * EXACTLY as `price \u2212 subscriptionPrice` instead of the floored percentage \u2014 so a fixed-price/fixed-amount\n * plan lands on the Function's cent, not a percent-derived near-miss (C-45). It takes precedence over\n * `subscriptionPercentage` (which stays the source of the whole-percent LABEL); omit it for percentage\n * plans or when no allocation price is available.\n */\nexport const computeDisplayPrice = (\n price: number,\n compareAtPrice: number | null | undefined,\n widgetDiscount?: Discount,\n productDiscount?: Discount,\n decimals = 2,\n conversionRate = 1,\n subscriptionPercentage = 0,\n shopDecimals = decimals,\n subscriptionPrice: number | undefined = undefined\n): number => {\n const hasWidget = isActive(widgetDiscount);\n const isFunctions = widgetDiscount?.discountedBy === 'functions';\n\n // Discount off the compare-at price when the widget opts in (and isn't a Function).\n const base =\n hasWidget && !isFunctions && widgetDiscount!.discountedFrom === 'compare_at_price' && compareAtPrice\n ? compareAtPrice\n : price;\n\n const discount = isActive(productDiscount) ? productDiscount : hasWidget ? widgetDiscount : undefined;\n const subscription =\n Number.isFinite(subscriptionPercentage) && subscriptionPercentage > 0 ? subscriptionPercentage : 0;\n\n /**\n * The selling-plan saving in minor units: Shopify's exact allocation saving (`price \u2212 subscriptionPrice`)\n * when the caller supplies the allocation price \u2014 clamped at 0 so a non-discounting plan never adds\n * back \u2014 otherwise the floored percentage off the same base as the other discount (legacy summed both,\n * never compounded). The exact amount is why a fixed plan tracks the Function to the cent (C-45).\n */\n const subscriptionMinor =\n subscriptionPrice !== undefined && Number.isFinite(subscriptionPrice)\n ? Math.max(0, amountToCents(price, decimals) - amountToCents(subscriptionPrice, decimals))\n : subscription\n ? Math.floor(amountToCents(base, decimals) * (subscription / 100))\n : 0;\n\n if (!discount && !subscriptionMinor) return base;\n\n const rate = Number.isFinite(conversionRate) && conversionRate > 0 ? conversionRate : 1;\n let minor = 0; // total discount amount in the currency's minor units\n\n if (discount) {\n const amount = Number(discount.amount);\n\n // Malformed amounts (real merchant data carries shapes like \"10%\") must not forecast the\n // item as free via NaN falling through the final clamp \u2014 skip this discount instead (a\n // stacked subscription discount still applies).\n if (Number.isFinite(amount)) {\n if (discount.type === 'percentage')\n /**\n * ORACLE: the product-discount Function emits a `percentage` that Shopify applies to the\n * LINE cost. On a subscribed line that cost is the allocation price, so a stacked % applies\n * to `base \u2212 subscriptionSaving`, NOT the one-time base \u2014 else the card under-quotes vs the\n * cart ($100 plan-20% widget-10%: $72, not $70). `subscriptionMinor` is 0 for a one-time\n * purchase, so this is `base \u00D7 amount%` unchanged there. Fixed amounts are flat \u2192 unaffected.\n */\n minor = Math.floor((amountToCents(base, decimals) - subscriptionMinor) * (amount / 100));\n else if (discount.type === 'fixed' && hasWidget && isFunctions)\n // Functions send the amount in the SHOP currency's MINOR units; rebase to the buyer's minor\n // units via the rate and the shop-vs-buyer decimal gap, so a 500\u00A2 ($5) discount at 147\u00A5/$ is\n // \u00A5735 (500 \u00D7 147 \u00D7 10^(0\u22122)), not \u00A573,500. Same-currency (shopDecimals === decimals) is a no-op.\n minor = Math.round(amount * rate * 10 ** (decimals - shopDecimals));\n else if (discount.type === 'fixed' || discount.type === 'fixed_amount')\n // Otherwise the amount is in shop MAJOR units (`fixed_amount` always; `fixed` unless applied\n // by Functions). Apply the rate to the major amount and round ONCE at the buyer's precision:\n // a $4.35 discount at 147\u00A5/$ is \u00A5639 (round(639.45)), never \u00A5588 (rounding $4.35 \u2192 $4 first).\n minor = Math.round(amount * rate * 10 ** decimals);\n }\n }\n\n minor += subscriptionMinor;\n\n // Scale into whichever precision is finer \u2014 the currency's or the price's own \u2014 so the\n // minor-unit discount lines up, then clamp at zero.\n const priceDecimals = (base.toString().split('.')[1] ?? '').length;\n const outDecimals = Math.max(decimals, priceDecimals);\n const scale = 10 ** outDecimals;\n const result = (Math.round(base * scale) - minor * 10 ** (outDecimals - decimals)) / scale;\n\n return result > 0 ? result : 0;\n};\n", "/**\n * ISO 4217 minor-unit exponents that differ from the *display* digits CLDR reports via\n * `Intl`. CLDR reflects practical display habits \u2014 e.g. it renders IQD with 0 decimals\n * because Iraqi fils are effectively unused \u2014 but pricing math needs the currency's actual\n * minor unit. Seed this map with those divergences.\n *\n * IQD is ISO 4217 exponent 3 (matching onsite-js's table); `Intl` reports 0. Confirm how\n * Shopify actually denominates the amount for a given market before trusting a value here.\n */\nconst MINOR_UNIT_OVERRIDES: Record<string, number> = {\n IQD: 3,\n};\n\n/** Resolved exponents by (upper-cased) code \u2014 this runs per variant per render, and `Intl.NumberFormat` is not free. */\nconst CACHE = new Map<string, number>();\n\n/**\n * The minor-unit exponent (number of decimal places) for an ISO 4217 currency code \u2014\n * 2 for USD/EUR, 0 for JPY/KRW/CLP, 3 for KWD/BHD/OMR (and IQD via override). Unknown,\n * empty, or invalid codes fall back to 2.\n *\n * We derive this from `Intl` (Unicode CLDR) rather than a hand-maintained table on purpose:\n * the currency tables scattered across the other repos carry data bugs (KRW marked\n * 2-decimal, CLP marked both 0 and 3, no Gulf 3-decimal currencies at all). CLDR is the\n * baseline; MINOR_UNIT_OVERRIDES patches only the ISO-4217-vs-display divergences.\n */\nexport const currencyDecimals = (currencyCode?: string | null): number => {\n if (!currencyCode) return 2;\n\n const code = currencyCode.toUpperCase();\n const override = MINOR_UNIT_OVERRIDES[code];\n\n if (override !== undefined) return override;\n\n const cached = CACHE.get(code);\n\n if (cached !== undefined) return cached;\n\n let decimals = 2;\n\n try {\n const { maximumFractionDigits } = new Intl.NumberFormat('en', {\n currency: code,\n style: 'currency',\n }).resolvedOptions();\n\n /* v8 ignore next -- maximumFractionDigits is always set for a currency formatter; the fallback is unreachable */\n decimals = maximumFractionDigits ?? 2;\n } catch {\n decimals = 2;\n }\n\n CACHE.set(code, decimals);\n\n return decimals;\n};\n", "import { computeCompareAtPrice } from '~/pricing/computeCompareAtPrice';\nimport { computeDisplayPrice } from '~/pricing/computeDisplayPrice';\nimport { currencyDecimals } from '~/pricing/currencyDecimals';\nimport { type PricedVariant, type PriceVariantInput } from '~/pricing/types';\n\n/**\n * \u2550\u2550\u2550 Offer-card money forecast \u2014 the single source of truth \u2550\u2550\u2550\n *\n * `priceVariant` is the one pure entry point; `computeDisplayPrice` and `computeCompareAtPrice` are its\n * leaves. INVARIANT: **card == cart** \u2014 the forecast mirrors the deployed product-discount Function (the\n * charge) and Shopify's line-cost semantics; it never sets a price, it only predicts one for display.\n *\n * ORACLE HIERARCHY (which source defines \"correct\", cited per branch below):\n * - **Discount math** \u2192 the deployed `product-discount` Shopify Function. A divergence from it is a bug\n * HERE, unless the Function itself contradicts an external spec.\n * - **Subscription / compare-at DISPLAY parity** \u2192 legacy `getVariantPrice` / `getVariantCompareAtPrice`.\n * - **Currency minor units** \u2192 ISO 4217 / CLDR (`currencyDecimals`). Where the Function contradicts ISO\n * (the cross-decimal fixed-discount bug) the fix belongs in the Function \u2014 see the escalation\n * `product-discount/JIRA-fixed-amount-currency-conversion.md`; this forecast already computes the\n * correct shop-decimal math, so `card == cart` follows once the Function ships. Same-currency is aligned\n * today; cross-decimal markets stay gated on the extension side until then.\n */\n\n/**\n * Price one variant from its already-localized market price: returns the display price\n * and the strikethrough compare-at, which is null unless it genuinely exceeds the display\n * price (so a non-discounted, non-sale variant shows no strike). Pass `currencyCode` so\n * discount math uses the right minor unit for zero- and three-decimal currencies; it\n * defaults to two-decimal behavior when omitted. `shopDecimals` (the shop currency's exponent)\n * defaults to the buyer currency's decimals, so same-currency callers are unaffected; supply the\n * real shop exponent so a Functions-applied fixed discount converts correctly in a cross-decimal market.\n * `subscriptionPrice` (Shopify's exact allocation price, buyer major units) forecasts the selling-plan\n * saving to the cent \u2014 see {@link PriceVariantInput}.\n */\nexport const priceVariant = ({\n compareAtPrice,\n conversionRate,\n currencyCode,\n price,\n productDiscount,\n shopDecimals,\n subscriptionPercentage,\n subscriptionPrice,\n widgetDiscount,\n}: PriceVariantInput): PricedVariant => {\n const decimals = currencyDecimals(currencyCode);\n const display = computeDisplayPrice(\n price,\n compareAtPrice,\n widgetDiscount,\n productDiscount,\n decimals,\n conversionRate,\n subscriptionPercentage,\n shopDecimals ?? decimals,\n subscriptionPrice\n );\n\n /**\n * The selling-plan allocation (subscription) price \u2014 Shopify's exact value when supplied, else derived\n * from the whole-percent plan discount \u2014 feeds the strike so it mirrors the subscribed display.\n */\n const subscribed = subscriptionPrice !== undefined || (subscriptionPercentage ?? 0) > 0;\n /** `price` when not subscribed (unused \u2014 the strike's subscription branch is gated on `subscribed`). */\n const allocationPrice = subscriptionPrice ?? price * (1 - (subscriptionPercentage ?? 0) / 100);\n\n const strike = computeCompareAtPrice(price, compareAtPrice, widgetDiscount, subscribed, allocationPrice);\n const compareAt = strike > display ? strike : null;\n\n /** Explicit saving vs the shown strike (0 when nothing is struck) for the token layer, so it isn't re-derived per call site. Rounded to kill subtraction float noise (18.4 \u2212 13.8 = 4.600000000000001). */\n const savings = compareAt !== null ? Math.round((compareAt - display) * 1e6) / 1e6 : 0;\n const savingsPercentage = compareAt ? Math.round((savings / compareAt) * 100) : 0;\n\n return { compareAtPrice: compareAt, price: display, savings, savingsPercentage };\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["const round = (value: number, precision = 0): number => {\n const factor = 10 ** precision;\n\n return Math.round(value * factor) / factor;\n};\n\n/**\n * A monetary amount in integer minor units. `decimals` is the currency's minor-unit\n * exponent (2 for USD/EUR, 0 for JPY, 3 for IQD/KWD) and defaults to 2 \u2014 resolve it\n * with `currencyDecimals(code)`. Rounds to that precision first so float drift\n * (19.999 \u2192 20.00 \u2192 2000 at 2 decimals) doesn't leak in; non-finite input \u2192 0. Ported\n * from the React checkout extension's `amountToCents`, generalized off its hard 2-decimal\n * assumption so zero- and three-decimal currencies are exact.\n */\nexport const amountToCents = (amount: number | string | null | undefined, decimals = 2): number => {\n const value = Number(amount);\n\n return Number.isFinite(value) ? round(round(value, decimals) * 10 ** decimals) : 0;\n};\n", "import { type Discount } from '~/pricing/types';\n\n/**\n * The strikethrough (compare-at) amount on an ALREADY-localized market price \u2014 ported from the React\n * extension's `getVariantCompareAtPrice`. Uses the variant compare-at when there's no discount or the\n * discount is taken from it; otherwise the (pre-discount) price is the strike. Returns the price itself\n * when there's no genuine strike \u2014 callers decide to show it only when it exceeds the display price.\n *\n * `subscribed`/`subscriptionPrice`: ORACLE `getVariantCompareAtPrice` subscription branch \u2014 while\n * subscribed, the strike is the SUBSCRIPTION (allocation) price when a widget discount also stacks (the\n * display price is the further-discounted amount), else the one-time price; NEVER the variant compare-at.\n * Mirrors the subscribed display in `computeDisplayPrice` so the strike represents the real saving.\n */\nexport const computeCompareAtPrice = (\n price: number,\n compareAtPrice: number | null | undefined,\n widgetDiscount?: Discount,\n subscribed?: boolean,\n subscriptionPrice?: number\n): number => {\n const hasWidgetDiscount = !!widgetDiscount?.type && widgetDiscount.type !== 'none';\n\n if (\n subscribed &&\n subscriptionPrice !== undefined &&\n Number.isFinite(subscriptionPrice) &&\n subscriptionPrice < price\n )\n return hasWidgetDiscount ? subscriptionPrice : price;\n\n const compareAt = Number(compareAtPrice);\n const isValidCompareAt = Number.isFinite(compareAt) && Number.isFinite(price) && compareAt > price;\n const useCompareAt = !hasWidgetDiscount || widgetDiscount?.discountedFrom === 'compare_at_price';\n\n return useCompareAt && isValidCompareAt ? compareAt : price;\n};\n", "import { amountToCents } from '~/pricing/amountToCents';\nimport { type Discount } from '~/pricing/types';\n\nconst isActive = (discount?: Discount): boolean => !!discount?.type && discount.type !== 'none';\n\n/**\n * The displayed (discounted) price, computed on an ALREADY-localized market price.\n * Ported from the React checkout extension's `getVariantPrice`, minus the localization\n * multiplier (Shopify localizes upstream): a data-source (product) discount takes\n * precedence over the widget discount; never returns negative.\n *\n * `decimals` is the currency's minor-unit exponent (2 for USD/EUR, 0 for JPY/KRW,\n * 3 for IQD/KWD/BHD) and defaults to 2 \u2014 resolve it with `currencyDecimals(code)`. All\n * discount math runs in that currency's minor units so zero- and three-decimal currencies\n * are exact (the source hard-coded hundredths). The output keeps whichever precision is\n * finer, the currency's or the price's own decimals, so an over-precise localized price\n * isn't truncated.\n *\n * `conversionRate` (buyer price \u00F7 shop price) converts FIXED discount amounts \u2014 stored in\n * shop currency \u2014 into the market currency before subtracting, mirroring both the discount\n * Function (`convertShopFixedAmount`: shop minor \u00F7 10^shopDecimals \u00D7 rate, rounded to buyer\n * precision) and the React source (which subtracted in shop cents, then localized the result).\n * Percentages are scale-free; a missing/zero/non-finite rate falls back to 1 (same-currency).\n *\n * `shopDecimals` is the SHOP currency's minor-unit exponent and matters ONLY for a\n * Functions-applied `fixed` discount, whose `amount` arrives in shop MINOR units: the buyer-minor\n * discount is `amount \u00D7 rate \u00D7 10^(decimals \u2212 shopDecimals)`, so a 500\u00A2 ($5) discount at 147\u00A5/$\n * is \u00A5735, not \u00A573,500. It defaults to `decimals` (same-currency, where the exponent gap is 0), so\n * same-currency callers are unchanged; supply the real shop exponent for a cross-decimal market.\n * `fixed_amount` and non-Functions `fixed` amounts are shop MAJOR units and never need it.\n *\n * `subscriptionPercentage` is the selected selling plan's discount % \u2014 it STACKS additively\n * with the widget/product discount off the same base (the source pushed both into one\n * discounts array over one price). Scale-free like any percentage; 0/omitted = one-time.\n *\n * `subscriptionPrice` is Shopify's OWN per-cycle allocation price (buyer major units, e.g. from an\n * `@inContext` `sellingPlanAllocations` query) and, when supplied, forecasts the selling-plan saving\n * EXACTLY as `price \u2212 subscriptionPrice` instead of the floored percentage \u2014 so a fixed-price/fixed-amount\n * plan lands on the Function's cent, not a percent-derived near-miss (C-45). It takes precedence over\n * `subscriptionPercentage` (which stays the source of the whole-percent LABEL); omit it for percentage\n * plans or when no allocation price is available.\n */\nexport const computeDisplayPrice = (\n price: number,\n compareAtPrice: number | null | undefined,\n widgetDiscount?: Discount,\n productDiscount?: Discount,\n decimals = 2,\n conversionRate = 1,\n subscriptionPercentage = 0,\n shopDecimals = decimals,\n subscriptionPrice: number | undefined = undefined\n): number => {\n const hasWidget = isActive(widgetDiscount);\n const isFunctions = widgetDiscount?.discountedBy === 'functions';\n\n // Discount off the compare-at price when the widget opts in (and isn't a Function).\n const base =\n hasWidget && !isFunctions && widgetDiscount!.discountedFrom === 'compare_at_price' && compareAtPrice\n ? compareAtPrice\n : price;\n\n const discount = isActive(productDiscount) ? productDiscount : hasWidget ? widgetDiscount : undefined;\n const subscription =\n Number.isFinite(subscriptionPercentage) && subscriptionPercentage > 0 ? subscriptionPercentage : 0;\n\n /**\n * The selling-plan saving in minor units: Shopify's exact allocation saving (`price \u2212 subscriptionPrice`)\n * when the caller supplies the allocation price \u2014 clamped at 0 so a non-discounting plan never adds\n * back \u2014 otherwise the floored percentage off the same base as the other discount (legacy summed both,\n * never compounded). The exact amount is why a fixed plan tracks the Function to the cent (C-45).\n */\n const subscriptionMinor =\n subscriptionPrice !== undefined && Number.isFinite(subscriptionPrice)\n ? Math.max(0, amountToCents(price, decimals) - amountToCents(subscriptionPrice, decimals))\n : subscription\n ? Math.floor(amountToCents(base, decimals) * (subscription / 100))\n : 0;\n\n if (!discount && !subscriptionMinor) return base;\n\n const rate = Number.isFinite(conversionRate) && conversionRate > 0 ? conversionRate : 1;\n let minor = 0; // total discount amount in the currency's minor units\n\n if (discount) {\n const amount = Number(discount.amount);\n\n // Malformed amounts (real merchant data carries shapes like \"10%\") must not forecast the\n // item as free via NaN falling through the final clamp \u2014 skip this discount instead (a\n // stacked subscription discount still applies).\n if (Number.isFinite(amount)) {\n if (discount.type === 'percentage')\n /**\n * ORACLE: the product-discount Function emits a `percentage` that Shopify applies to the\n * LINE cost. On a subscribed line that cost is the allocation price, so a stacked % applies\n * to `base \u2212 subscriptionSaving`, NOT the one-time base \u2014 else the card under-quotes vs the\n * cart ($100 plan-20% widget-10%: $72, not $70). `subscriptionMinor` is 0 for a one-time\n * purchase, so this is `base \u00D7 amount%` unchanged there. Fixed amounts are flat \u2192 unaffected.\n */\n minor = Math.floor((amountToCents(base, decimals) - subscriptionMinor) * (amount / 100));\n else if (discount.type === 'fixed' && hasWidget && isFunctions)\n // Functions send the amount in the SHOP currency's MINOR units; rebase to the buyer's minor\n // units via the rate and the shop-vs-buyer decimal gap, so a 500\u00A2 ($5) discount at 147\u00A5/$ is\n // \u00A5735 (500 \u00D7 147 \u00D7 10^(0\u22122)), not \u00A573,500. Same-currency (shopDecimals === decimals) is a no-op.\n minor = Math.round(amount * rate * 10 ** (decimals - shopDecimals));\n else if (discount.type === 'fixed' || discount.type === 'fixed_amount')\n // Otherwise the amount is in shop MAJOR units (`fixed_amount` always; `fixed` unless applied\n // by Functions). Apply the rate to the major amount and round ONCE at the buyer's precision:\n // a $4.35 discount at 147\u00A5/$ is \u00A5639 (round(639.45)), never \u00A5588 (rounding $4.35 \u2192 $4 first).\n minor = Math.round(amount * rate * 10 ** decimals);\n }\n }\n\n minor += subscriptionMinor;\n\n // Scale into whichever precision is finer \u2014 the currency's or the price's own \u2014 so the\n // minor-unit discount lines up, then clamp at zero.\n const priceDecimals = (base.toString().split('.')[1] ?? '').length;\n const outDecimals = Math.max(decimals, priceDecimals);\n const scale = 10 ** outDecimals;\n const result = (Math.round(base * scale) - minor * 10 ** (outDecimals - decimals)) / scale;\n\n return result > 0 ? result : 0;\n};\n", "/**\n * ISO 4217 minor-unit exponents that differ from the *display* digits CLDR reports via\n * `Intl`. CLDR reflects practical display habits \u2014 e.g. it renders IQD with 0 decimals\n * because Iraqi fils are effectively unused \u2014 but pricing math needs the currency's actual\n * minor unit. Seed this map with those divergences.\n *\n * IQD is ISO 4217 exponent 3 (matching onsite-js's table); `Intl` reports 0. Confirm how\n * Shopify actually denominates the amount for a given market before trusting a value here.\n */\nconst MINOR_UNIT_OVERRIDES: Record<string, number> = {\n IQD: 3,\n};\n\n/** Resolved exponents by (upper-cased) code \u2014 this runs per variant per render, and `Intl.NumberFormat` is not free. */\nconst CACHE = new Map<string, number>();\n\n/**\n * The minor-unit exponent (number of decimal places) for an ISO 4217 currency code \u2014\n * 2 for USD/EUR, 0 for JPY/KRW/CLP, 3 for KWD/BHD/OMR (and IQD via override). Unknown,\n * empty, or invalid codes fall back to 2.\n *\n * We derive this from `Intl` (Unicode CLDR) rather than a hand-maintained table on purpose:\n * the currency tables scattered across the other repos carry data bugs (KRW marked\n * 2-decimal, CLP marked both 0 and 3, no Gulf 3-decimal currencies at all). CLDR is the\n * baseline; MINOR_UNIT_OVERRIDES patches only the ISO-4217-vs-display divergences.\n */\nexport const currencyDecimals = (currencyCode?: string | null): number => {\n if (!currencyCode) return 2;\n\n const code = currencyCode.toUpperCase();\n const override = MINOR_UNIT_OVERRIDES[code];\n\n if (override !== undefined) return override;\n\n const cached = CACHE.get(code);\n\n if (cached !== undefined) return cached;\n\n let decimals = 2;\n\n try {\n const { maximumFractionDigits } = new Intl.NumberFormat('en', {\n currency: code,\n style: 'currency',\n }).resolvedOptions();\n\n /* v8 ignore next -- maximumFractionDigits is always set for a currency formatter; the fallback is unreachable */\n decimals = maximumFractionDigits ?? 2;\n } catch {\n decimals = 2;\n }\n\n CACHE.set(code, decimals);\n\n return decimals;\n};\n", "import { computeCompareAtPrice } from '~/pricing/computeCompareAtPrice';\nimport { computeDisplayPrice } from '~/pricing/computeDisplayPrice';\nimport { currencyDecimals } from '~/pricing/currencyDecimals';\nimport { type PricedVariant, type PriceVariantInput } from '~/pricing/types';\n\n/**\n * \u2550\u2550\u2550 Offer-card money forecast \u2014 the single source of truth \u2550\u2550\u2550\n *\n * `priceVariant` is the one pure entry point; `computeDisplayPrice` and `computeCompareAtPrice` are its\n * leaves. INVARIANT: **card == cart** \u2014 the forecast mirrors the deployed product-discount Function (the\n * charge) and Shopify's line-cost semantics; it never sets a price, it only predicts one for display.\n *\n * ORACLE HIERARCHY (which source defines \"correct\", cited per branch below):\n * - **Discount math** \u2192 the deployed `product-discount` Shopify Function. A divergence from it is a bug\n * HERE, unless the Function itself contradicts an external spec.\n * - **Subscription / compare-at DISPLAY parity** \u2192 legacy `getVariantPrice` / `getVariantCompareAtPrice`.\n * - **Currency minor units** \u2192 ISO 4217 / CLDR (`currencyDecimals`). Where the Function contradicts ISO\n * (the cross-decimal fixed-discount bug) the fix belongs in the Function \u2014 tracked in REB-23607; this\n * forecast already computes the correct shop-decimal math, so `card == cart` follows once the Function\n * ships. Same-currency is aligned today; cross-decimal markets stay gated on the extension side until then.\n */\n\n/**\n * Price one variant from its already-localized market price: returns the display price\n * and the strikethrough compare-at, which is null unless it genuinely exceeds the display\n * price (so a non-discounted, non-sale variant shows no strike). Pass `currencyCode` so\n * discount math uses the right minor unit for zero- and three-decimal currencies; it\n * defaults to two-decimal behavior when omitted. `shopDecimals` (the shop currency's exponent)\n * defaults to the buyer currency's decimals, so same-currency callers are unaffected; supply the\n * real shop exponent so a Functions-applied fixed discount converts correctly in a cross-decimal market.\n * `subscriptionPrice` (Shopify's exact allocation price, buyer major units) forecasts the selling-plan\n * saving to the cent \u2014 see {@link PriceVariantInput}.\n */\nexport const priceVariant = ({\n compareAtPrice,\n conversionRate,\n currencyCode,\n price,\n productDiscount,\n shopDecimals,\n subscriptionPercentage,\n subscriptionPrice,\n widgetDiscount,\n}: PriceVariantInput): PricedVariant => {\n const decimals = currencyDecimals(currencyCode);\n const display = computeDisplayPrice(\n price,\n compareAtPrice,\n widgetDiscount,\n productDiscount,\n decimals,\n conversionRate,\n subscriptionPercentage,\n shopDecimals ?? decimals,\n subscriptionPrice\n );\n\n /**\n * The selling-plan allocation (subscription) price \u2014 Shopify's exact value when supplied, else derived\n * from the whole-percent plan discount \u2014 feeds the strike so it mirrors the subscribed display.\n */\n const subscribed = subscriptionPrice !== undefined || (subscriptionPercentage ?? 0) > 0;\n /** `price` when not subscribed (unused \u2014 the strike's subscription branch is gated on `subscribed`). */\n const allocationPrice = subscriptionPrice ?? price * (1 - (subscriptionPercentage ?? 0) / 100);\n\n const strike = computeCompareAtPrice(price, compareAtPrice, widgetDiscount, subscribed, allocationPrice);\n const compareAt = strike > display ? strike : null;\n\n /** Explicit saving vs the shown strike (0 when nothing is struck) for the token layer, so it isn't re-derived per call site. Rounded to kill subtraction float noise (18.4 \u2212 13.8 = 4.600000000000001). */\n const savings = compareAt !== null ? Math.round((compareAt - display) * 1e6) / 1e6 : 0;\n const savingsPercentage = compareAt ? Math.round((savings / compareAt) * 100) : 0;\n\n return { compareAtPrice: compareAt, price: display, savings, savingsPercentage };\n};\n"],
5
+ "mappings": ";AAAA,IAAM,QAAQ,CAAC,OAAe,YAAY,MAAc;AACpD,QAAM,SAAS,MAAM;AAErB,SAAO,KAAK,MAAM,QAAQ,MAAM,IAAI;AACxC;AAUO,IAAM,gBAAgB,CAAC,QAA4C,WAAW,MAAc;AAC/F,QAAM,QAAQ,OAAO,MAAM;AAE3B,SAAO,OAAO,SAAS,KAAK,IAAI,MAAM,MAAM,OAAO,QAAQ,IAAI,MAAM,QAAQ,IAAI;AACrF;;;ACLO,IAAM,wBAAwB,CACjC,OACA,gBACA,gBACA,YACA,sBACS;AACT,QAAM,oBAAoB,CAAC,CAAC,gBAAgB,QAAQ,eAAe,SAAS;AAE5E,MACI,cACA,sBAAsB,UACtB,OAAO,SAAS,iBAAiB,KACjC,oBAAoB;AAEpB,WAAO,oBAAoB,oBAAoB;AAEnD,QAAM,YAAY,OAAO,cAAc;AACvC,QAAM,mBAAmB,OAAO,SAAS,SAAS,KAAK,OAAO,SAAS,KAAK,KAAK,YAAY;AAC7F,QAAM,eAAe,CAAC,qBAAqB,gBAAgB,mBAAmB;AAE9E,SAAO,gBAAgB,mBAAmB,YAAY;AAC1D;;;AChCA,IAAM,WAAW,CAAC,aAAiC,CAAC,CAAC,UAAU,QAAQ,SAAS,SAAS;AAuClF,IAAM,sBAAsB,CAC/B,OACA,gBACA,gBACA,iBACA,WAAW,GACX,iBAAiB,GACjB,yBAAyB,GACzB,eAAe,UACf,oBAAwC,WAC/B;AACT,QAAM,YAAY,SAAS,cAAc;AACzC,QAAM,cAAc,gBAAgB,iBAAiB;AAGrD,QAAM,OACF,aAAa,CAAC,eAAe,eAAgB,mBAAmB,sBAAsB,iBAChF,iBACA;AAEV,QAAM,WAAW,SAAS,eAAe,IAAI,kBAAkB,YAAY,iBAAiB;AAC5F,QAAM,eACF,OAAO,SAAS,sBAAsB,KAAK,yBAAyB,IAAI,yBAAyB;AAQrG,QAAM,oBACF,sBAAsB,UAAa,OAAO,SAAS,iBAAiB,IAC9D,KAAK,IAAI,GAAG,cAAc,OAAO,QAAQ,IAAI,cAAc,mBAAmB,QAAQ,CAAC,IACvF,eACE,KAAK,MAAM,cAAc,MAAM,QAAQ,KAAK,eAAe,IAAI,IAC/D;AAEZ,MAAI,CAAC,YAAY,CAAC,kBAAmB,QAAO;AAE5C,QAAM,OAAO,OAAO,SAAS,cAAc,KAAK,iBAAiB,IAAI,iBAAiB;AACtF,MAAI,QAAQ;AAEZ,MAAI,UAAU;AACV,UAAM,SAAS,OAAO,SAAS,MAAM;AAKrC,QAAI,OAAO,SAAS,MAAM,GAAG;AACzB,UAAI,SAAS,SAAS;AAQlB,gBAAQ,KAAK,OAAO,cAAc,MAAM,QAAQ,IAAI,sBAAsB,SAAS,IAAI;AAAA,eAClF,SAAS,SAAS,WAAW,aAAa;AAI/C,gBAAQ,KAAK,MAAM,SAAS,OAAO,OAAO,WAAW,aAAa;AAAA,eAC7D,SAAS,SAAS,WAAW,SAAS,SAAS;AAIpD,gBAAQ,KAAK,MAAM,SAAS,OAAO,MAAM,QAAQ;AAAA,IACzD;AAAA,EACJ;AAEA,WAAS;AAIT,QAAM,iBAAiB,KAAK,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI;AAC5D,QAAM,cAAc,KAAK,IAAI,UAAU,aAAa;AACpD,QAAM,QAAQ,MAAM;AACpB,QAAM,UAAU,KAAK,MAAM,OAAO,KAAK,IAAI,QAAQ,OAAO,cAAc,aAAa;AAErF,SAAO,SAAS,IAAI,SAAS;AACjC;;;AClHA,IAAM,uBAA+C;AAAA,EACjD,KAAK;AACT;AAGA,IAAM,QAAQ,oBAAI,IAAoB;AAY/B,IAAM,mBAAmB,CAAC,iBAAyC;AACtE,MAAI,CAAC,aAAc,QAAO;AAE1B,QAAM,OAAO,aAAa,YAAY;AACtC,QAAM,WAAW,qBAAqB,IAAI;AAE1C,MAAI,aAAa,OAAW,QAAO;AAEnC,QAAM,SAAS,MAAM,IAAI,IAAI;AAE7B,MAAI,WAAW,OAAW,QAAO;AAEjC,MAAI,WAAW;AAEf,MAAI;AACA,UAAM,EAAE,sBAAsB,IAAI,IAAI,KAAK,aAAa,MAAM;AAAA,MAC1D,UAAU;AAAA,MACV,OAAO;AAAA,IACX,CAAC,EAAE,gBAAgB;AAGnB,eAAW,yBAAyB;AAAA,EACxC,QAAQ;AACJ,eAAW;AAAA,EACf;AAEA,QAAM,IAAI,MAAM,QAAQ;AAExB,SAAO;AACX;;;ACtBO,IAAM,eAAe,CAAC;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,MAAwC;AACpC,QAAM,WAAW,iBAAiB,YAAY;AAC9C,QAAM,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB;AAAA,EACJ;AAMA,QAAM,aAAa,sBAAsB,WAAc,0BAA0B,KAAK;AAEtF,QAAM,kBAAkB,qBAAqB,SAAS,KAAK,0BAA0B,KAAK;AAE1F,QAAM,SAAS,sBAAsB,OAAO,gBAAgB,gBAAgB,YAAY,eAAe;AACvG,QAAM,YAAY,SAAS,UAAU,SAAS;AAG9C,QAAM,UAAU,cAAc,OAAO,KAAK,OAAO,YAAY,WAAW,GAAG,IAAI,MAAM;AACrF,QAAM,oBAAoB,YAAY,KAAK,MAAO,UAAU,YAAa,GAAG,IAAI;AAEhF,SAAO,EAAE,gBAAgB,WAAW,OAAO,SAAS,SAAS,kBAAkB;AACnF;",
6
6
  "names": []
7
7
  }
@@ -11,10 +11,9 @@ import { type PricedVariant, type PriceVariantInput } from '../pricing/types';
11
11
  * HERE, unless the Function itself contradicts an external spec.
12
12
  * - **Subscription / compare-at DISPLAY parity** → legacy `getVariantPrice` / `getVariantCompareAtPrice`.
13
13
  * - **Currency minor units** → ISO 4217 / CLDR (`currencyDecimals`). Where the Function contradicts ISO
14
- * (the cross-decimal fixed-discount bug) the fix belongs in the Function — see the escalation
15
- * `product-discount/JIRA-fixed-amount-currency-conversion.md`; this forecast already computes the
16
- * correct shop-decimal math, so `card == cart` follows once the Function ships. Same-currency is aligned
17
- * today; cross-decimal markets stay gated on the extension side until then.
14
+ * (the cross-decimal fixed-discount bug) the fix belongs in the Function — tracked in REB-23607; this
15
+ * forecast already computes the correct shop-decimal math, so `card == cart` follows once the Function
16
+ * ships. Same-currency is aligned today; cross-decimal markets stay gated on the extension side until then.
18
17
  */
19
18
  /**
20
19
  * Price one variant from its already-localized market price: returns the display price
@@ -1 +1 @@
1
- {"version":3,"file":"priceVariant.d.ts","sourceRoot":"","sources":["../../src/pricing/priceVariant.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE7E;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GAAI,oJAU1B,iBAAiB,KAAG,aA8BtB,CAAC"}
1
+ {"version":3,"file":"priceVariant.d.ts","sourceRoot":"","sources":["../../src/pricing/priceVariant.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE7E;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GAAI,oJAU1B,iBAAiB,KAAG,aA8BtB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebuy/rebuy",
3
3
  "description": "This is the default library for Rebuy",
4
- "version": "2.32.0-rc.2",
4
+ "version": "2.32.0-rc.3",
5
5
  "license": "MIT",
6
6
  "author": "Rebuy, Inc.",
7
7
  "type": "module",