Skip to content

Commit 0871fa4

Browse files
committed
fix(herbatika): centralize storefront currency pricing
1 parent 5a95719 commit 0871fa4

21 files changed

Lines changed: 300 additions & 293 deletions

apps/herbatika/src/components/brands/use-brand-listing-controller.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useRegionContext } from "@techsio/storefront-data/shared/region-context";
44
import { useQueryStates } from "nuqs";
55
import { useEffect, useMemo } from "react";
6-
import { resolveProductCurrencyCode } from "@/components/category/category-product-utils";
76
import { useCategoryFacetItems } from "@/components/category/use-category-facet-items";
87
import {
98
buildCatalogProductsParams,
@@ -13,6 +12,7 @@ import {
1312
import { useCatalogProducts } from "@/lib/storefront/catalog-products";
1413
import { resolveErrorMessage } from "@/lib/storefront/error-utils";
1514
import { PLP_PAGE_SIZE, plpQueryParsers } from "@/lib/storefront/plp-query-state";
15+
import { resolveRegionCurrency } from "@/lib/storefront/region-selection";
1616
import {
1717
useCatalogListingInteractions,
1818
useCatalogListingPageBounds,
@@ -26,6 +26,7 @@ export function useBrandListingController({
2626
brandFacetId,
2727
}: UseBrandListingControllerProps) {
2828
const region = useRegionContext();
29+
const regionCurrencyCode = resolveRegionCurrency(region);
2930
const [queryState, setQueryState] = useQueryStates(plpQueryParsers);
3031
const visibleQueryState = useMemo(
3132
() => ({
@@ -135,7 +136,7 @@ export function useBrandListingController({
135136
(catalogQuery.products.length > 0 || catalogQuery.query.isPlaceholderData),
136137
priceBounds: resolveCatalogPriceBounds(catalogQuery.facets.price),
137138
products: catalogQuery.products,
138-
productsCurrencyCode: resolveProductCurrencyCode(catalogQuery.products),
139+
productsCurrencyCode: regionCurrencyCode,
139140
queryState,
140141
};
141142
}

apps/herbatika/src/components/category/category-listing.helpers.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

apps/herbatika/src/components/category/category-product-utils.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,46 +17,3 @@ export const resolveCategoryRank = (
1717

1818
return Number.MAX_SAFE_INTEGER;
1919
};
20-
21-
const asRecord = (value: unknown): Record<string, unknown> | null => {
22-
if (value && typeof value === "object" && !Array.isArray(value)) {
23-
return value as Record<string, unknown>;
24-
}
25-
26-
return null;
27-
};
28-
29-
export const resolveProductPriceAmount = (
30-
product: HttpTypes.StoreProduct,
31-
): number | null => {
32-
const amount = product.variants?.[0]?.calculated_price?.calculated_amount;
33-
return typeof amount === "number" && Number.isFinite(amount) ? amount : null;
34-
};
35-
36-
export const resolveProductCurrencyCode = (
37-
products: HttpTypes.StoreProduct[],
38-
): string => {
39-
for (const product of products) {
40-
const code = product.variants?.[0]?.calculated_price?.currency_code;
41-
if (typeof code === "string" && code.length === 3) {
42-
return code.toUpperCase();
43-
}
44-
}
45-
46-
return "EUR";
47-
};
48-
49-
export const resolveProductInStock = (
50-
product: HttpTypes.StoreProduct,
51-
): boolean => {
52-
const metadata = asRecord(product.metadata);
53-
const topOffer = asRecord(metadata?.top_offer);
54-
const stock = asRecord(topOffer?.stock);
55-
const amount = stock?.amount;
56-
57-
if (typeof amount === "number") {
58-
return amount > 0;
59-
}
60-
61-
return true;
62-
};

apps/herbatika/src/components/category/use-category-listing-queries.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import {
1313
import {
1414
normalizeCategoryName,
1515
resolveCategoryRank,
16-
resolveProductCurrencyCode,
1716
} from "@/components/category/category-product-utils";
1817
import { useCatalogProducts } from "@/lib/storefront/catalog-products";
1918
import {
2019
buildCatalogProductsParams,
2120
resolveCatalogActiveFilterCount,
21+
resolveCatalogPriceBounds,
2222
} from "@/lib/storefront/catalog-query-state";
2323
import {
2424
STOREFRONT_CATEGORY_TREE_FIELDS,
@@ -32,20 +32,7 @@ import {
3232
} from "@/lib/storefront/plp-query-state";
3333
import { useCategoryFacetItems } from "@/components/category/use-category-facet-items";
3434
import { resolveErrorMessage } from "@/lib/storefront/error-utils";
35-
36-
const resolvePriceBounds = (priceFacet: {
37-
min: number | null;
38-
max: number | null;
39-
}) => {
40-
if (priceFacet.min === null && priceFacet.max === null) {
41-
return null;
42-
}
43-
44-
return {
45-
min: priceFacet.min ?? 0,
46-
max: priceFacet.max ?? priceFacet.min ?? 1,
47-
};
48-
};
35+
import { resolveRegionCurrency } from "@/lib/storefront/region-selection";
4936

5037
const resolveBreadcrumbItems = (
5138
slug: string,
@@ -103,6 +90,7 @@ export function useCategoryListingQueries({
10390
slug,
10491
}: UseCategoryListingQueriesProps) {
10592
const region = useRegionContext();
93+
const regionCurrencyCode = resolveRegionCurrency(region);
10694
const categoriesQuery = useCategories({
10795
page: 1,
10896
limit: STOREFRONT_CATEGORY_TREE_LIMIT,
@@ -272,9 +260,9 @@ export function useCategoryListingQueries({
272260
categoriesQuery.isLoading ||
273261
catalogQuery.isLoading ||
274262
catalogFacetSeedQuery.isLoading,
275-
priceBounds: resolvePriceBounds(catalogQuery.facets.price),
263+
priceBounds: resolveCatalogPriceBounds(catalogQuery.facets.price),
276264
products: catalogQuery.products,
277-
productsCurrencyCode: resolveProductCurrencyCode(catalogQuery.products),
265+
productsCurrencyCode: regionCurrencyCode,
278266
showCategoryNotFound:
279267
!categoriesQuery.isLoading &&
280268
categoriesQuery.categories.length > 0 &&

apps/herbatika/src/components/checkout/sections/checkout-cart-step-section.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ import { resolveErrorMessage } from "@/lib/storefront/error-utils";
1515
import { resolveFreeShippingThresholdAmount } from "@/lib/storefront/free-shipping";
1616
import { formatCurrencyAmount } from "@/lib/storefront/price-format";
1717
import { STOREFRONT_PRODUCT_DETAIL_FIELDS } from "@/lib/storefront/products";
18-
19-
const resolveSupportedCurrency = (currencyCode: string): "EUR" | "CZK" => {
20-
return currencyCode.toUpperCase() === "CZK" ? "CZK" : "EUR";
21-
};
18+
import { resolveSupportedCurrencyCode } from "@/lib/storefront/currency";
2219

2320
type CheckoutCartStepSectionProps = {
2421
cartId?: string;
@@ -41,7 +38,7 @@ export function CheckoutCartStepSection({
4138

4239
const isPending =
4340
updateLineItemMutation.isPending || removeLineItemMutation.isPending;
44-
const supportedCurrencyCode = resolveSupportedCurrency(currencyCode);
41+
const supportedCurrencyCode = resolveSupportedCurrencyCode(currencyCode);
4542
const freeShippingThresholdAmount =
4643
resolveFreeShippingThresholdAmount(supportedCurrencyCode);
4744
const missingAmount =

apps/herbatika/src/components/checkout/use-checkout-controller.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
resolveEffectiveCheckoutAddressDetails,
2323
} from "@/lib/forms/checkout/address.form";
2424
import { resolveErrorMessage } from "@/lib/storefront/error-utils";
25+
import { resolveSupportedCurrencyCode } from "@/lib/storefront/currency";
26+
import { resolveRegionCurrency } from "@/lib/storefront/region-selection";
2527
import { storefront } from "@/lib/storefront/storefront";
2628
import { resolveHasStoredAddress } from "./checkout-address.utils";
2729
import { useCheckoutActions } from "./use-checkout-actions";
@@ -30,6 +32,7 @@ import { useCheckoutDetailsForm } from "./use-checkout-details-form";
3032
export function useCheckoutController() {
3133
const queryClient = useQueryClient();
3234
const region = useRegionContext();
35+
const regionCurrencyCode = resolveRegionCurrency(region);
3336
const authQuery = useAuth();
3437
const [allowCartAutoCreate, setAllowCartAutoCreate] = useState(true);
3538
const [completedOrderId, setCompletedOrderId] = useState<string | null>(null);
@@ -186,9 +189,10 @@ export function useCheckoutController() {
186189
return saveAddressSucceededRef.current;
187190
};
188191

189-
const currencyCode = useMemo(() => {
190-
return (cartQuery.cart?.currency_code ?? "eur").toUpperCase();
191-
}, [cartQuery.cart?.currency_code]);
192+
const currencyCode = resolveSupportedCurrencyCode(
193+
cartQuery.cart?.currency_code,
194+
regionCurrencyCode,
195+
);
192196

193197
const cartItems = cartQuery.cart?.items ?? [];
194198
const hasItems = cartQuery.itemCount > 0 || cartItems.length > 0;

apps/herbatika/src/components/herbatika-header.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { FocusEvent, FormEvent } from "react";
1313
import { useState } from "react";
1414
import { cartReadQueryOptions, useCart } from "@/lib/storefront/cart";
1515
import { resolveCartTotalAmount } from "@/lib/storefront/cart-calculations";
16+
import { resolveSupportedCurrencyCode } from "@/lib/storefront/currency";
1617
import { formatCurrencyAmount } from "@/lib/storefront/price-format";
1718
import { resolveRegionCurrency } from "@/lib/storefront/region-selection";
1819
import { HerbatikaAccountPopover } from "./header/herbatika-account-popover";
@@ -57,10 +58,14 @@ export function HerbatikaHeader() {
5758
},
5859
);
5960

60-
const currency = resolveRegionCurrency(region);
61+
const regionCurrency = resolveRegionCurrency(region);
62+
const cartCurrency = resolveSupportedCurrencyCode(
63+
cart?.currency_code,
64+
regionCurrency,
65+
);
6166
const cartTotalLabel = formatCurrencyAmount(
6267
resolveCartTotalAmount(cart),
63-
currency,
68+
cartCurrency,
6469
);
6570

6671
const handleSearchSubmit = (event: FormEvent<HTMLFormElement>) => {
@@ -101,7 +106,7 @@ export function HerbatikaHeader() {
101106
<div className="hidden w-full max-w-search-form flex-1 @header-desktop:block">
102107
<SearchAutocomplete
103108
countryCode={region?.country_code}
104-
currencyCode={currency}
109+
currencyCode={regionCurrency}
105110
onSubmit={handleSearchSubmit}
106111
regionId={region?.region_id}
107112
variant="desktop"
@@ -138,7 +143,7 @@ export function HerbatikaHeader() {
138143
<HerbatikaCartPopover
139144
cart={cart}
140145
cartTotalLabel={cartTotalLabel}
141-
currencyCode={currency}
146+
currencyCode={cartCurrency}
142147
itemCount={itemCount}
143148
/>
144149
</Header.Actions>
@@ -170,7 +175,7 @@ export function HerbatikaHeader() {
170175
<div className="mx-auto w-full max-w-max-w px-header-lg pb-300 2xl:px-header-2xl @header-desktop:hidden">
171176
<SearchAutocomplete
172177
countryCode={region?.country_code}
173-
currencyCode={currency}
178+
currencyCode={regionCurrency}
174179
onSubmit={handleSearchSubmit}
175180
regionId={region?.region_id}
176181
variant="mobile"

apps/herbatika/src/components/herbatika-product-card.shared.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"use client";
22

33
import type { HttpTypes } from "@medusajs/types";
4+
import { useRegionContext } from "@techsio/storefront-data/shared/region-context";
45
import { useEffect, useState } from "react";
56
import { PRODUCT_FALLBACK_IMAGE } from "@/components/product-card/product-card.constants";
67
import { resolvePriceState } from "@/components/product-card/product-card.pricing";
78
import { resolveThumbnail } from "@/components/product-card/product-card.thumbnail";
9+
import { resolveRegionCurrency } from "@/lib/storefront/region-selection";
810

911
export type HerbatikaProductCardBaseProps = {
1012
product: HttpTypes.StoreProduct;
@@ -16,8 +18,10 @@ export function useHerbatikaProductCardState(
1618
product: HttpTypes.StoreProduct,
1719
onImageError?: () => void,
1820
) {
21+
const region = useRegionContext();
22+
const currencyCode = resolveRegionCurrency(region);
1923
const productHref = product.handle ? `/p/${product.handle}` : "/#";
20-
const price = resolvePriceState(product);
24+
const price = resolvePriceState(product, currencyCode);
2125
const thumbnail = resolveThumbnail(product);
2226
const [imageSrc, setImageSrc] = useState(thumbnail);
2327
const title = product.title || "Produkt";

apps/herbatika/src/components/product-card/product-card.pricing.ts

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,58 @@
11
import type { HttpTypes } from "@medusajs/types";
2-
import { formatCurrencyAmount } from "@/lib/storefront/price-format";
32
import {
43
DEFAULT_CURRENCY_CODE,
5-
asCurrencyCode,
4+
resolveSupportedCurrencyCode,
5+
} from "@/lib/storefront/currency";
6+
import { formatCurrencyAmount } from "@/lib/storefront/price-format";
7+
import {
68
resolveProductTopOffer,
7-
resolveTopOfferCurrentAmount,
8-
resolveTopOfferOriginalAmount,
9+
resolveStorefrontPrice,
910
} from "@/lib/storefront/product-pricing";
1011
import type { ProductPriceState } from "./product-card.types";
1112

1213
export const resolvePriceState = (
1314
product: HttpTypes.StoreProduct,
15+
expectedCurrencyCode?: string | null,
1416
): ProductPriceState => {
1517
const calculatedPrice = product.variants?.[0]?.calculated_price;
16-
const calculatedAmount = calculatedPrice?.calculated_amount;
17-
const calculatedOriginalAmount = calculatedPrice?.original_amount;
1818
const topOffer = resolveProductTopOffer(product);
19-
20-
const currentAmount =
21-
typeof calculatedAmount === "number"
22-
? calculatedAmount
23-
: resolveTopOfferCurrentAmount(topOffer);
24-
const currencyCode =
25-
asCurrencyCode(calculatedPrice?.currency_code) ??
26-
asCurrencyCode(topOffer?.currency) ??
27-
DEFAULT_CURRENCY_CODE;
28-
29-
const calculatedOriginal =
30-
typeof calculatedOriginalAmount === "number" &&
31-
typeof currentAmount === "number" &&
32-
calculatedOriginalAmount > currentAmount
33-
? calculatedOriginalAmount
34-
: null;
35-
const offerOriginal = resolveTopOfferOriginalAmount({
36-
currentAmount,
19+
const price = resolveStorefrontPrice({
20+
calculatedAmount: calculatedPrice?.calculated_amount,
21+
calculatedCurrencyCode: calculatedPrice?.currency_code,
22+
calculatedOriginalAmount: calculatedPrice?.original_amount,
23+
expectedCurrencyCode,
3724
topOffer,
3825
});
39-
const originalAmount = calculatedOriginal ?? offerOriginal;
4026

41-
if (typeof currentAmount !== "number") {
27+
if (!price) {
4228
return {
4329
currentLabel: "Cena na vyžiadanie",
4430
originalLabel: null,
4531
currentAmount: null,
4632
originalAmount: null,
47-
currencyCode,
33+
currencyCode: resolveSupportedCurrencyCode(
34+
expectedCurrencyCode,
35+
DEFAULT_CURRENCY_CODE,
36+
),
4837
};
4938
}
5039

51-
const currentLabel = formatCurrencyAmount(currentAmount, currencyCode);
40+
const currentLabel = formatCurrencyAmount(
41+
price.currentAmount,
42+
price.currencyCode,
43+
);
5244
const originalLabel =
53-
typeof originalAmount === "number" && originalAmount > currentAmount
54-
? formatCurrencyAmount(originalAmount, currencyCode)
45+
typeof price.originalAmount === "number" &&
46+
price.originalAmount > price.currentAmount
47+
? formatCurrencyAmount(price.originalAmount, price.currencyCode)
5548
: null;
5649

5750
return {
5851
currentLabel,
5952
originalLabel,
60-
currentAmount,
61-
originalAmount,
62-
currencyCode: currencyCode.toUpperCase(),
53+
currentAmount: price.currentAmount,
54+
originalAmount: price.originalAmount,
55+
currencyCode: price.currencyCode,
6356
};
6457
};
6558

@@ -75,7 +68,7 @@ export const resolveDiscountLabel = (
7568
}
7669

7770
const discountAmount = price.originalAmount - price.currentAmount;
78-
return `${formatCurrencyAmount(discountAmount, price.currencyCode)}`;
71+
return `-${formatCurrencyAmount(discountAmount, price.currencyCode)}`;
7972
};
8073

8174
export const getProductPriceLabel = (

0 commit comments

Comments
 (0)