@tapcart/mobile-components 0.14.1 → 0.14.6

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 +1 @@
1
- {"version":3,"file":"icon.d.ts","sourceRoot":"","sources":["../../../components/ui/icon.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAEL,eAAe,EAyChB,MAAM,qBAAqB,CAAA;AAI5B,QAAA,MAAM,YAAY;;;mFAiBjB,CAAA;AA+DD,MAAM,WAAW,SACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,EACzD,YAAY,CAAC,OAAO,YAAY,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAuID,iBAAS,IAAI,CAAC,EACZ,SAAS,EACT,IAAI,EACJ,IAAW,EACX,KAAK,EACL,GAAG,EACH,SAAS,EACT,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,cAAc,EACd,GAAG,KAAK,EACT,EAAE,SAAS,2CA+BX;AAED,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,eAAe,EAAE,CAAA"}
1
+ {"version":3,"file":"icon.d.ts","sourceRoot":"","sources":["../../../components/ui/icon.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAEL,eAAe,EAyChB,MAAM,qBAAqB,CAAA;AAI5B,QAAA,MAAM,YAAY;;;mFAiBjB,CAAA;AA+DD,MAAM,WAAW,SACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,EACzD,YAAY,CAAC,OAAO,YAAY,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AA8ID,iBAAS,IAAI,CAAC,EACZ,SAAS,EACT,IAAI,EACJ,IAAW,EACX,KAAK,EACL,GAAG,EACH,SAAS,EACT,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,cAAc,EACd,GAAG,KAAK,EACT,EAAE,SAAS,2CA+BX;AAED,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,eAAe,EAAE,CAAA"}
@@ -11,6 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  return t;
12
12
  };
13
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
+ import * as React from "react";
14
15
  import { ReactSVG } from "react-svg";
15
16
  import { cva } from "class-variance-authority";
16
17
  import { IconUpload, IconPencilMinus, IconPlus, IconMinus, IconAdjustmentsAlt, IconArrowsSort, IconAlertCircle, IconCircleX, IconInfoCircle, IconCheck, IconSquareOff, IconMapPin, IconRepeat, IconSquare, IconSquareCheck, IconChevronUp, IconChevronRight, IconChevronLeft, IconEyeOff, IconSquareX, IconCircle, IconChevronDown, IconHeartFilled, IconCurrencyDollar, IconX, IconCircleOff, IconCircleDotFilled, IconMoodSad, IconSquareXFilled, IconTrash, IconLoader2, IconColumns1, IconColumns2, IconColumns3, IconSearch, IconShoppingCartOff, IconStarFilled, IconShoppingBagPlus, IconShieldCheckFilled, IconShoppingBag, IconHeartCancel, IconStar, } from "@tabler/icons-react";
@@ -93,62 +94,72 @@ const TablerIcon = ({ name, size }) => {
93
94
  return IconComponent ? (_jsx(IconComponent, { size: sizeMapping[size], strokeWidth: strokeWidthMapping[size] })) : null;
94
95
  };
95
96
  const CustomIcon = ({ url, size, color, strokeColor, strokeWidth, fillColor, fillPercentage, secondaryFillColor, }) => {
96
- return (_jsx(ReactSVG, { src: url, beforeInjection: (svg) => {
97
- svg.setAttribute("style", `width: ${sizeMapping[size]}px; height: ${sizeMapping[size]}px`);
98
- const paths = svg.querySelectorAll("path");
99
- paths.forEach((path) => {
100
- if (color) {
101
- if (path.hasAttribute("fill") && fillPercentage === 1) {
102
- path.setAttribute("fill", color);
103
- }
104
- path.setAttribute("stroke", color);
97
+ // Stable identity keeps ReactSVG from tearing down and re-injecting on every
98
+ // parent re-render concurrent same-URL injections race inside
99
+ // @tanem/svg-injector (Sentry NEXTJS-CONSUMER-PAGES-78B).
100
+ const beforeInjection = React.useCallback((svg) => {
101
+ svg.setAttribute("style", `width: ${sizeMapping[size]}px; height: ${sizeMapping[size]}px`);
102
+ const paths = svg.querySelectorAll("path");
103
+ paths.forEach((path) => {
104
+ if (color) {
105
+ if (path.hasAttribute("fill") && fillPercentage === 1) {
106
+ path.setAttribute("fill", color);
105
107
  }
106
- else {
107
- if (fillColor &&
108
- path.hasAttribute("fill") &&
109
- fillPercentage === 1) {
110
- path.setAttribute("fill", fillColor);
111
- }
112
- if (strokeColor) {
113
- path.setAttribute("stroke", strokeColor);
114
- }
108
+ path.setAttribute("stroke", color);
109
+ }
110
+ else {
111
+ if (fillColor && path.hasAttribute("fill") && fillPercentage === 1) {
112
+ path.setAttribute("fill", fillColor);
115
113
  }
116
- if (path.hasAttribute("stroke")) {
117
- path.setAttribute("stroke-width", strokeWidthMapping[size].toString());
114
+ if (strokeColor) {
115
+ path.setAttribute("stroke", strokeColor);
118
116
  }
119
- // Override stroke width if one is provided
120
- if (strokeWidth != null && path.hasAttribute("stroke")) {
121
- path.setAttribute("stroke-width", strokeWidth.toString());
117
+ }
118
+ if (path.hasAttribute("stroke")) {
119
+ path.setAttribute("stroke-width", strokeWidthMapping[size].toString());
120
+ }
121
+ // Override stroke width if one is provided
122
+ if (strokeWidth != null && path.hasAttribute("stroke")) {
123
+ path.setAttribute("stroke-width", strokeWidth.toString());
124
+ }
125
+ // Create a gradient fill based on the fillPercentage
126
+ if (fillPercentage && fillPercentage > 0 && fillPercentage < 1) {
127
+ const percentage = Math.min(Math.max(fillPercentage, 0), 1);
128
+ const uniqueId = crypto.randomUUID();
129
+ const fillGradientId = `fill-gradient-${uniqueId}`;
130
+ let defs = svg.querySelector("defs");
131
+ if (!defs) {
132
+ defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
133
+ svg.prepend(defs);
122
134
  }
123
- // Create a gradient fill based on the fillPercentage
124
- if (fillPercentage && fillPercentage > 0 && fillPercentage < 1) {
125
- const percentage = Math.min(Math.max(fillPercentage, 0), 1);
126
- const uniqueId = crypto.randomUUID();
127
- const fillGradientId = `fill-gradient-${uniqueId}`;
128
- let defs = svg.querySelector("defs");
129
- if (!defs) {
130
- defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
131
- svg.prepend(defs);
132
- }
133
- const fillGradient = document.createElementNS("http://www.w3.org/2000/svg", "linearGradient");
134
- fillGradient.setAttribute("id", fillGradientId);
135
- fillGradient.setAttribute("x1", "0%");
136
- fillGradient.setAttribute("y1", "0%");
137
- fillGradient.setAttribute("x2", "100%");
138
- fillGradient.setAttribute("y2", "0%");
139
- const fillStop1 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
140
- fillStop1.setAttribute("offset", `${percentage * 100}%`);
141
- fillStop1.setAttribute("stop-color", color || fillColor || "transparent");
142
- fillGradient.appendChild(fillStop1);
143
- const fillStop2 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
144
- fillStop2.setAttribute("offset", `${percentage * 100}%`);
145
- fillStop2.setAttribute("stop-color", secondaryFillColor || "transparent");
146
- fillGradient.appendChild(fillStop2);
147
- defs.appendChild(fillGradient);
148
- path.setAttribute("fill", `url(#${fillGradientId})`);
149
- }
150
- });
151
- } }));
135
+ const fillGradient = document.createElementNS("http://www.w3.org/2000/svg", "linearGradient");
136
+ fillGradient.setAttribute("id", fillGradientId);
137
+ fillGradient.setAttribute("x1", "0%");
138
+ fillGradient.setAttribute("y1", "0%");
139
+ fillGradient.setAttribute("x2", "100%");
140
+ fillGradient.setAttribute("y2", "0%");
141
+ const fillStop1 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
142
+ fillStop1.setAttribute("offset", `${percentage * 100}%`);
143
+ fillStop1.setAttribute("stop-color", color || fillColor || "transparent");
144
+ fillGradient.appendChild(fillStop1);
145
+ const fillStop2 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
146
+ fillStop2.setAttribute("offset", `${percentage * 100}%`);
147
+ fillStop2.setAttribute("stop-color", secondaryFillColor || "transparent");
148
+ fillGradient.appendChild(fillStop2);
149
+ defs.appendChild(fillGradient);
150
+ path.setAttribute("fill", `url(#${fillGradientId})`);
151
+ }
152
+ });
153
+ }, [
154
+ size,
155
+ color,
156
+ fillColor,
157
+ strokeColor,
158
+ strokeWidth,
159
+ fillPercentage,
160
+ secondaryFillColor,
161
+ ]);
162
+ return _jsx(ReactSVG, { src: url, beforeInjection: beforeInjection });
152
163
  };
153
164
  function Icon(_a) {
154
165
  var { className, name, size = "md", color, url, fillColor, secondaryFillColor, strokeColor, strokeWidth, fillPercentage } = _a, props = __rest(_a, ["className", "name", "size", "color", "url", "fillColor", "secondaryFillColor", "strokeColor", "strokeWidth", "fillPercentage"]);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { addItemToWishlist, cn, createCollectionImageMap, cva, formatRelativeTime, getBackgroundAndPaddingStyle, getBackgroundAndSpacingStyle, getBadgesForProductFn, getBorderSidesStyle, getBorderStyle, getColor, getDestinationHandler, getEnabledWishlistIntegration, getGridSpacing, getIdFromGid, getInputPlaceholderTextProps, getLoyaltyButtonProps, getMarginStyle, getOverlayStyle, getPaddingStyle, getProductGidsFromIds, getSpaceBetween, getTextStyle, getVariantGidsFromIds, getVerticalAlignment, getVerticalAlignmentStyle, getWishlistAddItemNavigation, isFavoriteIntegrationEnabled, isVideo, mapFlexToAlignment, parsePhoneNumber, pluralize, productGidFromId, removeItemFromWishlists, stringRatioToInt, supportedVideoExtensions, supportsMultipleWishlists, throttleFunction, variantGidFromId, formatFrequency, formatPrice, shareContent, } from "./lib/utils";
2
2
  export type { WishlistEntryMatch } from "./lib/utils";
3
3
  export * from "./lib/cart.util";
4
+ export * from "./lib/discountAllocation.util";
4
5
  export * from "./lib/variablesCart.util";
5
6
  export * from "./lib/isTapcartVersion20.util";
6
7
  export * from "./components/contexts/translation-context";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // component exports
2
2
  export { addItemToWishlist, cn, createCollectionImageMap, cva, formatRelativeTime, getBackgroundAndPaddingStyle, getBackgroundAndSpacingStyle, getBadgesForProductFn, getBorderSidesStyle, getBorderStyle, getColor, getDestinationHandler, getEnabledWishlistIntegration, getGridSpacing, getIdFromGid, getInputPlaceholderTextProps, getLoyaltyButtonProps, getMarginStyle, getOverlayStyle, getPaddingStyle, getProductGidsFromIds, getSpaceBetween, getTextStyle, getVariantGidsFromIds, getVerticalAlignment, getVerticalAlignmentStyle, getWishlistAddItemNavigation, isFavoriteIntegrationEnabled, isVideo, mapFlexToAlignment, parsePhoneNumber, pluralize, productGidFromId, removeItemFromWishlists, stringRatioToInt, supportedVideoExtensions, supportsMultipleWishlists, throttleFunction, variantGidFromId, formatFrequency, formatPrice, shareContent, } from "./lib/utils";
3
3
  export * from "./lib/cart.util";
4
+ export * from "./lib/discountAllocation.util";
4
5
  export * from "./lib/variablesCart.util";
5
6
  export * from "./lib/isTapcartVersion20.util";
6
7
  export * from "./components/contexts/translation-context";
@@ -1 +1 @@
1
- {"version":3,"file":"cart.util.d.ts","sourceRoot":"","sources":["../../lib/cart.util.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,6BAA6B,EAC7B,cAAc,EACf,MAAM,kBAAkB,CAAA;AAGzB,eAAO,MAAM,eAAe;cAKhB,MAAM;aACP,cAAc;;0BAEH;YAChB,gBAAgB,CAAC,EAAE;gBACjB,MAAM,EAAE,MAAM,CAAA;gBACd,YAAY,EAAE,MAAM,CAAA;aACrB,CAAA;YACD,KAAK,EAAE;gBACL,MAAM,EAAE,MAAM,CAAA;gBACd,YAAY,EAAE,MAAM,CAAA;aACrB,CAAA;YACD,cAAc,CAAC,EAAE;gBACf,MAAM,EAAE,MAAM,CAAA;gBACd,YAAY,EAAE,MAAM,CAAA;aACrB,CAAA;SACF,EAAE;;;;;;;CAgCN,CAAA;AAuCD,eAAO,MAAM,oBAAoB,SAAU,MAAM,QAAQ,YAAY,YAMpE,CAAA;AAED,eAAO,MAAM,kBAAkB,SAAU,MAAM,QAAQ,YAAY,YAMlE,CAAA;AAoHD,MAAM,MAAM,cAAc,GAAG;IAC3B,yBAAyB,EAAE,sBAAsB,EAAE,CAAA;IACnD,gBAAgB,EAAE,6BAA6B,EAAE,CAAA;IACjD,cAAc,EAAE,OAAO,CAAA;IACvB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,eAAO,MAAM,uBAAuB,EAAE,cAUrC,CAAA;AAED,eAAO,MAAM,qBAAqB,SAAU,YAAY,KAAG,cA8B1D,CAAA;AAGD,eAAO,MAAM,mBAAmB;eACnB,MAAM;;;;;aAEG,MAAM;eAAS,MAAM;;;;oBAIN,MAAM;0BAAgB,MAAM;;;eAElD,MAAM;;;;;gBAML,MAAM;cACR,MAAM;;cAEN,MAAM;;;0BAIM;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE;oBAC9C,MAAM;;;;YAsCzB,CAAA"}
1
+ {"version":3,"file":"cart.util.d.ts","sourceRoot":"","sources":["../../lib/cart.util.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,6BAA6B,EAC7B,cAAc,EACf,MAAM,kBAAkB,CAAA;AAOzB,eAAO,MAAM,eAAe;cAKhB,MAAM;aACP,cAAc;;0BAEH;YAChB,gBAAgB,CAAC,EAAE;gBACjB,MAAM,EAAE,MAAM,CAAA;gBACd,YAAY,EAAE,MAAM,CAAA;aACrB,CAAA;YACD,KAAK,EAAE;gBACL,MAAM,EAAE,MAAM,CAAA;gBACd,YAAY,EAAE,MAAM,CAAA;aACrB,CAAA;YACD,cAAc,CAAC,EAAE;gBACf,MAAM,EAAE,MAAM,CAAA;gBACd,YAAY,EAAE,MAAM,CAAA;aACrB,CAAA;SACF,EAAE;;;;;;;CAgCN,CAAA;AAuCD,eAAO,MAAM,oBAAoB,SAAU,MAAM,QAAQ,YAAY,YAMpE,CAAA;AAED,eAAO,MAAM,kBAAkB,SAAU,MAAM,QAAQ,YAAY,YAMlE,CAAA;AAoHD,MAAM,MAAM,cAAc,GAAG;IAC3B,yBAAyB,EAAE,sBAAsB,EAAE,CAAA;IACnD,gBAAgB,EAAE,6BAA6B,EAAE,CAAA;IACjD,cAAc,EAAE,OAAO,CAAA;IACvB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,eAAO,MAAM,uBAAuB,EAAE,cAUrC,CAAA;AAED,eAAO,MAAM,qBAAqB,SAAU,YAAY,KAAG,cA8B1D,CAAA;AAGD,eAAO,MAAM,mBAAmB;eACnB,MAAM;;;;;aAEG,MAAM;eAAS,MAAM;;;;oBAIN,MAAM;0BAAgB,MAAM;;;eAElD,MAAM;;;;;gBAML,MAAM;cACR,MAAM;;cAEN,MAAM;;;0BAIM;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE;oBAC9C,MAAM;;;;YAsCzB,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { isMerchandiseDiscountAllocation, isShippingDiscountAllocation, } from "./discountAllocation.util";
1
2
  import { getIdFromGid } from "./utils";
2
3
  export const getVariantPrice = ({ quantity, variant, sellingPlanAllocation = { priceAdjustments: [] }, }) => {
3
4
  var _a, _b, _c, _d;
@@ -27,7 +28,7 @@ export const getVariantPrice = ({ quantity, variant, sellingPlanAllocation = { p
27
28
  };
28
29
  const getOrderLevelDiscounts = (cart) => {
29
30
  var _a;
30
- const discountMap = (_a = cart.discountAllocations) === null || _a === void 0 ? void 0 : _a.filter((discount) => discount.targetType !== "SHIPPING_LINE").reduce((acc, discount) => {
31
+ const discountMap = (_a = cart.discountAllocations) === null || _a === void 0 ? void 0 : _a.filter((discount) => isMerchandiseDiscountAllocation(discount, cart)).reduce((acc, discount) => {
31
32
  var _a, _b;
32
33
  const code = ((_a = discount.code) !== null && _a !== void 0 ? _a : "unknown").toUpperCase();
33
34
  if (!acc[code]) {
@@ -51,7 +52,7 @@ const getShippingDiscounts = (cart) => {
51
52
  var _a;
52
53
  if (!(cart === null || cart === void 0 ? void 0 : cart.discountAllocations))
53
54
  return [];
54
- return (_a = cart.discountAllocations) === null || _a === void 0 ? void 0 : _a.filter((discount) => discount.targetType === "SHIPPING_LINE").map((discount) => {
55
+ return (_a = cart.discountAllocations) === null || _a === void 0 ? void 0 : _a.filter((discount) => isShippingDiscountAllocation(discount, cart)).map((discount) => {
55
56
  var _a;
56
57
  return ({
57
58
  amount: (_a = discount === null || discount === void 0 ? void 0 : discount.discountedAmount) === null || _a === void 0 ? void 0 : _a.amount,
@@ -88,6 +88,95 @@ describe("cart-provider.util", () => {
88
88
  const result = getCalculatedCartData(mockCart);
89
89
  expect(result.isFreeShipping).toBe(true);
90
90
  });
91
+ it("should exclude shipping discount without targetType from merchandise totals (TICKET-4028)", () => {
92
+ const mockCart = Object.assign(Object.assign({}, baseCartData), { discounts: ["CEXFS9P882N"], cost: {
93
+ subtotalAmount: { amount: "14.00", currencyCode: "USD" },
94
+ totalAmount: { amount: "14.00", currencyCode: "USD" },
95
+ totalTaxAmount: { amount: "0.00", currencyCode: "USD" },
96
+ }, items: [
97
+ {
98
+ id: "line1",
99
+ quantity: 1,
100
+ productId: "prod1",
101
+ variantId: "var1",
102
+ productDetails: {},
103
+ variantDetails: {
104
+ compareAtPrice: { amount: "14.00", currencyCode: "USD" },
105
+ price: { amount: "14.00", currencyCode: "USD" },
106
+ },
107
+ discounts: [],
108
+ cost: { totalAmount: { amount: "14.00", currencyCode: "USD" } },
109
+ },
110
+ ], discountAllocations: [
111
+ {
112
+ discountedAmount: { amount: "7.95", currencyCode: "USD" },
113
+ code: "CEXFS9P882N",
114
+ },
115
+ ], appliedGiftCards: [], deliveryGroups: [
116
+ {
117
+ deliveryOptions: [
118
+ {
119
+ title: "Ground Shipping",
120
+ estimatedCost: { amount: "7.95", currencyCode: "USD" },
121
+ },
122
+ {
123
+ title: "Express Shipping",
124
+ estimatedCost: { amount: "18.95", currencyCode: "USD" },
125
+ },
126
+ ],
127
+ },
128
+ ] });
129
+ const result = getCalculatedCartData(mockCart);
130
+ expect(result.orderAndLineItemDiscounts).toHaveLength(0);
131
+ expect(result.discountsTotalAmount).toBe(0);
132
+ expect(result.isFreeShipping).toBe(true);
133
+ expect(result.totalDiscountedPrice).toBe(14);
134
+ });
135
+ it("should exclude shipping from merchandise totals when merch + free ship stack (TICKET-4028)", () => {
136
+ const mockCart = Object.assign(Object.assign({}, baseCartData), { discounts: ["SAVE10", "FREESHIP"], cost: {
137
+ subtotalAmount: { amount: "100.00", currencyCode: "USD" },
138
+ totalAmount: { amount: "90.00", currencyCode: "USD" },
139
+ totalTaxAmount: { amount: "0.00", currencyCode: "USD" },
140
+ }, items: [
141
+ {
142
+ id: "line1",
143
+ quantity: 1,
144
+ productId: "prod1",
145
+ variantId: "var1",
146
+ productDetails: {},
147
+ variantDetails: {
148
+ compareAtPrice: { amount: "100.00", currencyCode: "USD" },
149
+ price: { amount: "100.00", currencyCode: "USD" },
150
+ },
151
+ discounts: [],
152
+ cost: { totalAmount: { amount: "100.00", currencyCode: "USD" } },
153
+ },
154
+ ], discountAllocations: [
155
+ {
156
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
157
+ code: "SAVE10",
158
+ },
159
+ {
160
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
161
+ code: "FREESHIP",
162
+ },
163
+ ], deliveryGroups: [
164
+ {
165
+ deliveryOptions: [
166
+ {
167
+ title: "Standard",
168
+ estimatedCost: { amount: "10.00", currencyCode: "USD" },
169
+ },
170
+ ],
171
+ },
172
+ ] });
173
+ const result = getCalculatedCartData(mockCart);
174
+ expect(result.orderAndLineItemDiscounts).toHaveLength(1);
175
+ expect(result.orderAndLineItemDiscounts[0].amount).toBe(10);
176
+ expect(result.discountsTotalAmount).toBe(10);
177
+ expect(result.isFreeShipping).toBe(true);
178
+ expect(result.totalDiscountedPrice).toBe(90);
179
+ });
91
180
  it("should calculate total discounted price correctly", () => {
92
181
  const mockCart = Object.assign(Object.assign({}, baseCartData), { discounts: ["Discount10"], items: [
93
182
  {
@@ -0,0 +1,24 @@
1
+ import { CartDiscountAllocation } from "app-studio-types";
2
+ type DeliveryOptionCost = {
3
+ estimatedCost?: {
4
+ amount?: string | number | null;
5
+ };
6
+ };
7
+ type CartWithDeliveryGroups = {
8
+ discountAllocations?: CartDiscountAllocation[];
9
+ deliveryGroups?: Array<{
10
+ deliveryOptions?: DeliveryOptionCost[];
11
+ }>;
12
+ cost?: {
13
+ subtotalAmount?: {
14
+ amount?: string | number | null;
15
+ };
16
+ totalAmount?: {
17
+ amount?: string | number | null;
18
+ };
19
+ };
20
+ };
21
+ export declare const isShippingDiscountAllocation: (allocation: CartDiscountAllocation, cart?: CartWithDeliveryGroups | null) => boolean;
22
+ export declare const isMerchandiseDiscountAllocation: (allocation: CartDiscountAllocation, cart?: CartWithDeliveryGroups | null) => boolean;
23
+ export {};
24
+ //# sourceMappingURL=discountAllocation.util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discountAllocation.util.d.ts","sourceRoot":"","sources":["../../lib/discountAllocation.util.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EAEvB,MAAM,kBAAkB,CAAA;AAEzB,KAAK,kBAAkB,GAAG;IACxB,aAAa,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CACpD,CAAA;AAED,KAAK,sBAAsB,GAAG;IAC5B,mBAAmB,CAAC,EAAE,sBAAsB,EAAE,CAAA;IAC9C,cAAc,CAAC,EAAE,KAAK,CAAC;QACrB,eAAe,CAAC,EAAE,kBAAkB,EAAE,CAAA;KACvC,CAAC,CAAA;IACF,IAAI,CAAC,EAAE;QACL,cAAc,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;SAAE,CAAA;QACpD,WAAW,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;SAAE,CAAA;KAClD,CAAA;CACF,CAAA;AAkMD,eAAO,MAAM,4BAA4B,eAC3B,sBAAsB,SAC3B,sBAAsB,GAAG,IAAI,KACnC,OAAiE,CAAA;AAEpE,eAAO,MAAM,+BAA+B,eAC9B,sBAAsB,SAC3B,sBAAsB,GAAG,IAAI,KACnC,OAAoE,CAAA"}
@@ -0,0 +1,150 @@
1
+ import { DiscountApplicationTargetType, } from "app-studio-types";
2
+ const MONEY_EPSILON = 0.01;
3
+ const parseAmount = (amount) => {
4
+ if (amount === undefined || amount === null || amount === "")
5
+ return 0;
6
+ const parsed = parseFloat(String(amount));
7
+ return Number.isFinite(parsed) ? parsed : 0;
8
+ };
9
+ const approxEqual = (a, b, epsilon = MONEY_EPSILON) => Math.abs(a - b) <= epsilon;
10
+ const getDeliveryOptionAmounts = (cart) => {
11
+ var _a;
12
+ if (!((_a = cart === null || cart === void 0 ? void 0 : cart.deliveryGroups) === null || _a === void 0 ? void 0 : _a.length))
13
+ return [];
14
+ return cart.deliveryGroups.flatMap((group) => {
15
+ var _a;
16
+ return ((_a = group.deliveryOptions) !== null && _a !== void 0 ? _a : []).map((option) => { var _a; return parseAmount((_a = option.estimatedCost) === null || _a === void 0 ? void 0 : _a.amount); });
17
+ });
18
+ };
19
+ const allocationMatchesDeliveryOptionCost = (allocation, cart) => {
20
+ var _a;
21
+ const allocationAmount = parseAmount((_a = allocation === null || allocation === void 0 ? void 0 : allocation.discountedAmount) === null || _a === void 0 ? void 0 : _a.amount);
22
+ if (!allocationAmount)
23
+ return false;
24
+ return getDeliveryOptionAmounts(cart).some((deliveryCost) => approxEqual(deliveryCost, allocationAmount));
25
+ };
26
+ const inferShippingWhenTargetTypeMissing = (allocation, cart) => {
27
+ var _a, _b, _c, _d, _e, _f;
28
+ if (!((_a = cart === null || cart === void 0 ? void 0 : cart.deliveryGroups) === null || _a === void 0 ? void 0 : _a.length))
29
+ return false;
30
+ const allocationAmount = parseAmount((_b = allocation === null || allocation === void 0 ? void 0 : allocation.discountedAmount) === null || _b === void 0 ? void 0 : _b.amount);
31
+ if (!allocationAmount)
32
+ return false;
33
+ const deliveryAmounts = getDeliveryOptionAmounts(cart);
34
+ if (!deliveryAmounts.length)
35
+ return false;
36
+ const subtotal = parseAmount((_d = (_c = cart === null || cart === void 0 ? void 0 : cart.cost) === null || _c === void 0 ? void 0 : _c.subtotalAmount) === null || _d === void 0 ? void 0 : _d.amount);
37
+ const total = parseAmount((_f = (_e = cart === null || cart === void 0 ? void 0 : cart.cost) === null || _e === void 0 ? void 0 : _e.totalAmount) === null || _f === void 0 ? void 0 : _f.amount);
38
+ if (subtotal > 0 && total > 0) {
39
+ if (approxEqual(total, subtotal - allocationAmount))
40
+ return false;
41
+ if (approxEqual(total, subtotal) &&
42
+ deliveryAmounts.some((cost) => approxEqual(cost, allocationAmount))) {
43
+ return true;
44
+ }
45
+ if (total > subtotal + MONEY_EPSILON &&
46
+ deliveryAmounts.some((cost) => approxEqual(cost, allocationAmount))) {
47
+ return true;
48
+ }
49
+ // Merchandise discounts reduce total below subtotal; unchanged total implies
50
+ // shipping-side allocation (e.g. FREESHIPPING with zeroed delivery rates).
51
+ if (approxEqual(total, subtotal) && allocationAmount > 0) {
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+ return allocationMatchesDeliveryOptionCost(allocation, cart);
57
+ };
58
+ /**
59
+ * When multiple allocations omit targetType (stacked merch + free ship), per-allocation
60
+ * inference misclassifies shipping as merchandise. Partition using merchBudget =
61
+ * subtotal − total (Shopify subtotalAmount is merchandise-only).
62
+ */
63
+ const partitionAmbiguousAllocations = (ambiguous, cart) => {
64
+ var _a, _b, _c, _d;
65
+ const result = new Map();
66
+ const subtotal = parseAmount((_b = (_a = cart === null || cart === void 0 ? void 0 : cart.cost) === null || _a === void 0 ? void 0 : _a.subtotalAmount) === null || _b === void 0 ? void 0 : _b.amount);
67
+ const total = parseAmount((_d = (_c = cart === null || cart === void 0 ? void 0 : cart.cost) === null || _c === void 0 ? void 0 : _c.totalAmount) === null || _d === void 0 ? void 0 : _d.amount);
68
+ const merchBudget = subtotal > 0 && total > 0 ? Math.max(0, subtotal - total) : 0;
69
+ const items = ambiguous.map((allocation) => {
70
+ var _a;
71
+ return ({
72
+ allocation,
73
+ amount: parseAmount((_a = allocation === null || allocation === void 0 ? void 0 : allocation.discountedAmount) === null || _a === void 0 ? void 0 : _a.amount),
74
+ matchesDelivery: allocationMatchesDeliveryOptionCost(allocation, cart),
75
+ });
76
+ });
77
+ let assignedMerch = 0;
78
+ for (const item of items) {
79
+ if (!item.matchesDelivery) {
80
+ result.set(item.allocation, "merchandise");
81
+ assignedMerch += item.amount;
82
+ }
83
+ }
84
+ for (const item of items.filter((i) => i.matchesDelivery)) {
85
+ if (assignedMerch + item.amount <= merchBudget + MONEY_EPSILON) {
86
+ result.set(item.allocation, "merchandise");
87
+ assignedMerch += item.amount;
88
+ }
89
+ else if (assignedMerch >= merchBudget - MONEY_EPSILON ||
90
+ assignedMerch + item.amount > merchBudget + MONEY_EPSILON) {
91
+ result.set(item.allocation, "shipping");
92
+ }
93
+ else {
94
+ result.set(item.allocation, "merchandise");
95
+ assignedMerch += item.amount;
96
+ }
97
+ }
98
+ return result;
99
+ };
100
+ const kindMapCache = new WeakMap();
101
+ const buildAllocationKindMap = (cart) => {
102
+ const allocations = cart === null || cart === void 0 ? void 0 : cart.discountAllocations;
103
+ if (!Array.isArray(allocations) || !allocations.length)
104
+ return null;
105
+ const cached = kindMapCache.get(cart);
106
+ if (cached)
107
+ return cached;
108
+ const map = new Map();
109
+ const ambiguous = [];
110
+ for (const allocation of allocations) {
111
+ if ((allocation === null || allocation === void 0 ? void 0 : allocation.targetType) === DiscountApplicationTargetType.ShippingLine) {
112
+ map.set(allocation, "shipping");
113
+ continue;
114
+ }
115
+ if ((allocation === null || allocation === void 0 ? void 0 : allocation.targetType) != null) {
116
+ map.set(allocation, "merchandise");
117
+ continue;
118
+ }
119
+ ambiguous.push(allocation);
120
+ }
121
+ if (ambiguous.length === 1) {
122
+ const allocation = ambiguous[0];
123
+ map.set(allocation, inferShippingWhenTargetTypeMissing(allocation, cart)
124
+ ? "shipping"
125
+ : "merchandise");
126
+ }
127
+ else if (ambiguous.length > 1) {
128
+ const partitioned = partitionAmbiguousAllocations(ambiguous, cart);
129
+ partitioned.forEach((kind, allocation) => map.set(allocation, kind));
130
+ }
131
+ kindMapCache.set(cart, map);
132
+ return map;
133
+ };
134
+ const resolveAllocationKind = (allocation, cart) => {
135
+ if ((allocation === null || allocation === void 0 ? void 0 : allocation.targetType) === DiscountApplicationTargetType.ShippingLine) {
136
+ return "shipping";
137
+ }
138
+ if ((allocation === null || allocation === void 0 ? void 0 : allocation.targetType) != null) {
139
+ return "merchandise";
140
+ }
141
+ const kindMap = buildAllocationKindMap(cart);
142
+ const kind = kindMap === null || kindMap === void 0 ? void 0 : kindMap.get(allocation);
143
+ if (kind)
144
+ return kind;
145
+ return inferShippingWhenTargetTypeMissing(allocation, cart)
146
+ ? "shipping"
147
+ : "merchandise";
148
+ };
149
+ export const isShippingDiscountAllocation = (allocation, cart) => resolveAllocationKind(allocation, cart) === "shipping";
150
+ export const isMerchandiseDiscountAllocation = (allocation, cart) => resolveAllocationKind(allocation, cart) === "merchandise";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=discountAllocation.util.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discountAllocation.util.test.d.ts","sourceRoot":"","sources":["../../lib/discountAllocation.util.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,200 @@
1
+ import { DiscountApplicationTargetType, } from "app-studio-types";
2
+ import { isMerchandiseDiscountAllocation, isShippingDiscountAllocation, } from "./discountAllocation.util";
3
+ const naturalLifeDeliveryGroups = [
4
+ {
5
+ deliveryOptions: [
6
+ {
7
+ title: "Ground Shipping",
8
+ estimatedCost: { amount: "7.95", currencyCode: "USD" },
9
+ },
10
+ {
11
+ title: "Express Shipping",
12
+ estimatedCost: { amount: "18.95", currencyCode: "USD" },
13
+ },
14
+ ],
15
+ },
16
+ ];
17
+ const naturalLifeFreeShippingCost = {
18
+ subtotalAmount: { amount: "14.00", currencyCode: "USD" },
19
+ totalAmount: { amount: "14.00", currencyCode: "USD" },
20
+ };
21
+ describe("discountAllocation.util", () => {
22
+ describe("isShippingDiscountAllocation", () => {
23
+ it("returns true when targetType is SHIPPING_LINE", () => {
24
+ const allocation = {
25
+ targetType: DiscountApplicationTargetType.ShippingLine,
26
+ discountedAmount: { amount: "5.00", currencyCode: "USD" },
27
+ };
28
+ expect(isShippingDiscountAllocation(allocation)).toBe(true);
29
+ expect(isMerchandiseDiscountAllocation(allocation)).toBe(false);
30
+ });
31
+ it("returns false when targetType is LINE_ITEM", () => {
32
+ const allocation = {
33
+ targetType: DiscountApplicationTargetType.LineItem,
34
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
35
+ };
36
+ expect(isShippingDiscountAllocation(allocation)).toBe(false);
37
+ expect(isMerchandiseDiscountAllocation(allocation)).toBe(true);
38
+ });
39
+ it("infers shipping when targetType is missing and amount matches a delivery option (TICKET-4028)", () => {
40
+ const allocation = {
41
+ discountedAmount: { amount: "7.95", currencyCode: "USD" },
42
+ code: "CEXFS9P882N",
43
+ };
44
+ const cart = {
45
+ deliveryGroups: naturalLifeDeliveryGroups,
46
+ cost: naturalLifeFreeShippingCost,
47
+ };
48
+ expect(isShippingDiscountAllocation(allocation, cart)).toBe(true);
49
+ expect(isMerchandiseDiscountAllocation(allocation, cart)).toBe(false);
50
+ });
51
+ it("infers shipping without cart.cost when amount matches delivery (legacy fallback)", () => {
52
+ const allocation = {
53
+ discountedAmount: { amount: "7.95", currencyCode: "USD" },
54
+ code: "CEXFS9P882N",
55
+ };
56
+ const cart = { deliveryGroups: naturalLifeDeliveryGroups };
57
+ expect(isShippingDiscountAllocation(allocation, cart)).toBe(true);
58
+ expect(isMerchandiseDiscountAllocation(allocation, cart)).toBe(false);
59
+ });
60
+ it("treats missing targetType as merchandise when amount equals shipping but total dropped vs subtotal", () => {
61
+ const allocation = {
62
+ discountedAmount: { amount: "7.95", currencyCode: "USD" },
63
+ code: "SAVE795",
64
+ };
65
+ const cart = {
66
+ deliveryGroups: naturalLifeDeliveryGroups,
67
+ cost: {
68
+ subtotalAmount: { amount: "14.00", currencyCode: "USD" },
69
+ totalAmount: { amount: "6.05", currencyCode: "USD" },
70
+ },
71
+ };
72
+ expect(isShippingDiscountAllocation(allocation, cart)).toBe(false);
73
+ expect(isMerchandiseDiscountAllocation(allocation, cart)).toBe(true);
74
+ });
75
+ it("treats missing targetType as merchandise when amount is less than shipping and total exceeds subtotal", () => {
76
+ const allocation = {
77
+ discountedAmount: { amount: "5.00", currencyCode: "USD" },
78
+ code: "SAVE5",
79
+ };
80
+ const cart = {
81
+ deliveryGroups: naturalLifeDeliveryGroups,
82
+ cost: {
83
+ subtotalAmount: { amount: "50.00", currencyCode: "USD" },
84
+ totalAmount: { amount: "52.95", currencyCode: "USD" },
85
+ },
86
+ };
87
+ expect(isShippingDiscountAllocation(allocation, cart)).toBe(false);
88
+ expect(isMerchandiseDiscountAllocation(allocation, cart)).toBe(true);
89
+ });
90
+ it("infers shipping when total exceeds subtotal and amount exactly matches delivery cost", () => {
91
+ const allocation = {
92
+ discountedAmount: { amount: "7.95", currencyCode: "USD" },
93
+ code: "FREESHIP",
94
+ };
95
+ const cart = {
96
+ deliveryGroups: naturalLifeDeliveryGroups,
97
+ cost: {
98
+ subtotalAmount: { amount: "50.00", currencyCode: "USD" },
99
+ totalAmount: { amount: "57.95", currencyCode: "USD" },
100
+ },
101
+ };
102
+ expect(isShippingDiscountAllocation(allocation, cart)).toBe(true);
103
+ expect(isMerchandiseDiscountAllocation(allocation, cart)).toBe(false);
104
+ });
105
+ it("infers shipping when total equals subtotal despite delivery cost mismatch (FREESHIPPING zeroed rates)", () => {
106
+ const allocation = {
107
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
108
+ code: "FREESHIPPING",
109
+ };
110
+ const cart = {
111
+ deliveryGroups: [
112
+ {
113
+ deliveryOptions: [
114
+ {
115
+ title: "Standard",
116
+ estimatedCost: { amount: "0.0", currencyCode: "USD" },
117
+ },
118
+ {
119
+ title: "Express Shipping",
120
+ estimatedCost: { amount: "0.0", currencyCode: "USD" },
121
+ },
122
+ ],
123
+ },
124
+ ],
125
+ cost: {
126
+ subtotalAmount: { amount: "50.0", currencyCode: "USD" },
127
+ totalAmount: { amount: "50.0", currencyCode: "USD" },
128
+ },
129
+ };
130
+ expect(isShippingDiscountAllocation(allocation, cart)).toBe(true);
131
+ expect(isMerchandiseDiscountAllocation(allocation, cart)).toBe(false);
132
+ });
133
+ it("infers shipping for single allocation when total equals subtotal (unchanged merchandise total)", () => {
134
+ const allocation = {
135
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
136
+ code: "SAVE10",
137
+ };
138
+ const cart = {
139
+ deliveryGroups: naturalLifeDeliveryGroups,
140
+ cost: naturalLifeFreeShippingCost,
141
+ };
142
+ expect(isShippingDiscountAllocation(allocation, cart)).toBe(true);
143
+ expect(isMerchandiseDiscountAllocation(allocation, cart)).toBe(false);
144
+ });
145
+ it("treats missing targetType as merchandise when cart has no deliveryGroups", () => {
146
+ const allocation = {
147
+ discountedAmount: { amount: "7.95", currencyCode: "USD" },
148
+ code: "CEXFS9P882N",
149
+ };
150
+ expect(isShippingDiscountAllocation(allocation, {})).toBe(false);
151
+ expect(isMerchandiseDiscountAllocation(allocation, {})).toBe(true);
152
+ });
153
+ it("treats missing targetType as merchandise when deliveryGroups is empty (pre-checkout)", () => {
154
+ const allocation = {
155
+ discountedAmount: { amount: "7.95", currencyCode: "GBP" },
156
+ code: "PROMO",
157
+ };
158
+ const cart = {
159
+ deliveryGroups: [],
160
+ cost: {
161
+ subtotalAmount: { amount: "5.7", currencyCode: "GBP" },
162
+ totalAmount: { amount: "5.7", currencyCode: "GBP" },
163
+ },
164
+ };
165
+ expect(isShippingDiscountAllocation(allocation, cart)).toBe(false);
166
+ expect(isMerchandiseDiscountAllocation(allocation, cart)).toBe(true);
167
+ });
168
+ it("partitions stacked $10 off + $10 free ship when targetType is missing (TICKET-4028)", () => {
169
+ const merchAllocation = {
170
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
171
+ code: "SAVE10",
172
+ };
173
+ const shipAllocation = {
174
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
175
+ code: "FREESHIP",
176
+ };
177
+ const cart = {
178
+ discountAllocations: [merchAllocation, shipAllocation],
179
+ deliveryGroups: [
180
+ {
181
+ deliveryOptions: [
182
+ {
183
+ title: "Standard",
184
+ estimatedCost: { amount: "10.00", currencyCode: "USD" },
185
+ },
186
+ ],
187
+ },
188
+ ],
189
+ cost: {
190
+ subtotalAmount: { amount: "100.00", currencyCode: "USD" },
191
+ totalAmount: { amount: "90.00", currencyCode: "USD" },
192
+ },
193
+ };
194
+ expect(isMerchandiseDiscountAllocation(merchAllocation, cart)).toBe(true);
195
+ expect(isShippingDiscountAllocation(merchAllocation, cart)).toBe(false);
196
+ expect(isShippingDiscountAllocation(shipAllocation, cart)).toBe(true);
197
+ expect(isMerchandiseDiscountAllocation(shipAllocation, cart)).toBe(false);
198
+ });
199
+ });
200
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"variablesCart.util.d.ts","sourceRoot":"","sources":["../../lib/variablesCart.util.ts"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,sBAAsB,EACtB,6BAA6B,EAG9B,MAAM,kBAAkB,CAAA;AAEzB,eAAO,MAAM,2BAA2B;;cAM5B,MAAM;;0BAEI;YAChB,gBAAgB,CAAC,EAAE,MAAM,CAAA;YACzB,KAAK,EAAE,MAAM,CAAA;YACb,cAAc,CAAC,EAAE,MAAM,CAAA;SACxB,EAAE;;;;;;CAyBN,CAAA;AA8FD,eAAO,MAAM,6BAA6B,SAClC,MAAM,QACN,SAAS,GAAG,IAAI,YAUvB,CAAA;AAED,eAAO,MAAM,2BAA2B,SAChC,MAAM,QACN,SAAS,GAAG,IAAI,YAUvB,CAAA;AAoMD,MAAM,MAAM,uBAAuB,GAAG;IACpC,yBAAyB,EAAE,sBAAsB,EAAE,CAAA;IACnD,gBAAgB,EAAE,6BAA6B,EAAE,CAAA;IACjD,cAAc,EAAE,OAAO,CAAA;IACvB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,eAAO,MAAM,iCAAiC,EAAE,uBAU/C,CAAA;AAED,eAAO,MAAM,8BAA8B,SACnC,SAAS,GAAG,IAAI,KACrB,uBAmCF,CAAA"}
1
+ {"version":3,"file":"variablesCart.util.d.ts","sourceRoot":"","sources":["../../lib/variablesCart.util.ts"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,sBAAsB,EACtB,6BAA6B,EAE9B,MAAM,kBAAkB,CAAA;AAMzB,eAAO,MAAM,2BAA2B;;cAM5B,MAAM;;0BAEI;YAChB,gBAAgB,CAAC,EAAE,MAAM,CAAA;YACzB,KAAK,EAAE,MAAM,CAAA;YACb,cAAc,CAAC,EAAE,MAAM,CAAA;SACxB,EAAE;;;;;;CAyBN,CAAA;AA4FD,eAAO,MAAM,6BAA6B,SAClC,MAAM,QACN,SAAS,GAAG,IAAI,YAUvB,CAAA;AAED,eAAO,MAAM,2BAA2B,SAChC,MAAM,QACN,SAAS,GAAG,IAAI,YAUvB,CAAA;AAoMD,MAAM,MAAM,uBAAuB,GAAG;IACpC,yBAAyB,EAAE,sBAAsB,EAAE,CAAA;IACnD,gBAAgB,EAAE,6BAA6B,EAAE,CAAA;IACjD,cAAc,EAAE,OAAO,CAAA;IACvB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,eAAO,MAAM,iCAAiC,EAAE,uBAU/C,CAAA;AAED,eAAO,MAAM,8BAA8B,SACnC,SAAS,GAAG,IAAI,KACrB,uBAmCF,CAAA"}
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
2
- import { DiscountApplicationTargetType, } from "app-studio-types";
1
+ import { isMerchandiseDiscountAllocation, isShippingDiscountAllocation, } from "./discountAllocation.util";
3
2
  export const getSubscriptionPriceDetails = ({ totalAmount, quantity, sellingPlanAllocation = { priceAdjustments: [] }, }) => {
4
3
  var _a;
5
4
  const isSubscription = ((_a = sellingPlanAllocation === null || sellingPlanAllocation === void 0 ? void 0 : sellingPlanAllocation.priceAdjustments) === null || _a === void 0 ? void 0 : _a.length) > 0;
@@ -34,7 +33,7 @@ const getOrderLevelDiscounts = (cart) => {
34
33
  return [];
35
34
  // We'll build a map of code -> { amount, isAutomatic }
36
35
  const discountMap = discountAllocations
37
- .filter((discount) => discount.targetType !== DiscountApplicationTargetType.ShippingLine)
36
+ .filter((discount) => isMerchandiseDiscountAllocation(discount, cart))
38
37
  .reduce((acc, discount) => {
39
38
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
40
39
  // Figure out the code for this discount
@@ -87,7 +86,7 @@ const getShippingDiscounts = (cart) => {
87
86
  if (!discountAllocations)
88
87
  return [];
89
88
  return discountAllocations
90
- .filter((discount) => discount.targetType === DiscountApplicationTargetType.ShippingLine)
89
+ .filter((discount) => isShippingDiscountAllocation(discount, cart))
91
90
  .map((discount) => {
92
91
  var _a;
93
92
  return ({
@@ -103,6 +103,91 @@ describe("cart-provider.util", () => {
103
103
  const result = getVariablesCalculatedCartData(mockCart);
104
104
  expect(result.isFreeShipping).toBe(true);
105
105
  });
106
+ it("should exclude shipping discount without targetType from merchandise totals (TICKET-4028)", () => {
107
+ const mockCart = Object.assign(Object.assign({}, baseCartData), { discounts: ["CEXFS9P882N"], cost: {
108
+ subtotalAmount: { amount: "14.00", currencyCode: "USD" },
109
+ totalAmount: { amount: "14.00", currencyCode: "USD" },
110
+ totalTaxAmount: { amount: "0.00", currencyCode: "USD" },
111
+ }, items: [
112
+ {
113
+ id: "line1",
114
+ quantity: 1,
115
+ productId: "prod1",
116
+ variantId: "var1",
117
+ price: 14.0,
118
+ compareAtPrice: 14.0,
119
+ currencyCode: "USD",
120
+ discounts: [],
121
+ cost: { totalAmount: { amount: "14.00", currencyCode: "USD" } },
122
+ },
123
+ ], discountAllocations: [
124
+ {
125
+ discountedAmount: { amount: "7.95", currencyCode: "USD" },
126
+ code: "CEXFS9P882N",
127
+ },
128
+ ], deliveryGroups: [
129
+ {
130
+ deliveryOptions: [
131
+ {
132
+ title: "Ground Shipping",
133
+ estimatedCost: { amount: "7.95", currencyCode: "USD" },
134
+ },
135
+ {
136
+ title: "Express Shipping",
137
+ estimatedCost: { amount: "18.95", currencyCode: "USD" },
138
+ },
139
+ ],
140
+ },
141
+ ] });
142
+ const result = getVariablesCalculatedCartData(mockCart);
143
+ expect(result.orderAndLineItemDiscounts).toHaveLength(0);
144
+ expect(result.discountsTotalAmount).toBe(0);
145
+ expect(result.isFreeShipping).toBe(true);
146
+ expect(result.totalDiscountedPrice).toBe(14);
147
+ });
148
+ it("should exclude shipping from merchandise totals when merch + free ship stack (TICKET-4028)", () => {
149
+ const mockCart = Object.assign(Object.assign({}, baseCartData), { discounts: ["SAVE10", "FREESHIP"], cost: {
150
+ subtotalAmount: { amount: "100.00", currencyCode: "USD" },
151
+ totalAmount: { amount: "90.00", currencyCode: "USD" },
152
+ totalTaxAmount: { amount: "0.00", currencyCode: "USD" },
153
+ }, items: [
154
+ {
155
+ id: "line1",
156
+ quantity: 1,
157
+ productId: "prod1",
158
+ variantId: "var1",
159
+ price: 100.0,
160
+ compareAtPrice: 100.0,
161
+ currencyCode: "USD",
162
+ discounts: [],
163
+ cost: { totalAmount: { amount: "100.00", currencyCode: "USD" } },
164
+ },
165
+ ], discountAllocations: [
166
+ {
167
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
168
+ code: "SAVE10",
169
+ },
170
+ {
171
+ discountedAmount: { amount: "10.00", currencyCode: "USD" },
172
+ code: "FREESHIP",
173
+ },
174
+ ], deliveryGroups: [
175
+ {
176
+ deliveryOptions: [
177
+ {
178
+ title: "Standard",
179
+ estimatedCost: { amount: "10.00", currencyCode: "USD" },
180
+ },
181
+ ],
182
+ },
183
+ ] });
184
+ const result = getVariablesCalculatedCartData(mockCart);
185
+ expect(result.orderAndLineItemDiscounts).toHaveLength(1);
186
+ expect(result.orderAndLineItemDiscounts[0].amount).toBe(10);
187
+ expect(result.discountsTotalAmount).toBe(10);
188
+ expect(result.isFreeShipping).toBe(true);
189
+ expect(result.totalDiscountedPrice).toBe(90);
190
+ });
106
191
  it("should calculate total discounted price correctly", () => {
107
192
  const mockCart = Object.assign(Object.assign({}, baseCartData), { discounts: ["Discount10"], items: [
108
193
  {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=icon-before-injection-identity.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon-before-injection-identity.test.d.ts","sourceRoot":"","sources":["../../tests/icon-before-injection-identity.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from "@testing-library/react";
3
+ import { Icon } from "../components/ui/icon";
4
+ // A stable beforeInjection identity is what stops react-svg's shallowDiffers
5
+ // check from tearing down and re-injecting the SVG on every parent re-render —
6
+ // the amplifier behind Sentry NEXTJS-CONSUMER-PAGES-78B.
7
+ const mockBeforeInjections = [];
8
+ jest.mock("react-svg", () => ({
9
+ ReactSVG: ({ beforeInjection, }) => {
10
+ mockBeforeInjections.push(beforeInjection);
11
+ return null;
12
+ },
13
+ }));
14
+ describe("Icon beforeInjection identity", () => {
15
+ beforeEach(() => {
16
+ mockBeforeInjections.length = 0;
17
+ });
18
+ it("keeps the same beforeInjection reference across rerenders with identical props", () => {
19
+ const { rerender } = render(_jsx(Icon, { url: "https://assets.example.com/icon.svg", color: "red", size: "md" }));
20
+ rerender(_jsx(Icon, { url: "https://assets.example.com/icon.svg", color: "red", size: "md" }));
21
+ expect(mockBeforeInjections).toHaveLength(2);
22
+ expect(mockBeforeInjections[1]).toBe(mockBeforeInjections[0]);
23
+ });
24
+ it("produces a new beforeInjection reference when a styling prop changes", () => {
25
+ const { rerender } = render(_jsx(Icon, { url: "https://assets.example.com/icon.svg", color: "red", size: "md" }));
26
+ rerender(_jsx(Icon, { url: "https://assets.example.com/icon.svg", color: "blue", size: "md" }));
27
+ expect(mockBeforeInjections).toHaveLength(2);
28
+ expect(mockBeforeInjections[1]).not.toBe(mockBeforeInjections[0]);
29
+ });
30
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=svg-injector-double-delivery.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svg-injector-double-delivery.test.d.ts","sourceRoot":"","sources":["../../tests/svg-injector-double-delivery.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,117 @@
1
+ import { SVGInjector } from "@tanem/svg-injector";
2
+ // Deterministic repro of Sentry NEXTJS-CONSUMER-PAGES-78B: @tanem/svg-injector
3
+ // shares one module-level request queue + cache per URL; a cached Error
4
+ // re-arms the cache and re-issues the XHR while the old queue entries stay
5
+ // live, so a later successful pass re-delivers to an injection that already
6
+ // finalized (el = null) and crashes on el.getAttribute("id"). Our pnpm patch
7
+ // (patches/@tanem__svg-injector@10.1.68.patch) drops the stale delivery.
8
+ class MockXHR {
9
+ constructor() {
10
+ this.onreadystatechange = null;
11
+ this.readyState = 0;
12
+ this.status = 0;
13
+ this.statusText = "";
14
+ this.responseXML = null;
15
+ this.withCredentials = false;
16
+ MockXHR.instances.push(this);
17
+ }
18
+ open() { }
19
+ send() { }
20
+ overrideMimeType() { }
21
+ getResponseHeader() {
22
+ return "image/svg+xml";
23
+ }
24
+ // No-op: the spec's abort-refires-readystatechange aggravator is excluded
25
+ // so the scripted interleaving stays exact.
26
+ abort() { }
27
+ fail() {
28
+ var _a;
29
+ this.readyState = 4;
30
+ this.status = 404;
31
+ this.responseXML = null;
32
+ (_a = this.onreadystatechange) === null || _a === void 0 ? void 0 : _a.call(this);
33
+ }
34
+ succeed(svgMarkup) {
35
+ var _a;
36
+ this.readyState = 4;
37
+ this.status = 200;
38
+ this.responseXML = new DOMParser().parseFromString(svgMarkup, "image/svg+xml");
39
+ (_a = this.onreadystatechange) === null || _a === void 0 ? void 0 : _a.call(this);
40
+ }
41
+ }
42
+ MockXHR.instances = [];
43
+ const makeTarget = (url) => {
44
+ const wrapper = document.createElement("div");
45
+ wrapper.setAttribute("data-src", url);
46
+ const container = document.createElement("div");
47
+ container.appendChild(wrapper);
48
+ document.body.appendChild(container);
49
+ return { wrapper, container };
50
+ };
51
+ const realXHR = window.XMLHttpRequest;
52
+ describe("svg-injector stale queue re-delivery", () => {
53
+ beforeEach(() => {
54
+ jest.useFakeTimers();
55
+ window.XMLHttpRequest = MockXHR;
56
+ MockXHR.instances.length = 0;
57
+ });
58
+ afterEach(() => {
59
+ jest.useRealTimers();
60
+ window.XMLHttpRequest = realXHR;
61
+ document.body.innerHTML = "";
62
+ });
63
+ it("drops re-delivery to a finalized injection instead of crashing, and still injects the live target", () => {
64
+ const url = "https://assets.example.com/double-delivery.svg";
65
+ const targetA = makeTarget(url);
66
+ const targetC = makeTarget(url);
67
+ const targetE = makeTarget(url);
68
+ const deliveries = [];
69
+ // 1. A starts the request for U -> queue [cbA], XHR1 in flight.
70
+ SVGInjector(targetA.wrapper, {
71
+ afterEach: (error) => deliveries.push({ id: "A", error }),
72
+ });
73
+ expect(MockXHR.instances).toHaveLength(1);
74
+ // XHR1 errors -> cache = Error, error pass P1 scheduled via setTimeout(0).
75
+ MockXHR.instances[0].fail();
76
+ // 2. C injects U before P1's timer fires: the cached Error falls through,
77
+ // re-arms the cache to undefined and re-issues the XHR -> queue
78
+ // [cbA, cbC], XHR2 in flight.
79
+ SVGInjector(targetC.wrapper, {
80
+ afterEach: (error) => {
81
+ deliveries.push({ id: "C", error });
82
+ // 4. E injects U from inside C's error delivery (pass-2, index 1):
83
+ // the queue grows to [cbA, cbC, cbE], so the pass's last-index
84
+ // cleanup never fires and the finalized cbA stays queued.
85
+ SVGInjector(targetE.wrapper, {
86
+ afterEach: (innerError) => deliveries.push({ id: "E", error: innerError }),
87
+ });
88
+ },
89
+ });
90
+ expect(MockXHR.instances).toHaveLength(2);
91
+ // 3. P1 runs against the re-armed (undefined) cache: no delivery, and the
92
+ // queue is not cleaned up because it grew past P1's snapshot.
93
+ jest.runOnlyPendingTimers();
94
+ expect(deliveries).toHaveLength(0);
95
+ // XHR2 errors -> pass P2 delivers the error to A (finalizing it, el =
96
+ // null) and to C, whose callback injects E and re-issues XHR3.
97
+ MockXHR.instances[1].fail();
98
+ jest.runOnlyPendingTimers();
99
+ expect(deliveries.map((d) => d.id)).toEqual(["A", "C"]);
100
+ expect(MockXHR.instances).toHaveLength(3);
101
+ // XHR3 succeeds -> pass P3 walks the surviving queue from index 0,
102
+ // re-delivering a cloned SVG to the finalized cbA. Unpatched, this throws
103
+ // TypeError (el.getAttribute on null) out of the timer flush.
104
+ MockXHR.instances[2].succeed('<svg xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z"/></svg>');
105
+ expect(() => jest.runOnlyPendingTimers()).not.toThrow();
106
+ // The stale deliveries were dropped, not double-fired.
107
+ expect(deliveries.filter((d) => d.id === "A")).toHaveLength(1);
108
+ expect(deliveries.filter((d) => d.id === "C")).toHaveLength(1);
109
+ // The live target E still completes: its wrapper is replaced with the SVG.
110
+ expect(deliveries).toEqual([
111
+ { id: "A", error: expect.any(Error) },
112
+ { id: "C", error: expect.any(Error) },
113
+ { id: "E", error: null },
114
+ ]);
115
+ expect(targetE.container.querySelector("svg.injected-svg")).not.toBeNull();
116
+ });
117
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapcart/mobile-components",
3
- "version": "0.14.1",
3
+ "version": "0.14.6",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "style": "dist/styles.css",