Merge branch 'dev' into 'main'

fix cart & custom tariff calculations

See merge request frontend/marketplace!13
This commit is contained in:
Mikhail 2023-07-13 20:21:36 +00:00
commit 43695f18a9
27 changed files with 288 additions and 1200 deletions

@ -11,10 +11,10 @@
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.8",
"@frontend/kitui": "^1.0.11",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"axios": "^1.3.4",
"axios": "^1.4.0",
"formik": "^2.2.9",
"immer": "^10.0.2",
"isomorphic-fetch": "^3.0.0",
@ -24,7 +24,7 @@
"react-router-dom": "^6.4.3",
"web-vitals": "^2.1.0",
"yup": "^1.1.1",
"zustand": "^4.3.6"
"zustand": "^4.3.8"
},
"devDependencies": {
"@craco/craco": "^7.1.0",

@ -1,643 +0,0 @@
import { AnyDiscount } from "../model/discount";
export const mockDiscounts: AnyDiscount[] = [
{
_id: "id1",
name: "Лояльность 1",
description:
"постоянная скидка для юзеров, внёсших на проект от 10 000 рублей. Применяется на итоговую сумму, после скидок за сумму в корзине",
conditionType: "purchasesAmount",
layer: 4, // "слой", т.е. этап применения скидки
condition: {
purchasesAmount: 10000,
},
factor: 0.99, // множитель, применяемый к сумме
},
{
_id: "id2",
name: "Лояльность 2",
description:
"постоянная скидка для юзеров, внёсших на проект от 25 000 рублей. Применяется на итоговую сумму, после скидок за сумму в корзине",
conditionType: "purchasesAmount",
layer: 4,
condition: {
purchasesAmount: 25000,
},
factor: 0.98,
},
{
_id: "id3",
name: "Лояльность 3",
description:
"постоянная скидка для юзеров, внёсших на проект от 50 000 рублей. Применяется на итоговую сумму, после скидок за сумму в корзине",
conditionType: "purchasesAmount",
layer: 4,
condition: {
purchasesAmount: 50000,
},
factor: 0.975,
},
{
_id: "id4",
name: "Корзина 1",
description: "Скидка на размер корзины от 5 000 р. Применяется на итоговую сумму, после суммирования корзины",
conditionType: "cartPurchasesAmount",
layer: 3,
condition: {
cartPurchasesAmount: 5000,
},
factor: 0.985,
},
{
_id: "id5",
name: "Корзина 2",
description: "Скидка на размер корзины от 50 000 р. Применяется на итоговую сумму, после суммирования корзины",
conditionType: "cartPurchasesAmount",
layer: 3,
condition: {
cartPurchasesAmount: 50000,
},
factor: 0.965,
},
{
_id: "id6",
name: "Анлим Шабло 1",
description: "Скидка на количество безлимитных дней работы от 30 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
// если в condition условие для поиска, то вот я добавил в него условие для поиска по привилегии
id: "p1", // айди привилегии
value: 30, // скидка работает, если значние больше либо равно этому значению
},
},
target: {
products: [
{
privilegeId: "p1", // не знаю, стоит ли тут оставлять массив products, но на всякий случай оставлю. т.е. скидка, при срабатывании, применяется к этой привилегии в корзине, т.е. умножает её сумму на factor
factor: 0.975,
},
],
},
},
{
_id: "id7",
name: "Анлим Шабло 2",
description: "Скидка на количество безлимитных дней работы от 90 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p1",
value: 90,
},
},
target: {
products: [
{
privilegeId: "p1",
factor: 0.975,
},
],
},
},
{
_id: "id8.rev",
name: "Анлим Шабло 3",
description: "Скидка на количество безлимитных дней работы от 180 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p1",
value: 180,
},
},
target: {
products: [
{
privilegeId: "p1",
factor: 0.93,
},
],
},
},
{
_id: "id8",
name: "Генерации Шабло 1",
description: "Скидка на количество генераций от 100 шт",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "templateCnt",
value: 100,
},
},
target: {
products: [
{
privilegeId: "templateCnt",
factor: 0.995,
},
],
},
},
{
_id: "id9",
name: "Генерации Шабло 2",
description: "Скидка на количество генераций от 350 шт",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "templateCnt",
value: 350,
},
},
target: {
products: [
{
privilegeId: "templateCnt",
factor: 0.98,
},
],
},
},
{
_id: "id10",
name: "Генерации Шабло 3",
description: "Скидка на количество генераций от 500 шт",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "templateCnt",
value: 500,
},
},
target: {
products: [
{
privilegeId: "templateCnt",
factor: 0.945,
},
],
},
},
{
_id: "id11",
name: "Анлим Квиз 1",
description: "Скидка на количество дней безлимитного использования опросника, от 30 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p3",
value: 30,
},
},
target: {
products: [
{
privilegeId: "p3",
factor: 0.97,
},
],
},
},
{
_id: "id12",
name: "Анлим Квиз 2",
description: "Скидка на количество дней безлимитного использования опросника, от 90 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p3",
value: 90,
},
},
target: {
products: [
{
privilegeId: "p3",
factor: 0.93,
},
],
},
},
{
_id: "id13",
name: "Анлим Квиз 3",
description: "Скидка на количество дней безлимитного использования опросника, от 180 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p3",
value: 180,
},
},
target: {
products: [
{
privilegeId: "p3",
factor: 0.85,
},
],
},
},
{
_id: "id14",
name: "Актив квиз 1",
description: "Скидка на количество опросов от 100 шт",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p4",
value: 100,
},
},
target: {
products: [
{
privilegeId: "p4",
factor: 0.98,
},
],
},
},
{
_id: "id15",
name: "Актив квиз 2",
description: "Скидка на количество опросов от 200 шт",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p4",
value: 200,
},
},
target: {
products: [
{
privilegeId: "p4",
factor: 0.96,
},
],
},
},
{
_id: "id16",
name: "Актив квиз 3",
description: "Скидка на количество опросов от 350 шт",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p4",
value: 350,
},
},
target: {
products: [
{
privilegeId: "p4",
factor: 0.9,
},
],
},
},
{
_id: "id17",
name: "Анлим Сокращатель 1",
description: "Скидка на безлимитное использование сокращателя от 30 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p5",
value: 30,
},
},
target: {
products: [
{
privilegeId: "p5",
factor: 0.99,
},
],
},
},
{
_id: "id18",
name: "Анлим Сокращатель 2",
description: "Скидка на безлимитное использование сокращателя от 60 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p5",
value: 60,
},
},
target: {
products: [
{
privilegeId: "p5",
factor: 0.98,
},
],
},
},
{
_id: "id19",
name: "Анлим Сокращатель 3",
description: "Скидка на безлимитное использование сокращателя от 90 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p5",
value: 90,
},
},
target: {
products: [
{
privilegeId: "p5",
factor: 0.97,
},
],
},
},
{
_id: "id20",
name: "АБ Сокращатель 1",
description: "Скидка на количество АБ тестов, от 10 штук",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p6",
value: 10,
},
},
target: {
products: [
{
privilegeId: "p6",
factor: 0.99,
},
],
},
},
{
_id: "id22",
name: "АБ Сокращатель 2",
description: "Скидка на количество АБ тестов, от 25 штук",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p6",
value: 25,
},
},
target: {
products: [
{
privilegeId: "p6",
factor: 0.965,
},
],
},
},
{
_id: "id23",
name: "АБ Сокращатель 3",
description: "Скидка на количество АБ тестов, от 50 штук",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p6",
value: 50,
},
},
target: {
products: [
{
privilegeId: "p6",
factor: 0.935,
},
],
},
},
{
_id: "id24.1",
name: "Стата Сокращатель 1",
description: "Скидка на дни сбора расширенной статистики от 30 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p7",
value: 30,
},
},
target: {
products: [
{
privilegeId: "p7",
factor: 0.935,
},
],
},
},
{
_id: "id24.2",
name: "Стата Сокращатель 2",
description: "Скидка на дни сбора расширенной статистики от 90 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p7",
value: 90,
},
},
target: {
products: [
{
privilegeId: "p7",
factor: 0.875,
},
],
},
},
{
_id: "id25",
name: "Стата Сокращатель 3",
description: "Скидка на дни сбора расширенной статистики от 180 дней",
conditionType: "privilege",
layer: 1,
condition: {
privilege: {
id: "p7",
value: 180,
},
},
target: {
products: [
{
privilegeId: "p7",
factor: 0.83,
},
],
},
},
{
_id: "id26",
name: "Шаблонизатор 1",
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису шаблонизации, от 1000 р",
conditionType: "service",
layer: 2,
condition: {
service: {
id: "templategen",
value: 1000,
},
},
target: {
service: "templategen",
factor: 0.996,
},
},
{
_id: "id27",
name: "Шаблонизатор 2",
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису шаблонизации, от 5000 р",
conditionType: "service",
layer: 2,
condition: {
service: {
id: "templategen",
value: 5000,
},
},
target: {
service: "templategen",
factor: 0.983,
},
},
{
_id: "id28",
name: "Опросник 1",
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису опросника, от 2000 р",
conditionType: "service",
layer: 2,
condition: {
service: {
id: "squiz",
value: 2000,
},
},
target: {
service: "squiz",
factor: 0.983,
},
},
{
_id: "id29",
name: "Опросник 2",
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису опросника, от 6000 р",
conditionType: "service",
layer: 2,
condition: {
service: {
id: "squiz",
value: 6000,
},
},
target: {
service: "squiz",
factor: 0.969,
},
},
{
_id: "id30",
name: "Сокращатель 1",
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису сокращателя, от 500 р",
conditionType: "service",
layer: 2,
condition: {
service: {
id: "dwarfener",
value: 500,
},
},
target: {
service: "dwarfener",
factor: 0.99,
},
},
{
_id: "id31",
name: "Сокращатель 2",
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису сокращателя, от 2500 р",
conditionType: "service",
layer: 2,
condition: {
service: {
id: "dwarfener",
value: 2500,
},
},
target: {
service: "dwarfener",
factor: 0.96,
},
},
{
_id: "id32",
name: "НКО",
description:
"Скидка всем подтвердившим статус НКО. Перекрывает ВСЕ остальные скидки. Если эта скидка срабатывает, остальные можно не вычислять. Т.е. если на уровне 0 находится какая-лидо скидка для выданных условий, просто суммируем корзину и применяем к сумме указанный множитель, после чего прекращаем процесс рассчёта",
conditionType: "userType",
layer: 0,
condition: {
userType: "nko",
},
target: {
IsAllProducts: true,
factor: 0.2,
},
overwhelm: true,
},
{
_id: "id33",
name: "Промокод На АБ тесты",
description: "Скидка, полученная конкретным юзером, после введения промокода. Заменяет собой не промокодовую",
conditionType: "user",
layer: 1,
condition: {
coupon: "ABCD", // на мой вкус, стоит при активации промокода создавать скидку, привязанную к юзеру по айдишнику, и удалять после использования. т.е. кондишн не по coupon, а по
user: "buddy",
},
target: {
products: [
{
privilegeId: "p6",
factor: 0.5,
},
],
},
overwhelm: true,
},
];

@ -1,31 +0,0 @@
import { useEffect, useState } from "react";
import { makeRequest } from "@frontend/kitui";
import {useSetDiscountsStore} from "@utils/hooks/useSetDiscountsStore";
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
export const GetDiscounts = () => {
const [discounts, setDiscounts] = useState<any[]>([]);
useEffect(() => {
const get = async () => {
// @ts-ignore
const data = await makeRequest<any>({
url: apiUrl + "/price/discounts",
method: "get",
useToken: true,
})
return data;
};
get()
.then(r => {
if (r) {
// @ts-ignore
setDiscounts(r.Discounts)
}
})
}, []);
return { discounts };
};

@ -10,6 +10,7 @@ import { useNavigate } from "react-router";
import { useCart } from "@root/utils/hooks/useCart";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { closeCartDrawer, openCartDrawer, useCartStore } from "@root/stores/cart";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
export default function Drawers() {
@ -18,6 +19,14 @@ export default function Drawers() {
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isDrawerOpen = useCartStore(state => state.isDrawerOpen);
const cart = useCart();
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice;
const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice;
return (
<IconButton sx={{ p: 0 }}>
@ -151,7 +160,7 @@ export default function Drawers() {
order: upMd ? 1 : 2,
}}
>
{currencyFormatter.format(cart.priceBeforeDiscounts / 100)}
{currencyFormatter.format(totalPriceBeforeDiscounts / 100)}
</Typography>
<Typography
variant="p1"
@ -162,7 +171,7 @@ export default function Drawers() {
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(cart.priceAfterDiscounts / 100)}
{currencyFormatter.format(totalPriceAfterDiscounts / 100)}
</Typography>
</Box>
<CustomButton

@ -1,26 +1,31 @@
import { Alert, Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "./CustomButton";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { patchCurrency, payCart } from "@root/api/cart";
import { changeUserCurrency, setUserAccount } from "@root/stores/user";
import { payCart } from "@root/api/cart";
import { setUserAccount } from "@root/stores/user";
import { isAxiosError } from "axios";
import { useState } from "react";
import { getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart";
import { useCartStore } from "@root/stores/cart";
import { useNavigate } from "react-router-dom";
import { useCart } from "@root/utils/hooks/useCart";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
interface Props {
price: number;
priceWithDiscounts: number;
}
export default function TotalPrice({ price, priceWithDiscounts }: Props) {
export default function TotalPrice() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount);
const navigate = useNavigate();
const { priceBeforeDiscounts, priceAfterDiscounts } = useCart();
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
const totalPriceBeforeDiscounts = priceBeforeDiscounts + basePrice;
const totalPriceAfterDiscounts = priceAfterDiscounts + discountedPrice;
function handlePayClick() {
// payCart().then(result => {
@ -42,24 +47,20 @@ export default function TotalPrice({ price, priceWithDiscounts }: Props) {
}
return (
<Box
sx={{
<Box sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
mt: upMd ? "80px" : "70px",
pt: upMd ? "30px" : undefined,
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}>
<Box sx={{
width: upMd ? "68.5%" : undefined,
pr: upMd ? "15%" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
mt: upMd ? "80px" : "70px",
pt: upMd ? "30px" : undefined,
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
pr: upMd ? "15%" : undefined,
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
}}
>
flexWrap: "wrap",
flexDirection: "column",
}}>
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
Итоговая цена
</Typography>
@ -68,34 +69,27 @@ export default function TotalPrice({ price, priceWithDiscounts }: Props) {
это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель
</Typography>
</Box>
<Box
sx={{
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
mb: "15px",
gap: "15px",
}}
>
<Box sx={{
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}>
<Box sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
mb: "15px",
gap: "15px",
}}>
<Typography
color={theme.palette.orange.main}
sx={{
textDecoration: "line-through",
order: upMd ? 1 : 2,
}}
variant="oldPrice"
sx={{ order: upMd ? 1 : 2 }}
>
{currencyFormatter.format(price / 100)}
{currencyFormatter.format(totalPriceBeforeDiscounts / 100)}
</Typography>
<Typography
variant="p1"
variant="price"
sx={{
fontWeight: 500,
fontSize: "26px",
@ -103,7 +97,7 @@ export default function TotalPrice({ price, priceWithDiscounts }: Props) {
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(priceWithDiscounts / 100)}
{currencyFormatter.format(totalPriceAfterDiscounts / 100)}
</Typography>
</Box>
{notEnoughMoneyAmount > 0 &&

@ -25,16 +25,17 @@ import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
import { useUser } from "./utils/hooks/useUser";
import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
import { useUserAccount } from "./utils/hooks/useUserAccount";
import { useSetDiscountsStore } from "./utils/hooks/useSetDiscountsStore";
import { setCustomTariffs} from "@root/stores/customTariffs";
import { setCustomTariffs } from "@root/stores/customTariffs";
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
import { useDiscounts } from "./utils/hooks/useDiscounts";
import { setDiscounts } from "./stores/discounts";
const App = () => {
const location = useLocation();
const userId = useUserStore(state => state.userId);
const navigate = useNavigate();
useCustomTariffs({
url: "https://admin.pena.digital/strator/privilege/service",
onNewUser: setCustomTariffs,
@ -44,8 +45,6 @@ const App = () => {
}
});
useSetDiscountsStore();
useUser({
url: `https://hub.pena.digital/user/${userId}`,
userId,
@ -75,6 +74,14 @@ const App = () => {
}
});
useDiscounts({
onNewDiscounts: setDiscounts,
onError: error => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
}
});
if (location.state?.redirectTo) return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
return (

@ -1,11 +1,13 @@
import { PrivilegeWithAmount, PrivilegeWithoutPrice } from "./privilege";
type ServiceKey = string;
export type CustomTariffUserValues = Record<string, number>;
export type CustomTariffUserValuesMap = Record<string, CustomTariffUserValues>;
export type CustomTariffUserValuesMap = Record<ServiceKey, CustomTariffUserValues>;
export type SummaryPriceMap = Record<string, number>;
export type ServiceKeyToPriceMap = Record<ServiceKey, number>;
export interface CustomTariff {
name: string;
@ -17,4 +19,4 @@ export interface CustomTariff {
createdAt?: string;
}
export type CreateTariffBody = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; };
export type CreateTariffBody = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; };

@ -1,102 +1,6 @@
export interface DiscountBase {
_id: string;
name: string;
description: string;
/** Этап применения скидки */
layer?: number;
disabled?: boolean;
}
import { Discount } from "@frontend/kitui";
export interface PurchasesAmountDiscount extends DiscountBase {
conditionType: "purchasesAmount";
condition: {
purchasesAmount: number;
};
/** Множитель, на который умножается сумма при применении скидки */
factor: number;
}
export interface CartPurchasesAmountDiscount extends DiscountBase {
conditionType: "cartPurchasesAmount";
condition: {
cartPurchasesAmount: number;
};
/** Множитель, на который умножается сумма при применении скидки */
factor: number;
}
export interface PrivilegeDiscount extends DiscountBase {
conditionType: "privilege";
condition: {
privilege: {
id: string;
/** Скидка применяется, если значение больше или равно этому значению */
value: number;
};
};
target: {
products: Array<{
privilegeId: string;
/** Множитель, на который умножается сумма при применении скидки */
factor: number;
}>;
};
}
export interface ServiceDiscount extends DiscountBase {
conditionType: "service";
condition: {
service: {
id: string;
/** Скидка применяется, если значение больше или равно этому значению */
value: number;
};
};
target: {
service: string;
/** Множитель, на который умножается сумма при применении скидки */
factor: number;
};
}
export interface UserTypeDiscount extends DiscountBase {
conditionType: "userType";
condition: {
userType: string;
};
target: {
IsAllProducts: boolean;
/** Множитель, на который умножается сумма при применении скидки */
factor: number;
};
overwhelm: boolean;
}
export interface UserDiscount extends DiscountBase {
conditionType: "user";
condition: {
coupon: string;
user: string;
};
target: {
products: Array<{
privilegeId: string;
/** Множитель, на который умножается сумма при применении скидки */
factor: number;
}>;
};
overwhelm: boolean;
}
export type AnyDiscount =
| PurchasesAmountDiscount
| CartPurchasesAmountDiscount
| PrivilegeDiscount
| ServiceDiscount
| UserTypeDiscount
| UserDiscount;
export type DiscountConditionType = AnyDiscount["conditionType"];
export const SERVICE_LIST = [
{
serviceKey: "templategen",
@ -113,43 +17,7 @@ export const SERVICE_LIST = [
] as const;
export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];
export interface BACKEND_Discount {
ID: string;
Name: string;
Layer: number;
Description: string;
Condition: {
Period: {
From: string;
To: string;
};
User: string;
UserType: string;
Coupon: string;
PurchasesAmount: number;
CartPurchasesAmount: number;
Product: string;
Term: string;
Usage: string;
PriceFrom: number;
Group: ServiceType;
};
Target: {
Products: [{
ID: string;
Factor: number;
Overhelm: boolean;
}];
Factor: number;
TargetScope: string;
Group: ServiceType;
Overhelm: boolean;
};
Audit: {
UpdatedAt: string;
CreatedAt: string;
DeletedAt?: string;
Deleted: boolean;
};
Deprecated: boolean;
};
export interface GetDiscountsResponse {
Discounts: Discount[];
}

@ -12,10 +12,10 @@ export interface Privilege {
createdAt?: string;
};
export type PrivilegeMap = Record<string, Privilege[]>;
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;
export type PrivilegeValueType = "шаблон" | "день" | "МБ";
export type PrivilegeWithAmount = Omit<Privilege, "_id"> & { amount: number; };
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">;
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">;

@ -5,7 +5,6 @@ import TotalPrice from "@components/TotalPrice";
import CustomWrapper from "./CustomWrapper";
import ComplexNavText from "@root/components/ComplexNavText";
import { useCart } from "@root/utils/hooks/useCart";
import {GetDiscounts} from "@root/api/discount";
export default function Basket() {
@ -49,7 +48,7 @@ export default function Basket() {
/>
)}
</Box>
<TotalPrice price={cart.priceBeforeDiscounts} priceWithDiscounts={cart.priceAfterDiscounts} />
<TotalPrice />
</SectionWrapper>
);
}

@ -7,9 +7,6 @@ import { cardShadow } from "@root/utils/themes/shadow";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { devlog, getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
import { findServiceDiscount } from "@root/utils/calcTariffPrices";
import { useDiscountStore } from "@root/stores/discounts";
import { useCartStore } from "@root/stores/cart";
interface Props {
@ -20,14 +17,12 @@ interface Props {
export default function CustomTariffCard({ serviceKey, tariffs }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const summaryPrice = useCustomTariffsStore(state => state.summaryPrice);
const summaryPriceBeforeDiscounts = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
const summaryPriceAfterDiscounts = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0;
const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0;
const tariffPrice = summaryPrice[serviceKey] ?? 0;
let discounted = tariffPrice;
const discounts = useDiscountStore.getState().discounts;
const cartservice = useCartStore.getState().cart.services.find(e => e.serviceKey === serviceKey)?.price;
const serviceDiscount = findServiceDiscount(serviceKey, tariffPrice + (cartservice || 0), discounts);
if (serviceDiscount) discounted *= serviceDiscount.target.factor;
async function handleConfirmClick() {
createAndSendTariff(serviceKey).then(result => {
devlog(result);
@ -105,8 +100,8 @@ export default function CustomTariffCard({ serviceKey, tariffs }: Props) {
gap: "20px",
mb: "30px",
}}>
<Typography variant="price">{currencyFormatter.format(discounted / 100)}</Typography>
<Typography variant="oldPrice" pt="3px">{currencyFormatter.format(tariffPrice / 100)}</Typography>
<Typography variant="price">{currencyFormatter.format(priceAfterDiscounts / 100)}</Typography>
<Typography variant="oldPrice" pt="3px">{currencyFormatter.format(priceBeforeDiscounts / 100)}</Typography>
</Box>
<CustomButton
variant="contained"

@ -2,28 +2,17 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "@root/components/CustomButton";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { useUserStore } from "@root/stores/user";
import { useDiscountStore } from "@root/stores/discounts";
import { useCartStore } from "@root/stores/cart";
import { findLoyalDiscount, findCartDiscount } from "@root/utils/calcTariffPrices";
export default function Summary() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const summaryPrice = useCustomTariffsStore(state => state.summaryPrice);
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
const basePrice = Object.values(summaryPrice).reduce((a,e) => a+e);
let discountedPrice = basePrice;
const discounts = useDiscountStore.getState().discounts;
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
const cartDiscount = findCartDiscount(discountedPrice + useCartStore.getState().cart.priceAfterDiscounts, discounts);
if (cartDiscount) discountedPrice *= cartDiscount.factor;
const acc = useUserStore.getState().userAccount;
if (acc) {
const loyalDiscount = findLoyalDiscount(acc!.wallet.purchasesAmount,discounts);
if (loyalDiscount) discountedPrice *= loyalDiscount.factor;
}
return (
<Box sx={{
display: "flex",
@ -63,7 +52,7 @@ export default function Summary() {
variant="oldPrice"
sx={{ order: upMd ? 1 : 2 }}
>
{currencyFormatter.format(basePrice/100)} руб.
{currencyFormatter.format(basePrice / 100)} руб.
</Typography>
<Typography
variant="price"
@ -74,7 +63,7 @@ export default function Summary() {
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(discountedPrice/100)} руб.
{currencyFormatter.format(discountedPrice / 100)} руб.
</Typography>
</Box>
<CustomButton

@ -1,23 +1,21 @@
import { Box, IconButton, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@root/components/ComplexNavText";
import { enqueueSnackbar } from "notistack";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
import Summary from "./Summary";
import { getMessageFromFetchError } from "@frontend/kitui";
import ComplexHeader from "@root/components/ComplexHeader";
import CustomTariffCard from "./CustomTariffCard";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@root/components/TotalPrice";
interface ServiceMap {
[key: string]: string
}
const servicemap: ServiceMap = {"templategen": "Шаблонизатор"};
[key: string]: string;
}
const servicemap: ServiceMap = { "templategen": "Шаблонизатор" };
export default function TariffConstructor() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const customTariffs = useCustomTariffsStore(state => state.customTariffs);
const customTariffs = useCustomTariffsStore(state => state.customTariffsMap);
return (
<SectionWrapper
@ -60,7 +58,7 @@ export default function TariffConstructor() {
</Box>
)}
</Box>
<Summary />
<TotalPrice />
</SectionWrapper>
);
}

@ -4,7 +4,10 @@ import CustomSlider from "@root/components/CustomSlider";
import CalendarIcon from "@root/components/icons/CalendarIcon";
import PieChartIcon from "@root/components/icons/PieChartIcon";
import { Privilege, PrivilegeValueType } from "@root/model/privilege";
import { useCartStore } from "@root/stores/cart";
import { setCustomTariffsUserValue, useCustomTariffsStore } from "@root/stores/customTariffs";
import { useDiscountStore } from "@root/stores/discounts";
import { useUserStore } from "@root/stores/user";
import { formatDateWithDeclention } from "@root/utils/date";
import { getDeclension } from "@root/utils/declension";
import { useEffect, useState } from "react";
@ -32,7 +35,10 @@ interface Props {
export default function TariffPrivilegeSlider({ tariff }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const userValue = useCustomTariffsStore(state => state.userValues[tariff.serviceKey]?.[tariff._id]) ?? 0;
const userValue = useCustomTariffsStore(state => state.userValuesMap[tariff.serviceKey]?.[tariff._id]) ?? 0;
const discounts = useDiscountStore(state => state.discounts);
const currentCartTotal = useCartStore(state => state.cart.priceAfterDiscounts);
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0;
const [value, setValue] = useState<number>(userValue);
const throttledValue = useThrottle(value, 200);
@ -57,8 +63,15 @@ export default function TariffPrivilegeSlider({ tariff }: Props) {
}
useEffect(function setStoreValue() {
setCustomTariffsUserValue(tariff.serviceKey, tariff._id, throttledValue);
}, [tariff._id, tariff.serviceKey, throttledValue]);
setCustomTariffsUserValue(
tariff.serviceKey,
tariff._id,
throttledValue,
discounts,
currentCartTotal,
purchasesAmount
);
}, [currentCartTotal, discounts, purchasesAmount, tariff._id, tariff.serviceKey, throttledValue]);
return (
<Box>
@ -96,4 +109,4 @@ export default function TariffPrivilegeSlider({ tariff }: Props) {
</Box>
</Box>
);
}
}

@ -12,19 +12,13 @@ export default function FreeTariffCard() {
color="#7E2AEA"
backgroundColor="white"
/>}
buttonText="Выбрать"
headerText="бесплатно"
text="Текст-заполнитель — это текст, который имеет "
onButtonClick={undefined}
price={<Typography variant="price" color="white">0 руб.</Typography>}
sx={{
backgroundColor: "#7E2AEA",
color: "white",
}}
buttonSx={{
color: "white",
borderColor: "white",
}}
/>
);
}

@ -10,13 +10,15 @@ interface Props {
headerText: string;
text: string | string[];
sx?: SxProps<Theme>;
buttonSx?: SxProps<Theme>;
onButtonClick?: MouseEventHandler<HTMLButtonElement>;
buttonText: string;
buttonProps?: {
sx?: SxProps<Theme>;
onClick?: MouseEventHandler<HTMLButtonElement>;
text?: string;
};
price?: ReactNode;
}
export default function TariffCard({ icon, headerText, text, sx, buttonText, onButtonClick, price, buttonSx }: Props) {
export default function TariffCard({ icon, headerText, text, sx, price, buttonProps }: Props) {
const theme = useTheme();
return (
@ -66,18 +68,20 @@ export default function TariffCard({ icon, headerText, text, sx, buttonText, onB
mb: "10px",
}}>{text}</Typography>
}
<CustomButton
onClick={onButtonClick}
variant="outlined"
sx={{
color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main,
mt: "auto",
...buttonSx,
}}
>
{buttonText}
</CustomButton>
{buttonProps &&
<CustomButton
onClick={buttonProps.onClick}
variant="outlined"
sx={{
color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main,
mt: "auto",
...buttonProps.sx,
}}
>
{buttonProps.text}
</CustomButton>
}
</Box>
);
}

@ -44,24 +44,30 @@ export default function Tariffs() {
icon={<CalendarIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
headerText="Тарифы на время"
text="безлимит на 1 месяц , 3 , 6 , 12"
buttonText="Подробнее"
onButtonClick={() => navigate("time")}
buttonProps={{
text: "Подробнее",
onClick: () => navigate("time")
}}
sx={{ maxWidth: "360px" }}
/>
<TariffCard
icon={<PieChartIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
headerText="Тариф на объем"
text="200 шаблонов, 1000 шаблонов, 5000 шаблонов, 10 000 шаблонов"
buttonText="Подробнее"
onButtonClick={() => navigate("volume")}
buttonProps={{
text: "Подробнее",
onClick: () => navigate("volume")
}}
sx={{ maxWidth: "360px" }}
/>
<TariffCard
icon={<CustomIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
headerText="Кастом"
text="Текст-заполнитель — это текст, который имеет "
buttonText="Подробнее"
onButtonClick={() => navigate("/tariffconstructor")}
buttonProps={{
text: "Подробнее",
onClick: () => navigate("/tariffconstructor")
}}
sx={{ maxWidth: "360px" }}
/>
</Box>
@ -114,4 +120,4 @@ export default function Tariffs() {
{/*</Box>*/}
</SectionWrapper>
);
}
}

@ -10,7 +10,7 @@ import { CustomTab } from "@root/components/CustomTab";
import TariffCard from "./TariffCard";
import NumberIcon from "@root/components/NumberIcon";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { calcTariffPrices } from "@root/utils/calcTariffPrices";
import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
import { getMessageFromFetchError } from "@frontend/kitui";
import FreeTariffCard from "./FreeTariffCard";
import { addTariffToCart } from "@root/stores/user";
@ -25,7 +25,7 @@ export default function TariffPage() {
const tariffs = useTariffStore(state => state.tariffs);
const [tabIndex, setTabIndex] = useState<number>(0);
const discounts = useDiscountStore(state => state.discounts);
const customTariffs = useCustomTariffsStore(state => state.customTariffs);
const customTariffs = useCustomTariffsStore(state => state.customTariffsMap);
const unit: string = String(location.pathname).slice(9);
@ -55,7 +55,7 @@ export default function TariffPage() {
});
const tariffElements = filteredTariffs.map((tariff, index) => {
const { price, priceWithDiscounts } = calcTariffPrices(tariff,discounts,customTariffs);
const { price, priceWithDiscounts } = calcIndividualTariffPrices(tariff, discounts, customTariffs);
return (
<TariffCard
@ -65,10 +65,12 @@ export default function TariffPage() {
color={unit === "time" ? "#7E2AEA" : "#FB5607"}
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
/>}
buttonText="Выбрать"
buttonProps={{
text: "Выбрать",
onClick: () => handleTariffItemClick(tariff._id),
}}
headerText={tariff.name}
text={tariff.privilegies.map(p => `${p.name} - ${p.amount}`)}
onButtonClick={() => handleTariffItemClick(tariff._id)}
price={<>
{price !== undefined && price !== priceWithDiscounts &&
<Typography variant="oldPrice">{currencyFormatter.format(price / 100)}</Typography>

@ -53,12 +53,13 @@ export const addCartTariffs = (tariffs: Tariff[]) => useCartStore.setState(
state.cartTariffMap[tariff._id] = tariff;
});
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
state.cart = calcCart(cartTariffs);
const cart = calcCart(cartTariffs);
state.cart = cart;
}),
false,
{
type: tariffs.length > 0 ? "addCartTariffs" : "rejected",
tariffIds: tariffs.map(tariff => tariff._id),
tariffs,
}
);
@ -72,8 +73,7 @@ export const removeMissingCartTariffs = (tariffIds: string[]) => useCartStore.se
}),
false,
{
type: "removeMissingCartTariffs",
tariffIds,
type: "rejected"
}
);

@ -1,23 +1,24 @@
import { createTariff } from "@root/api/tariff";
import { CustomTariffUserValuesMap, SummaryPriceMap } from "@root/model/customTariffs";
import { PrivilegeMap, PrivilegeWithoutPrice, PrivilegeWithAmount } from "@root/model/privilege";
import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs";
import { ServiceKeyToPrivilegesMap, PrivilegeWithoutPrice } from "@root/model/privilege";
import { produce } from "immer";
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { findPrivilegeDiscount } from "@root/utils/calcTariffPrices";
import { useDiscountStore } from "@root/stores/discounts";
import { Discount, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
interface CustomTariffsStore {
customTariffs: PrivilegeMap;
userValues: CustomTariffUserValuesMap;
summaryPrice: SummaryPriceMap;
customTariffsMap: ServiceKeyToPrivilegesMap;
userValuesMap: CustomTariffUserValuesMap;
summaryPriceBeforeDiscountsMap: ServiceKeyToPriceMap;
summaryPriceAfterDiscountsMap: ServiceKeyToPriceMap;
}
const initialState: CustomTariffsStore = {
customTariffs: {},
userValues: {},
summaryPrice: {},
customTariffsMap: {},
userValuesMap: {},
summaryPriceBeforeDiscountsMap: {},
summaryPriceAfterDiscountsMap: {},
};
export const useCustomTariffsStore = create<CustomTariffsStore>()(
@ -31,35 +32,51 @@ export const useCustomTariffsStore = create<CustomTariffsStore>()(
{
name: "customTariffs",
partialize: state => ({
userValues: state.userValues,
summaryPrice: state.summaryPrice,
userValuesMap: state.userValuesMap,
summaryPriceBeforeDiscountsMap: state.summaryPriceBeforeDiscountsMap,
summaryPriceAfterDiscountsMap: state.summaryPriceAfterDiscountsMap,
}),
migrate: () => initialState,
}
)
);
export const setCustomTariffs = (customTariffs: PrivilegeMap) => useCustomTariffsStore.setState({ customTariffs });
export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ customTariffsMap: customTariffs });
export const setCustomTariffsUserValue = (
serviceKey: string,
privilegeId: string,
value: number,
discounts: Discount[],
currentCartTotal: number,
purchasesAmount: number,
) => useCustomTariffsStore.setState(
produce<CustomTariffsStore>(state => {
state.userValues[serviceKey] ??= {};
state.userValues[serviceKey][privilegeId] = value;
state.userValuesMap[serviceKey] ??= {};
state.userValuesMap[serviceKey][privilegeId] = value;
const sum = state.customTariffs[serviceKey].reduce((acc, tariff) => {
const amount = state.userValues[serviceKey]?.[tariff._id] ?? 0;
const discounts = useDiscountStore.getState().discounts;
const discount = findPrivilegeDiscount({privilegeId: tariff.privilegeId, amount: amount} as PrivilegeWithAmount, discounts);
//console.log(discount)
if (discount)
return acc + tariff.price * amount * discount.target.products[0].factor;
return acc + tariff.price * amount;
}, 0);
let priceWithoutDiscounts = 0;
let priceAfterDiscounts = 0;
state.summaryPrice[serviceKey] = sum;
state.customTariffsMap[serviceKey].forEach(tariff => {
const amount = state.userValuesMap[serviceKey]?.[tariff._id] ?? 0;
priceWithoutDiscounts += tariff.price * amount;
const discount = findPrivilegeDiscount(tariff.privilegeId, tariff.price * amount, discounts);
priceAfterDiscounts += tariff.price * amount * discount.factor;
});
const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts);
priceAfterDiscounts *= serviceDiscount.factor;
const cartDiscount = findCartDiscount(currentCartTotal, discounts);
priceAfterDiscounts *= cartDiscount.factor;
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
priceAfterDiscounts *= loyaltyDiscount.factor;
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
})
);
@ -68,10 +85,10 @@ export const createAndSendTariff = (serviceKey: string) => {
const privilegies: PrivilegeWithoutPrice[] = [];
Object.entries(state.userValues[serviceKey]).forEach(([privilegeId, userValue]) => {
Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => {
if (userValue === 0) return;
const privilege = state.customTariffs[serviceKey].find(p => p._id === privilegeId);
const privilege = state.customTariffsMap[serviceKey].find(p => p._id === privilegeId);
if (!privilege) throw new Error(`Privilege not found: ${privilegeId}`);
const p2 = {

@ -1,10 +1,10 @@
import { AnyDiscount } from "@root/model/discount";
import { Discount } from "@frontend/kitui";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface DiscountStore {
discounts: AnyDiscount[];
discounts: Discount[];
}
export const useDiscountStore = create<DiscountStore>()(
@ -19,4 +19,4 @@ export const useDiscountStore = create<DiscountStore>()(
)
);
export const setDiscounts = (discounts: AnyDiscount[]) => useDiscountStore.setState({ discounts });
export const setDiscounts = (discounts: DiscountStore["discounts"]) => useDiscountStore.setState({ discounts });

@ -1,7 +1,6 @@
import { findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
import { CartData, PrivilegeCartData } from "@root/model/cart";
import { AnyDiscount } from "@root/model/discount";
import { Tariff } from "@root/model/tariff";
import { findPrivilegeDiscount, findServiceDiscount, findCartDiscount, findLoyalDiscount } from "./calcTariffPrices";
import { useDiscountStore } from "@root/stores/discounts";
import { useUserStore } from "@root/stores/user";
@ -33,12 +32,11 @@ export function calcCart(tariffs: Tariff[]): CartData {
if (!tariff.price) cartData.priceBeforeDiscounts += privilegePrice;
const privilegeDiscount = findPrivilegeDiscount(privilege, discounts);
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * privilege.amount, discounts);
privilegePrice *= privilegeDiscount.factor;
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts);
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
privilegePrice *= serviceDiscount.factor;
const privilegeData: PrivilegeCartData = {
tariffId: tariff._id,
@ -54,11 +52,11 @@ export function calcCart(tariffs: Tariff[]): CartData {
});
});
const cartDiscount = findCartDiscount(cartData.priceAfterDiscounts, discounts)
if (cartDiscount) cartData.priceAfterDiscounts *= cartDiscount.factor;
const cartDiscount = findCartDiscount(cartData.priceAfterDiscounts, discounts);
cartData.priceAfterDiscounts *= cartDiscount.factor;
const loyalDiscount = findLoyalDiscount(useUserStore.getState().userAccount!.wallet.purchasesAmount, discounts)
if (loyalDiscount) cartData.priceAfterDiscounts *= loyalDiscount.factor;
const loyalDiscount = findLoyaltyDiscount(useUserStore.getState().userAccount!.wallet.purchasesAmount, discounts);
cartData.priceAfterDiscounts *= loyalDiscount.factor;
return cartData;
}

@ -1,124 +1,43 @@
import { Tariff } from "@root/model/tariff";
import { PrivilegeMap} from "@root/model/privilege";
import { PrivilegeWithAmount } from "@root/model/privilege";
import { AnyDiscount, PrivilegeDiscount, ServiceDiscount, CartPurchasesAmountDiscount, PurchasesAmountDiscount } from "../model/discount";
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
import { useCartStore } from "@root/stores/cart";
import { useUserStore } from "@root/stores/user";
import { Discount, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
export function calcTariffPrices(tariff: Tariff, discounts: AnyDiscount[], privilegies: PrivilegeMap): {
export function calcIndividualTariffPrices(tariff: Tariff, discounts: Discount[], privilegies: ServiceKeyToPrivilegesMap): {
price: number | undefined;
priceWithDiscounts: number | undefined;
} {
let price = tariff.price || tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0);
console.log(useCartStore.getState().cart)
let priceWithDiscounts = tariff.privilegies.reduce((sum, privilege) => {
let privilegePrice = privilege.amount * privilege.price;
let realprivilegie = privilegies[privilege.serviceKey].find(e => e._id === privilege.privilegeId)
if (realprivilegie) privilege.privilegeId = realprivilegie.privilegeId
const privilegeDiscount = findPrivilegeDiscount(privilege, discounts);
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
let realprivilegie = privilegies[privilege.serviceKey].find(e => e._id === privilege.privilegeId);
if (realprivilegie) privilege.privilegeId = realprivilegie.privilegeId;
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * privilege.amount, discounts);
privilegePrice *= privilegeDiscount.factor;
const serviceCartData = useCartStore.getState().cart.services.find(e => e.serviceKey === privilege.serviceKey);
let serviceprice = 0;
if (serviceCartData) serviceprice = serviceCartData.price;
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice + serviceprice, discounts);
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
if (serviceDiscount) privilegePrice *= serviceDiscount.factor;
return sum + privilegePrice;
}, 0);
const cartDiscount = findCartDiscount(priceWithDiscounts + useCartStore.getState().cart.priceAfterDiscounts, discounts);
if (cartDiscount) priceWithDiscounts *= cartDiscount.factor;
priceWithDiscounts *= cartDiscount.factor;
const acc = useUserStore.getState();
const loyalDiscount = findLoyalDiscount(acc!.userAccount!.wallet.purchasesAmount, discounts);
if (loyalDiscount) priceWithDiscounts *= loyalDiscount.factor;
const loyalDiscount = findLoyaltyDiscount(acc!.userAccount!.wallet.purchasesAmount, discounts);
priceWithDiscounts *= loyalDiscount.factor;
return {
price,
priceWithDiscounts,
};
}
export function findPrivilegeDiscount(privilege: PrivilegeWithAmount, discounts: AnyDiscount[]): PrivilegeDiscount | null {
const applicableDiscounts = discounts.filter((discount): discount is PrivilegeDiscount => {
return (
discount.conditionType === "privilege" &&
privilege.privilegeId === discount.condition.privilege.id &&
privilege.amount >= discount.condition.privilege.value
);
});
if (!applicableDiscounts.length) return null;
const maxValueDiscount = applicableDiscounts.reduce((prev, current) =>
current.condition.privilege.value > prev.condition.privilege.value ? current : prev
);
return maxValueDiscount;
}
export function findServiceDiscount(
serviceKey: string,
currentPrice: number,
discounts: AnyDiscount[],
): ServiceDiscount | null {
const discountsForTariffService = discounts.filter((discount): discount is ServiceDiscount => {
return (
discount.conditionType === "service" &&
discount.condition.service.id === serviceKey &&
currentPrice >= discount.condition.service.value
);
});
if (!discountsForTariffService.length) return null;
const maxValueDiscount = discountsForTariffService.reduce((prev, current) => {
return current.condition.service.value > prev.condition.service.value ? current : prev;
});
return maxValueDiscount;
}
export function findCartDiscount(
currentPrice: number,
discounts: AnyDiscount[],
): CartPurchasesAmountDiscount | null {
const discountsForCart = discounts.filter((discount) : discount is CartPurchasesAmountDiscount => {
return (
discount.conditionType === "cartPurchasesAmount" &&
currentPrice >= discount.condition.cartPurchasesAmount
)
})
if (!discountsForCart.length) return null;
const maxValueDiscount = discountsForCart.reduce((prev, current) => {
return current.condition.cartPurchasesAmount > prev.condition.cartPurchasesAmount ? current : prev;
})
return maxValueDiscount;
}
export function findLoyalDiscount(
currentPrice: number,
discounts: AnyDiscount[],
): PurchasesAmountDiscount | null {
const discountsForCart = discounts.filter((discount) : discount is PurchasesAmountDiscount => {
return (
discount.conditionType === "purchasesAmount" &&
currentPrice >= discount.condition.purchasesAmount
)
})
if (!discountsForCart.length) return null;
const maxValueDiscount = discountsForCart.reduce((prev, current) => {
return current.condition.purchasesAmount > prev.condition.purchasesAmount ? current : prev;
})
return maxValueDiscount;
}

@ -1,11 +1,11 @@
import { devlog, makeRequest } from "@frontend/kitui";
import { PrivilegeMap } from "@root/model/privilege";
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
import { useEffect, useLayoutEffect, useRef } from "react";
export function useCustomTariffs({ onError, onNewUser, url }: {
url: string;
onNewUser: (response: PrivilegeMap) => void;
onNewUser: (response: ServiceKeyToPrivilegesMap) => void;
onError: (error: any) => void;
}) {
const onNewUserRef = useRef(onNewUser);
@ -19,7 +19,7 @@ export function useCustomTariffs({ onError, onNewUser, url }: {
useEffect(() => {
const controller = new AbortController();
makeRequest<never, PrivilegeMap>({
makeRequest<never, ServiceKeyToPrivilegesMap>({
url,
signal: controller.signal,
method: "get",
@ -33,4 +33,4 @@ export function useCustomTariffs({ onError, onNewUser, url }: {
return () => controller.abort();
}, [url]);
}
}

@ -0,0 +1,36 @@
import { Discount, devlog, makeRequest } from "@frontend/kitui";
import { GetDiscountsResponse } from "@root/model/discount";
import { useEffect, useLayoutEffect, useRef } from "react";
export function useDiscounts({ url = "https://admin.pena.digital/price/discounts", 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();
makeRequest<never, GetDiscountsResponse>({
url,
method: "get",
useToken: true,
signal: controller.signal,
}).then((result) => {
onNewTariffsRef.current(result.Discounts);
}).catch(error => {
devlog("Error fetching tariffs", error);
onErrorRef.current(error);
});
return () => controller.abort();
}, [url]);
}

@ -1,89 +0,0 @@
import { useEffect } from "react";
import {GetDiscounts} from "@root/api/discount";
import {setDiscounts} from "@stores/discounts";
import {BACKEND_Discount} from "@root/model/discount";
export const useSetDiscountsStore = () => {
const gotten = GetDiscounts()
console.log(gotten)
// const gotten:any = getDiscounts()
useEffect(() => {
if (gotten.discounts !== undefined) {
let readyArray = gotten.discounts.map((discount:BACKEND_Discount) => {
if (discount.Condition.PurchasesAmount > 0) return {
_id: discount.ID,
name: discount.Name,
description: discount.Description,
conditionType: "purchasesAmount",
layer: 4,
disabled: discount.Deprecated || discount.Audit.Deleted,
condition:{
purchasesAmount: discount.Condition.PurchasesAmount
},
factor: discount.Target.Factor
}
if (discount.Condition.CartPurchasesAmount > 0) return {
_id: discount.ID,
name: discount.Name,
description: discount.Description,
conditionType: "cartPurchasesAmount",
layer: 3,
disabled: discount.Deprecated || discount.Audit.Deleted,
condition:{
cartPurchasesAmount: discount.Condition.CartPurchasesAmount
},
factor: discount.Target.Factor
}
if (discount.Condition.Product !== "") return {
_id: discount.ID,
name: discount.Name,
description: discount.Description,
conditionType: "privilege",
layer: 1,
disabled: discount.Deprecated || discount.Audit.Deleted,
condition:{
privilege: {
id: discount.Condition.Product,
value: Number(discount.Condition.Term)
}
},
target: {
products: [{
privilegeId: discount.Target.Products[0].ID,
factor: discount.Target.Products[0].Factor
}]
},
factor: discount.Target.Factor
}
if (discount.Condition.Group as string !== "") return {
_id: discount.ID,
name: discount.Name,
description: discount.Description,
conditionType: "service",
layer: 2,
disabled: discount.Deprecated || discount.Audit.Deleted,
condition:{
service: {
id: discount.Condition.Group,
value: discount.Condition.PriceFrom
}
},
target: {
service: discount.Target.Group,
factor: discount.Target.Factor
}
}
})
console.log(readyArray)
// @ts-ignore
setDiscounts(readyArray)
}
}, [gotten.discounts]);
return null
};

@ -1450,11 +1450,12 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@frontend/kitui@^1.0.8":
version "1.0.8"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.8.tgz#9d0619a558db6b417250dceaee60ec61af18b3fd"
integrity sha1-nQYZpVjba0FyUNzq7mDsYa8Ys/0=
"@frontend/kitui@^1.0.11":
version "1.0.11"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.11.tgz#c23c2cb3bcac6ed314e6193de6c56c57132e21a4"
integrity sha1-wjwss7ysbtMU5hk95sVsVxMuIaQ=
dependencies:
immer "^10.0.2"
reconnecting-eventsource "^1.6.2"
"@humanwhocodes/config-array@^0.11.6":
@ -3271,10 +3272,10 @@ axe-core@^4.4.3:
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz"
integrity sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==
axios@^1.3.4:
version "1.3.4"
resolved "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz"
integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==
axios@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
@ -10614,9 +10615,9 @@ yup@^1.1.1:
toposort "^2.0.2"
type-fest "^2.19.0"
zustand@^4.3.6:
version "4.3.6"
resolved "https://registry.npmjs.org/zustand/-/zustand-4.3.6.tgz"
integrity sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==
zustand@^4.3.8:
version "4.3.9"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.9.tgz#a7d4332bbd75dfd25c6848180b3df1407217f2ad"
integrity sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==
dependencies:
use-sync-external-store "1.2.0"