rework calcCart

use swr for discounts datafetching
remove discounts store
refetch discount on promocode apply
This commit is contained in:
nflnkr 2024-03-26 17:49:35 +03:00
parent 6607919624
commit 10e2022035
16 changed files with 240 additions and 202 deletions

@ -15,7 +15,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.70", "@frontend/kitui": "^1.0.71",
"@mui/icons-material": "^5.10.14", "@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",

@ -1,24 +1,37 @@
import { makeRequest } from "@frontend/kitui" import { makeRequest } from "@frontend/kitui";
import type { GetDiscountsResponse } from "@root/model/discount";
import { parseAxiosError } from "@root/utils/parse-error";
import { enqueueSnackbar } from "notistack";
import useSWR from "swr";
import { parseAxiosError } from "@root/utils/parse-error"
import type { GetDiscountsResponse } from "@root/model/discount" const apiUrl = process.env.REACT_APP_DOMAIN + "/price";
const apiUrl = process.env.REACT_APP_DOMAIN + "/price" export async function getDiscounts() {
try {
const discountsResponse = await makeRequest<never, GetDiscountsResponse>({
url: apiUrl + "/discounts",
method: "get",
useToken: true,
});
export async function getDiscounts(signal: AbortSignal | undefined): Promise<[GetDiscountsResponse | null, string?]> { return discountsResponse.Discounts;
try { } catch (nativeError) {
const discountsResponse = await makeRequest<never, GetDiscountsResponse>({ const [error] = parseAxiosError(nativeError);
url: apiUrl + "/discounts",
method: "get",
useToken: true,
signal,
})
return [discountsResponse] throw new Error(`Ошибка получения списка скидок. ${error}`);
} catch (nativeError) { }
const [error] = parseAxiosError(nativeError) }
return [null, `Ошибка получения списка скидок. ${error}`] export function useDiscounts() {
} const { data } = useSWR("discounts", getDiscounts, {
keepPreviousData: true,
onError: (error) => {
if (!(error instanceof Error)) return;
enqueueSnackbar(error.message, { variant: "error" });
}
});
return data;
} }

@ -19,8 +19,6 @@ import { enqueueSnackbar } from "notistack";
import { updateTariffs } from "@root/stores/tariffs"; import { updateTariffs } from "@root/stores/tariffs";
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs"; import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
import { setCustomTariffs } from "@root/stores/customTariffs"; import { setCustomTariffs } from "@root/stores/customTariffs";
import { useDiscounts } from "@root/utils/hooks/useDiscounts";
import { setDiscounts } from "@root/stores/discounts";
import { setPrivileges } from "@root/stores/privileges"; import { setPrivileges } from "@root/stores/privileges";
import { useHistoryData } from "@root/utils/hooks/useHistoryData"; import { useHistoryData } from "@root/utils/hooks/useHistoryData";
import { useSSETab } from "@root/utils/hooks/useSSETab"; import { useSSETab } from "@root/utils/hooks/useSSETab";
@ -79,14 +77,6 @@ export default function ProtectedLayout() {
}, },
}); });
useDiscounts({
onNewDiscounts: setDiscounts,
onError: (error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
},
});
usePrivilegeFetcher({ usePrivilegeFetcher({
onSuccess: setPrivileges, onSuccess: setPrivileges,
onError: (error) => { onError: (error) => {

@ -29,6 +29,7 @@ import CollapsiblePromocodeField from "./CollapsiblePromocodeField";
import PaymentMethodCard from "./PaymentMethodCard"; import PaymentMethodCard from "./PaymentMethodCard";
import { SorryModal } from "./SorryModal"; import { SorryModal } from "./SorryModal";
import { WarnModal } from "./WarnModal"; import { WarnModal } from "./WarnModal";
import { mutate } from "swr";
type PaymentMethod = { type PaymentMethod = {
label: string; label: string;
@ -171,6 +172,7 @@ export default function Payment() {
activatePromocode(promocodeField).then(response => { activatePromocode(promocodeField).then(response => {
enqueueSnackbar(response); enqueueSnackbar(response);
mutate("discounts");
}).catch(error => { }).catch(error => {
enqueueSnackbar(error.message); enqueueSnackbar(error.message);
}); });

@ -1,5 +1,6 @@
import { CustomPrivilege, Privilege, useThrottle } from "@frontend/kitui"; import { CustomPrivilege, useThrottle } from "@frontend/kitui";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useDiscounts } from "@root/api/price";
import { CustomSlider } from "@root/components/CustomSlider"; import { CustomSlider } from "@root/components/CustomSlider";
import NumberInputWithUnitAdornment from "@root/components/NumberInputWithUnitAdornment"; import NumberInputWithUnitAdornment from "@root/components/NumberInputWithUnitAdornment";
import CalendarIcon from "@root/components/icons/CalendarIcon"; import CalendarIcon from "@root/components/icons/CalendarIcon";
@ -30,6 +31,7 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
const userValue = useCustomTariffsStore((state) => state.userValuesMap[privilege.serviceKey]?.[privilege._id]) ?? sliderSettingsByType[privilege.value]?.min; const userValue = useCustomTariffsStore((state) => state.userValuesMap[privilege.serviceKey]?.[privilege._id]) ?? sliderSettingsByType[privilege.value]?.min;
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? sliderSettingsByType[privilege.value]?.min; const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? sliderSettingsByType[privilege.value]?.min;
const cartTariffs = useCartTariffs(); const cartTariffs = useCartTariffs();
const discounts = useDiscounts();
const userId = useUserStore(state => state.user?._id) ?? ""; const userId = useUserStore(state => state.user?._id) ?? "";
const [value, setValue] = useState<number>(userValue); const [value, setValue] = useState<number>(userValue);
const throttledValue = useThrottle(value, 200); const throttledValue = useThrottle(value, 200);
@ -38,6 +40,7 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
function setStoreValue() { function setStoreValue() {
setCustomTariffsUserValue( setCustomTariffsUserValue(
cartTariffs ?? [], cartTariffs ?? [],
discounts ?? [],
privilege.serviceKey, privilege.serviceKey,
privilege._id, privilege._id,
throttledValue, throttledValue,
@ -45,7 +48,7 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
userId, userId,
); );
}, },
[cartTariffs, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue, userId] [cartTariffs, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue, userId]
); );
function handleSliderChange(measurement: PrivilegeName) { function handleSliderChange(measurement: PrivilegeName) {

@ -3,7 +3,6 @@ import { Tariff, getMessageFromFetchError } from "@frontend/kitui";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import NumberIcon from "@root/components/NumberIcon"; import NumberIcon from "@root/components/NumberIcon";
import { useDiscountStore } from "@root/stores/discounts";
import { useTariffStore } from "@root/stores/tariffs"; import { useTariffStore } from "@root/stores/tariffs";
import { addTariffToCart, useUserStore } from "@root/stores/user"; import { addTariffToCart, useUserStore } from "@root/stores/user";
import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices"; import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
@ -17,6 +16,7 @@ import { withErrorBoundary } from "react-error-boundary";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import FreeTariffCard from "./FreeTariffCard"; import FreeTariffCard from "./FreeTariffCard";
import TariffCard from "./TariffCard"; import TariffCard from "./TariffCard";
import { useDiscounts } from "@root/api/price";
const subPages = ["Опросник", /*"Шаблонизатор",*/ /*"Сокращатель ссылок"*/]; const subPages = ["Опросник", /*"Шаблонизатор",*/ /*"Сокращатель ссылок"*/];
@ -35,7 +35,7 @@ function TariffPage() {
const location = useLocation(); const location = useLocation();
const tariffs = useTariffStore((state) => state.tariffs); const tariffs = useTariffStore((state) => state.tariffs);
const [selectedItem, setSelectedItem] = useState<number>(0); const [selectedItem, setSelectedItem] = useState<number>(0);
const discounts = useDiscountStore((state) => state.discounts); const discounts = useDiscounts();
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0; const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0;
const userId = useUserStore(state => state.user?._id) ?? ""; const userId = useUserStore(state => state.user?._id) ?? "";
const isUserNko = useUserStore((state) => state.userAccount?.status) === "nko"; const isUserNko = useUserStore((state) => state.userAccount?.status) === "nko";
@ -75,7 +75,7 @@ function TariffPage() {
.map((tariff, index) => { .map((tariff, index) => {
const { priceBeforeDiscounts, priceAfterDiscounts } = calcIndividualTariffPrices( const { priceBeforeDiscounts, priceAfterDiscounts } = calcIndividualTariffPrices(
tariff, tariff,
discounts, discounts ?? [],
purchasesAmount, purchasesAmount,
currentTariffs ?? [], currentTariffs ?? [],
isUserNko, isUserNko,

@ -1,4 +1,4 @@
import { CustomPrivilegeWithAmount, Tariff } from "@frontend/kitui"; import { CustomPrivilegeWithAmount, Discount, Tariff } from "@frontend/kitui";
import { createTariff } from "@root/api/tariff"; import { createTariff } from "@root/api/tariff";
import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs"; import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs";
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege"; import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
@ -6,7 +6,6 @@ import { calcCustomTariffPrice } from "@root/utils/calcCart/calcCustomTariffPric
import { produce } from "immer"; import { produce } from "immer";
import { create } from "zustand"; import { create } from "zustand";
import { devtools, persist } from "zustand/middleware"; import { devtools, persist } from "zustand/middleware";
import { useDiscountStore } from "./discounts";
import { useUserStore } from "./user"; import { useUserStore } from "./user";
@ -48,6 +47,7 @@ export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => us
export const setCustomTariffsUserValue = ( export const setCustomTariffsUserValue = (
cartTariffs: Tariff[], cartTariffs: Tariff[],
discounts: Discount[],
serviceKey: string, serviceKey: string,
privilegeId: string, privilegeId: string,
value: number, value: number,
@ -58,7 +58,6 @@ export const setCustomTariffsUserValue = (
state.userValuesMap[serviceKey] ??= {}; state.userValuesMap[serviceKey] ??= {};
state.userValuesMap[serviceKey][privilegeId] = value; state.userValuesMap[serviceKey][privilegeId] = value;
const isUserNko = useUserStore.getState().userAccount?.status === "nko"; const isUserNko = useUserStore.getState().userAccount?.status === "nko";
const discounts = useDiscountStore.getState().discounts;
const { priceBeforeDiscounts, priceAfterDiscounts } = calcCustomTariffPrice( const { priceBeforeDiscounts, priceAfterDiscounts } = calcCustomTariffPrice(
state.userValuesMap[serviceKey], state.userValuesMap[serviceKey],

@ -1,22 +0,0 @@
import { Discount } from "@frontend/kitui"
import { create } from "zustand"
import { devtools } from "zustand/middleware"
interface DiscountStore {
discounts: Discount[];
}
export const useDiscountStore = create<DiscountStore>()(
devtools(
(set, get) => ({
discounts: []
}),
{
name: "Discounts",
enabled: process.env.NODE_ENV === "development",
}
)
)
export const setDiscounts = (discounts: DiscountStore["discounts"]) => useDiscountStore.setState({ discounts })

@ -13,7 +13,7 @@ describe("Cart calculation", () => {
const tariffs = testTariffs.filter((_, index) => (usedTariffsMask[index] === 1)); const tariffs = testTariffs.filter((_, index) => (usedTariffsMask[index] === 1));
const cart = calcCart(tariffs, testDiscounts, 0, isNkoApplied, "someuserid"); const cart = calcCart(tariffs, testDiscounts, 0, "someuserid", isNkoApplied);
expect(cart.priceAfterDiscounts).toBeCloseTo(cartTestResults[i][0]); expect(cart.priceAfterDiscounts).toBeCloseTo(cartTestResults[i][0]);
}); });

@ -4,30 +4,27 @@ import {
PrivilegeCartData, PrivilegeCartData,
Tariff, Tariff,
TariffCartData, TariffCartData,
findCartDiscount,
findDiscountFactor, findDiscountFactor,
} from "@frontend/kitui";
import {
findCartDiscount,
findLoyaltyDiscount, findLoyaltyDiscount,
findNkoDiscount, findNkoDiscount,
findPrivilegeDiscount, findPrivilegeDiscount,
findServiceDiscount, findServiceDiscount,
} from "@frontend/kitui"; } from "./utils";
export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number, isUserNko?: boolean, userId: string = ""): CartData { export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number, userId: string, isUserNko?: boolean): CartData {
const cartData: CartData = { const cartData: CartData = {
services: [], services: [],
priceBeforeDiscounts: 0, priceBeforeDiscounts: 0,
priceAfterDiscounts: 0, priceAfterDiscounts: 0,
appliedCartPurchasesDiscount: null,
appliedLoyaltyDiscount: null,
allAppliedDiscounts: [], allAppliedDiscounts: [],
appliedDiscountsByPrivilegeId: new Map(),
}; };
const privilegeAmountById = new Map<string, number>(); const privilegeAmountById = new Map<string, number>();
const servicePriceByKey = new Map<string, number>(); const servicePriceByKey = new Map<string, number>();
const allAppliedDiscounts = new Set<Discount>(); const allAppliedDiscounts = new Set<Discount>();
// Формируем корзину
tariffs.forEach(tariff => { tariffs.forEach(tariff => {
if (tariff.privileges === undefined) return; if (tariff.privileges === undefined) return;
if ( if (
@ -43,7 +40,6 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
serviceKey: tariff.isCustom ? "custom" : tariff.privileges[0]?.serviceKey, serviceKey: tariff.isCustom ? "custom" : tariff.privileges[0]?.serviceKey,
tariffs: [], tariffs: [],
price: 0, price: 0,
appliedServiceDiscount: null,
}; };
cartData.services.push(serviceData); cartData.services.push(serviceData);
} }
@ -59,7 +55,8 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
tariff.privileges.forEach(privilege => { tariff.privileges.forEach(privilege => {
let privilegePrice = privilege.amount * privilege.price; let privilegePrice = privilege.amount * privilege.price;
if (tariff.price) privilegePrice = tariff.price; if (!tariff.price) tariffCartData.price += privilegePrice;
else privilegePrice = tariff.price;
const privilegeCartData: PrivilegeCartData = { const privilegeCartData: PrivilegeCartData = {
serviceKey: privilege.serviceKey, serviceKey: privilege.serviceKey,
@ -71,57 +68,63 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
privilegeAmountById.set( privilegeAmountById.set(
privilege.privilegeId, privilege.privilegeId,
privilege.amount + (privilegeAmountById.get(privilege.privilegeId) || 0) privilege.amount + (privilegeAmountById.get(privilege.privilegeId) ?? 0)
); );
servicePriceByKey.set( servicePriceByKey.set(
privilege.serviceKey, privilege.serviceKey,
privilegePrice + (servicePriceByKey.get(privilege.serviceKey) || 0) privilegePrice + (servicePriceByKey.get(privilege.serviceKey) ?? 0)
); );
cartData.appliedDiscountsByPrivilegeId.set(privilege.privilegeId, new Set());
tariffCartData.privileges.push(privilegeCartData); tariffCartData.privileges.push(privilegeCartData);
}); });
serviceData.price += tariffCartData.price;
}); });
cartData.priceBeforeDiscounts = Array.from(servicePriceByKey.values()).reduce((a, b) => a + b, 0); cartData.priceBeforeDiscounts = Array.from(servicePriceByKey.values()).reduce((a, b) => a + b, 0);
cartData.priceAfterDiscounts = cartData.priceBeforeDiscounts;
const nkoDiscount = findNkoDiscount(discounts); const nkoDiscount = findNkoDiscount(discounts);
if (isUserNko && nkoDiscount) { if (isUserNko && nkoDiscount) {
cartData.priceAfterDiscounts *= nkoDiscount.Target.Factor; cartData.allAppliedDiscounts = [nkoDiscount];
allAppliedDiscounts.add(nkoDiscount);
cartData.services.forEach(service => { cartData.services.forEach(service => {
service.tariffs.forEach(tariff => { service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => { tariff.privileges.forEach(privilege => {
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId); const discountAmount = privilege.price * (1 - findDiscountFactor(nkoDiscount));
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
privilegeAppliedDiscounts.add(nkoDiscount); privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
}); });
}); });
}); });
applyDiscountsToCart(cartData);
cartData.allAppliedDiscounts = Array.from(allAppliedDiscounts);
return cartData; return cartData;
} }
// Ищем и собираем скидки в appliedDiscountsByPrivilegeId
cartData.services.forEach(service => { cartData.services.forEach(service => {
service.tariffs.forEach(tariff => { service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => { tariff.privileges.forEach(privilege => {
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilegeAmountById.get(privilege.privilegeId) || 0, discounts, userId); const privilegeTotalAmount = privilegeAmountById.get(privilege.privilegeId) ?? 0;
if (!privilegeDiscount) return;
allAppliedDiscounts.add(privilegeDiscount); const discount = findPrivilegeDiscount(privilege.privilegeId, privilegeTotalAmount, discounts, userId);
if (!discount) return;
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId); allAppliedDiscounts.add(discount);
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
privilegeAppliedDiscounts.add(privilegeDiscount); const discountAmount = privilege.price * (1 - findDiscountFactor(discount));
privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
const serviceTotalPrice = servicePriceByKey.get(privilege.serviceKey);
if (!serviceTotalPrice) throw new Error(`Service key ${privilege.serviceKey} not found in servicePriceByKey`);
servicePriceByKey.set(privilege.serviceKey, serviceTotalPrice - discountAmount);
}); });
}); });
}); });
@ -129,96 +132,67 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
cartData.services.forEach(service => { cartData.services.forEach(service => {
service.tariffs.map(tariff => { service.tariffs.map(tariff => {
tariff.privileges.forEach(privilege => { tariff.privileges.forEach(privilege => {
const servicePrice = servicePriceByKey.get(privilege.serviceKey); const serviceTotalPrice = servicePriceByKey.get(privilege.serviceKey);
if (!servicePrice) return; if (!serviceTotalPrice) throw new Error(`Service key ${privilege.serviceKey} not found in servicePriceByKey`);
const serviceDiscount = findServiceDiscount(privilege.serviceKey, servicePrice, discounts, userId); const discount = findServiceDiscount(privilege.serviceKey, serviceTotalPrice, discounts, userId);
if (!serviceDiscount) return; if (!discount) return;
allAppliedDiscounts.add(serviceDiscount); allAppliedDiscounts.add(discount);
service.appliedServiceDiscount = serviceDiscount;
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId); const discountAmount = privilege.price * (1 - findDiscountFactor(discount));
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
privilegeAppliedDiscounts.add(serviceDiscount); privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
}); });
}); });
}); });
const intermediateCartData = structuredClone(cartData); const userDiscount = discounts.find(discount => discount.Condition.User === userId);
applyDiscountsToCart(intermediateCartData);
const intermediateCartPriceAfterDiscounts = intermediateCartData.priceAfterDiscounts; const cartDiscount = findCartDiscount(cartData.priceAfterDiscounts, discounts);
const cartDiscount = findCartDiscount(intermediateCartPriceAfterDiscounts, discounts);
if (cartDiscount) { if (cartDiscount) {
cartData.services.forEach(service => { cartData.services.forEach(service => {
if (service.serviceKey === userDiscount?.Condition.Group) return;
service.tariffs.forEach(tariff => { service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => { tariff.privileges.forEach(privilege => {
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId); allAppliedDiscounts.add(cartDiscount);
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
if (!Array.from(privilegeAppliedDiscounts)[0]?.Condition.User) privilegeAppliedDiscounts.add(cartDiscount); const discountAmount = privilege.price * (1 - findDiscountFactor(cartDiscount));
privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
}); });
}); });
}); });
allAppliedDiscounts.add(cartDiscount);
cartData.appliedCartPurchasesDiscount = cartDiscount;
} }
const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts); const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
if (loyalDiscount) { if (loyalDiscount) {
cartData.services.forEach(service => { cartData.services.forEach(service => {
if (service.serviceKey === userDiscount?.Condition.Group) return;
service.tariffs.forEach(tariff => { service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => { tariff.privileges.forEach(privilege => {
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId); allAppliedDiscounts.add(loyalDiscount);
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
if (!Array.from(privilegeAppliedDiscounts)[0]?.Condition.User) privilegeAppliedDiscounts.add(loyalDiscount); const discountAmount = privilege.price * (1 - findDiscountFactor(loyalDiscount));
privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
}); });
}); });
}); });
allAppliedDiscounts.add(loyalDiscount);
cartData.appliedLoyaltyDiscount = loyalDiscount;
} }
// Применяем скидки
applyDiscountsToCart(cartData);
cartData.allAppliedDiscounts = Array.from(allAppliedDiscounts); cartData.allAppliedDiscounts = Array.from(allAppliedDiscounts);
return Object.freeze(cartData); return cartData;
}
function applyDiscountsToCart(cartData: CartData) {
cartData.services.forEach(service => {
let servicePrice = 0;
service.tariffs.forEach(tariff => {
let privilegePriceSum = 0;
let tariffDiscountFactor = 1;
tariff.privileges.forEach(privilege => {
const discounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId) ?? [];
const discountsFactor = Array.from(discounts).reduce((factor, discount) => factor * findDiscountFactor(discount), 1);
privilege.price *= discountsFactor;
tariffDiscountFactor *= discountsFactor;
privilegePriceSum += privilege.price;
});
if (tariff.price) tariff.price *= tariffDiscountFactor;
else tariff.price = privilegePriceSum;
servicePrice += tariff.price;
});
service.price = servicePrice;
cartData.priceAfterDiscounts += servicePrice;
});
} }

@ -38,7 +38,7 @@ export function calcCustomTariffPrice(
privileges: privileges, privileges: privileges,
}; };
const cart = calcCart([...cartTariffs, customTariff], discounts, purchasesAmount, isUserNko, userId); const cart = calcCart([...cartTariffs, customTariff], discounts, purchasesAmount, userId, isUserNko);
const customService = cart.services.flatMap(service => service.tariffs).find(tariff => tariff.id === customTariff._id); const customService = cart.services.flatMap(service => service.tariffs).find(tariff => tariff.id === customTariff._id);
if (!customService) throw new Error("Custom service not found in cart"); if (!customService) throw new Error("Custom service not found in cart");

119
src/utils/calcCart/utils.ts Normal file

@ -0,0 +1,119 @@
import { Discount } from "@frontend/kitui";
export function findNkoDiscount(discounts: Discount[]): Discount | null {
const applicableDiscounts = discounts.filter(discount => discount.Condition.UserType === "nko");
if (!applicableDiscounts.length) return null;
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
return Number(current.Condition.CartPurchasesAmount) > Number(prev.Condition.CartPurchasesAmount) ? current : prev;
});
return maxValueDiscount;
}
export function findPrivilegeDiscount(
privilegeId: string,
privilegeAmount: number,
discounts: Discount[],
userId: string,
): Discount | null {
const applicableDiscounts = discounts.filter(discount => {
return (
discount.Layer === 1
&& privilegeId === discount.Condition.Product
&& privilegeAmount >= Number(discount.Condition.Term)
&& (discount.Condition.User === "" || discount.Condition.User === userId)
);
});
if (!applicableDiscounts.length) return null;
let maxValueDiscount: Discount = applicableDiscounts[0];
for (const discount of applicableDiscounts) {
if (discount.Condition.User !== "" && discount.Condition.User === userId) {
maxValueDiscount = discount;
break;
}
if (Number(discount.Condition.Term) > Number(maxValueDiscount.Condition.Term)) {
maxValueDiscount = discount;
}
}
return maxValueDiscount;
}
export function findServiceDiscount(
serviceKey: string,
currentPrice: number,
discounts: Discount[],
userId: string,
): Discount | null {
const applicableDiscounts = discounts.filter(discount => {
return (
discount.Layer === 2
&& serviceKey === discount.Condition.Group
&& currentPrice >= Number(discount.Condition.PriceFrom)
&& (discount.Condition.User === "" || discount.Condition.User === userId)
);
});
if (!applicableDiscounts.length) return null;
let maxValueDiscount: Discount = applicableDiscounts[0];
for (const discount of applicableDiscounts) {
if (discount.Condition.User !== "" && discount.Condition.User === userId) {
maxValueDiscount = discount;
break;
}
if (Number(discount.Condition.PriceFrom) > Number(maxValueDiscount.Condition.PriceFrom)) {
maxValueDiscount = discount;
}
}
return maxValueDiscount;
}
export function findCartDiscount(
cartPurchasesAmount: number,
discounts: Discount[],
): Discount | null {
const applicableDiscounts = discounts.filter(discount => {
return (
discount.Layer === 3
&& cartPurchasesAmount >= Number(discount.Condition.CartPurchasesAmount)
);
});
if (!applicableDiscounts.length) return null;
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
return Number(current.Condition.CartPurchasesAmount) > Number(prev.Condition.CartPurchasesAmount) ? current : prev;
});
return maxValueDiscount;
}
export function findLoyaltyDiscount(
purchasesAmount: number,
discounts: Discount[],
): Discount | null {
const applicableDiscounts = discounts.filter(discount => {
return (
discount.Layer === 4
&& discount.Condition.UserType !== "nko"
&& purchasesAmount >= Number(discount.Condition.PurchasesAmount)
);
});
if (!applicableDiscounts.length) return null;
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
return Number(current.Condition.PurchasesAmount) > Number(prev.Condition.PurchasesAmount) ? current : prev;
});
return maxValueDiscount;
}

@ -17,7 +17,7 @@ export function calcIndividualTariffPrices(
0 0
); );
const cart = calcCart([...currentTariffs, targetTariff], discounts, purchasesAmount, isUserNko, userId); const cart = calcCart([...currentTariffs, targetTariff], discounts, purchasesAmount, userId, isUserNko);
const tariffCartData = cart.services.flatMap(service => service.tariffs).find(tariff => tariff.id === targetTariff._id); const tariffCartData = cart.services.flatMap(service => service.tariffs).find(tariff => tariff.id === targetTariff._id);
if (!tariffCartData) throw new Error(`Target tariff ${targetTariff._id} not found in cart`); if (!tariffCartData) throw new Error(`Target tariff ${targetTariff._id} not found in cart`);

@ -1,23 +1,21 @@
import { useDiscountStore } from "@root/stores/discounts"; import { CartData } from "@frontend/kitui";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import { useDebugValue, useMemo } from "react"; import { useMemo } from "react";
import { calcCart } from "../calcCart/calcCart"; import { calcCart } from "../calcCart/calcCart";
import { useCartTariffs } from "./useCartTariffs"; import { useCartTariffs } from "./useCartTariffs";
import { CartData } from "@frontend/kitui"; import { useDiscounts } from "@root/api/price";
export function useCart(): CartData { export function useCart(): CartData {
const cartTariffs = useCartTariffs(); const cartTariffs = useCartTariffs();
const discounts = useDiscountStore((state) => state.discounts); const discounts = useDiscounts();
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0; const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0;
const isUserNko = useUserStore(state => state.userAccount?.status) === "nko"; const isUserNko = useUserStore(state => state.userAccount?.status) === "nko";
const userId = useUserStore(state => state.user?._id) ?? ""; const userId = useUserStore(state => state.user?._id) ?? "";
const cart = useMemo(() => { const cart = useMemo(() => {
return calcCart(cartTariffs ?? [], discounts, purchasesAmount, isUserNko, userId); return calcCart(cartTariffs ?? [], discounts ?? [], purchasesAmount, userId, isUserNko);
}, [cartTariffs, discounts, purchasesAmount, isUserNko, userId]); }, [cartTariffs, discounts, purchasesAmount, userId, isUserNko]);
useDebugValue(cart);
return cart; return cart;
} }

@ -1,38 +0,0 @@
import { useEffect, useLayoutEffect, useRef } from "react"
import { Discount, devlog } from "@frontend/kitui"
import { getDiscounts } from "@root/api/price"
export function useDiscounts({
onNewDiscounts,
onError,
}: {
url?: string;
onNewDiscounts: (response: Discount[]) => void;
onError: (error: Error) => void;
}) {
const onNewTariffsRef = useRef(onNewDiscounts)
const onErrorRef = useRef(onError)
useLayoutEffect(() => {
onNewTariffsRef.current = onNewDiscounts
onErrorRef.current = onError
}, [onError, onNewDiscounts])
useEffect(() => {
const controller = new AbortController()
getDiscounts(controller.signal)
.then(([discounts]) => {
if (discounts) {
onNewTariffsRef.current(discounts.Discounts)
}
})
.catch((error) => {
devlog("Error fetching tariffs", error)
onErrorRef.current(error)
})
return () => controller.abort()
}, [])
}

@ -1607,10 +1607,10 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
"@frontend/kitui@^1.0.70": "@frontend/kitui@^1.0.71":
version "1.0.70" version "1.0.71"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.70.tgz#4ff9d4ef51132bbf06d4efda561b95efdaf4e9f0" resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.71.tgz#16d20c163a1574ba98a4720c0deb21eab3e67b75"
integrity sha1-T/nU71ETK78G1O/aVhuV79r06fA= integrity sha1-FtIMFjoVdLqYpHIMDesh6rPme3U=
dependencies: dependencies:
immer "^10.0.2" immer "^10.0.2"
reconnecting-eventsource "^1.6.2" reconnecting-eventsource "^1.6.2"