Merge branch 'cart-calc' into promocode-field

This commit is contained in:
nflnkr 2024-03-22 20:47:07 +03:00
commit a2ded0d895
37 changed files with 16084 additions and 38146 deletions

24027
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,75 +1,77 @@
{ {
"name": "hub_frontend", "name": "hub_frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "craco start", "start": "craco start",
"build": "craco build", "build": "craco build",
"test": "craco test --env=node --transformIgnorePatterns \"node_modules/(?!@frontend)/\"", "test": "craco test --env=node --transformIgnorePatterns \"node_modules/(?!@frontend)/\"",
"test:cart": "craco test src/utils/calcCart --transformIgnorePatterns \"node_modules/(?!@frontend)/\"", "test:cart": "vitest ./src/utils/calcCart",
"eject": "craco eject", "eject": "craco eject",
"test:cypress": "start-server-and-test start http://localhost:3000 cypress", "test:cypress": "start-server-and-test start http://localhost:3000 cypress",
"cypress": "cypress open", "cypress": "cypress open",
"prepare": "husky install" "prepare": "husky install"
}, },
"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.65", "@frontend/kitui": "^1.0.66",
"@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",
"axios": "^1.4.0", "axios": "^1.4.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"cypress": "^12.17.3", "cypress": "^12.17.3",
"formik": "^2.2.9", "formik": "^2.2.9",
"husky": "^8.0.3", "husky": "^8.0.3",
"immer": "^10.0.2", "immer": "^10.0.2",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"pdfjs-dist": "3.6.172", "pdfjs-dist": "3.6.172",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11", "react-error-boundary": "^4.0.11",
"react-pdf": "^7.1.2", "react-pdf": "^7.1.2",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"react-slick": "^0.29.0", "react-slick": "^0.29.0",
"slick-carousel": "^1.8.1", "slick-carousel": "^1.8.1",
"use-debounce": "^10.0.0", "swr": "^2.2.5",
"web-vitals": "^2.1.0", "use-debounce": "^10.0.0",
"yup": "^1.1.1", "web-vitals": "^2.1.0",
"zustand": "^4.3.8" "yup": "^1.1.1",
}, "zustand": "^4.3.8"
"devDependencies": { },
"@craco/craco": "^7.1.0", "devDependencies": {
"@testing-library/jest-dom": "^5.14.1", "@craco/craco": "^7.1.0",
"@testing-library/react": "^13.0.0", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/user-event": "^13.2.1", "@testing-library/react": "^13.0.0",
"@types/jest": "^27.0.1", "@testing-library/user-event": "^13.2.1",
"@types/node": "^16.7.13", "@types/jest": "^27.0.1",
"@types/react": "^18.0.0", "@types/node": "^16.7.13",
"@types/react-dom": "^18.0.0", "@types/react": "^18.0.0",
"@types/react-slick": "^0.23.10", "@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^6.9.1", "@types/react-slick": "^0.23.10",
"@typescript-eslint/parser": "^6.9.1", "@typescript-eslint/eslint-plugin": "^6.9.1",
"craco-alias": "^3.0.1", "@typescript-eslint/parser": "^6.9.1",
"eslint": "^8.53.0", "craco-alias": "^3.0.1",
"eslint-plugin-react": "^7.33.2", "eslint": "^8.53.0",
"jest": "^29.5.0", "eslint-plugin-react": "^7.33.2",
"react-scripts": "5.0.1", "jest": "^29.5.0",
"typescript": "^5.4.2" "react-scripts": "5.0.1",
}, "typescript": "^5.4.2",
"browserslist": { "vitest": "^1.4.0"
"production": [ },
">0.2%", "browserslist": {
"not dead", "production": [
"not op_mini all" ">0.2%",
], "not dead",
"development": [ "not op_mini all"
"last 1 chrome version", ],
"last 1 firefox version", "development": [
"last 1 safari version" "last 1 chrome version",
] "last 1 firefox version",
} "last 1 safari version"
]
}
} }

@ -73,7 +73,6 @@ export async function getHistory(): Promise<[GetHistoryResponse | GetHistoryResp
historyResponse.records = checked || [] historyResponse.records = checked || []
console.log("historyResponse ", historyResponse)
return [historyResponse] return [historyResponse]
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError)

@ -1,88 +1,119 @@
import { Tariff, makeRequest } from "@frontend/kitui" import { Tariff, makeRequest } from "@frontend/kitui";
import { CreateTariffBody } from "@root/model/customTariffs" import { CreateTariffBody } from "@root/model/customTariffs";
import { parseAxiosError } from "@root/utils/parse-error" import { parseAxiosError } from "@root/utils/parse-error";
import type { ServiceKeyToPrivilegesMap } from "@root/model/privilege" import type { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
import type { GetTariffsResponse } from "@root/model/tariff" import type { GetTariffsResponse } from "@root/model/tariff";
import { removeTariffFromCart } from "@root/stores/user";
const apiUrl = process.env.REACT_APP_DOMAIN + "/strator" const apiUrl = process.env.REACT_APP_DOMAIN + "/strator"
console.log("домен с которого тарифы запрашиваются", process.env.REACT_APP_DOMAIN) console.log("домен с которого тарифы запрашиваются", process.env.REACT_APP_DOMAIN)
console.log("домен с которого тарифы запрашиваются", apiUrl) console.log("домен с которого тарифы запрашиваются", apiUrl)
export async function getTariffs( export async function getTariffs(
apiPage: number, apiPage: number,
tariffsPerPage: number, tariffsPerPage: number,
signal: AbortSignal | undefined signal: AbortSignal | undefined
): Promise<[GetTariffsResponse | null, string?]> { ): Promise<[GetTariffsResponse | null, string?]> {
try { try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({ const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
url: apiUrl + `/tariff?page=${apiPage}&limit=${tariffsPerPage}`, url: apiUrl + `/tariff?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get", method: "get",
useToken: true, useToken: true,
signal, signal,
}) });
return [tariffsResponse] return [tariffsResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список тарифов. ${error}`] return [null, `Не удалось получить список тарифов. ${error}`];
} }
} }
export async function createTariff(tariff: CreateTariffBody): Promise<[Tariff | null, string?]> { export async function createTariff(tariff: CreateTariffBody): Promise<[Tariff | null, string?]> {
try { try {
const createTariffResponse = await makeRequest<CreateTariffBody, Tariff>({ const createTariffResponse = await makeRequest<CreateTariffBody, Tariff>({
url: `${apiUrl}/tariff`, url: `${apiUrl}/tariff`,
method: "post", method: "post",
useToken: true, useToken: true,
body: tariff, body: tariff,
}) });
return [createTariffResponse] return [createTariffResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Не удалось создать тариф. ${error}`] return [null, `Не удалось создать тариф. ${error}`];
} }
} }
export async function getTariffById(tariffId: string): Promise<[Tariff | null, string?, number?]> { export async function getTariffById(tariffId: string): Promise<[Tariff | null, string?, number?]> {
try { try {
const getTariffByIdResponse = await makeRequest<never, Tariff>({ const getTariffByIdResponse = await makeRequest<never, Tariff>({
url: `${apiUrl}/tariff/${tariffId}`, url: `${apiUrl}/tariff/${tariffId}`,
method: "get", method: "get",
useToken: true, useToken: true,
}) });
return [getTariffByIdResponse] return [getTariffByIdResponse];
} catch (nativeError) { } catch (nativeError) {
const [error, status] = parseAxiosError(nativeError) const [error, status] = parseAxiosError(nativeError);
return [null, `Не удалось получить тарифы. ${error}`, status] return [null, `Не удалось получить тарифы. ${error}`, status];
} }
} }
export async function getCustomTariffs( export async function getCustomTariffs(
signal: AbortSignal | undefined signal: AbortSignal | undefined
): Promise<[ServiceKeyToPrivilegesMap | null, string?]> { ): Promise<[ServiceKeyToPrivilegesMap | null, string?]> {
try { try {
const customTariffsResponse = await makeRequest<null, ServiceKeyToPrivilegesMap>({ const customTariffsResponse = await makeRequest<null, ServiceKeyToPrivilegesMap>({
url: apiUrl + "/privilege/service", url: apiUrl + "/privilege/service",
signal, signal,
method: "get", method: "get",
useToken: true, useToken: true,
}) });
const tempCustomTariffsResponse = { const tempCustomTariffsResponse = {
...customTariffsResponse, ...customTariffsResponse,
squiz: customTariffsResponse.squiz squiz: customTariffsResponse.squiz
}; };
return [tempCustomTariffsResponse] return [tempCustomTariffsResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить мои тарифы. ${error}`] return [null, `Не удалось получить мои тарифы. ${error}`];
} }
}
export async function getTariffArray(tariffIds: string[] | undefined) {
if (!tariffIds) return null;
const responses = await Promise.allSettled(tariffIds.map(tariffId =>
makeRequest<never, Tariff>({
url: `${apiUrl}/tariff/${tariffId}`,
method: "get",
useToken: true,
})
));
const tariffs: Tariff[] = [];
responses.forEach((response, index) => {
switch (response.status) {
case "fulfilled": {
tariffs.push(response.value);
break;
}
case "rejected": {
const [, status] = parseAxiosError(response.reason);
if (status === 404) removeTariffFromCart(tariffIds[index]);
break;
}
}
});
return tariffs;
} }

@ -14,10 +14,9 @@ import type { MouseEvent } from "react"
interface Props { interface Props {
tariffCartData: TariffCartData; tariffCartData: TariffCartData;
outsideFactor: number;
} }
export default function CustomTariffAccordion({ tariffCartData, outsideFactor }: Props) { export default function CustomTariffAccordion({ tariffCartData }: Props) {
const theme = useTheme() const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md")) const upMd = useMediaQuery(theme.breakpoints.up("md"))
const upSm = useMediaQuery(theme.breakpoints.up("sm")) const upSm = useMediaQuery(theme.breakpoints.up("sm"))
@ -36,7 +35,6 @@ export default function CustomTariffAccordion({ tariffCartData, outsideFactor }:
}) })
} }
console.log('DRAWER', outsideFactor)
return ( return (
<Box <Box
sx={{ sx={{
@ -103,7 +101,7 @@ console.log('DRAWER', outsideFactor)
fontWeight: 500, fontWeight: 500,
}} }}
> >
{currencyFormatter.format(tariffCartData.price * outsideFactor / 100)} {currencyFormatter.format(tariffCartData.price / 100)}
</Typography> </Typography>
</Box> </Box>
<CloseButton <CloseButton
@ -150,7 +148,7 @@ console.log('DRAWER', outsideFactor)
fontWeight: 500, fontWeight: 500,
}} }}
> >
{currencyFormatter.format(privilege.price * outsideFactor / 100)} {currencyFormatter.format(privilege.price / 100)}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>

@ -13,6 +13,7 @@ import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
import type { MouseEvent } from "react"; import type { MouseEvent } from "react";
import CustomTariffAccordion from "@root/components/CustomTariffAccordion"; import CustomTariffAccordion from "@root/components/CustomTariffAccordion";
import { setNotEnoughMoneyAmount } from "@root/stores/cart";
const name: Record<string, string> = { const name: Record<string, string> = {
templategen: "Шаблонизатор", templategen: "Шаблонизатор",
@ -23,10 +24,9 @@ const name: Record<string, string> = {
interface Props { interface Props {
serviceData: ServiceCartData; serviceData: ServiceCartData;
outsideFactor: number;
} }
export default function CustomWrapperDrawer({ serviceData, outsideFactor }: Props) { export default function CustomWrapperDrawer({ serviceData }: Props) {
const [isExpanded, setIsExpanded] = useState<boolean>(false); const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
@ -51,6 +51,7 @@ export default function CustomWrapperDrawer({ serviceData, outsideFactor }: Prop
const errors: string[] = []; const errors: string[] = [];
setIsLoading(true); setIsLoading(true);
setNotEnoughMoneyAmount(0);
for (const { id } of serviceData.tariffs) { for (const { id } of serviceData.tariffs) {
const [cartItemsResponse, deleteError] = await deleteCart(id); const [cartItemsResponse, deleteError] = await deleteCart(id);
@ -138,7 +139,7 @@ export default function CustomWrapperDrawer({ serviceData, outsideFactor }: Prop
fontWeight: 500, fontWeight: 500,
}} }}
> >
{currencyFormatter.format(serviceData.price * outsideFactor / 100)} {currencyFormatter.format(serviceData.price / 100)}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -169,9 +170,8 @@ export default function CustomWrapperDrawer({ serviceData, outsideFactor }: Prop
serviceData.tariffs.map((tariff) => { serviceData.tariffs.map((tariff) => {
const privilege = tariff.privileges[0]; const privilege = tariff.privileges[0];
console.log('CD', tariff, outsideFactor)
return tariff.privileges.length > 1 ? ( return tariff.privileges.length > 1 ? (
<CustomTariffAccordion outsideFactor={outsideFactor} key={tariff.id} tariffCartData={tariff} /> <CustomTariffAccordion key={tariff.id} tariffCartData={tariff} />
) : ( ) : (
<Box <Box
key={tariff.id + privilege.privilegeId} key={tariff.id + privilege.privilegeId}
@ -211,7 +211,7 @@ console.log('CD', tariff, outsideFactor)
fontWeight: 500, fontWeight: 500,
}} }}
> >
{currencyFormatter.format((tariff.price || privilege.price) * outsideFactor / 100)} {currencyFormatter.format((tariff.price || privilege.price) / 100)}
</Typography> </Typography>
<SvgIcon <SvgIcon
sx={{ sx={{

@ -6,7 +6,6 @@ import { NotificationsModal } from "./NotificationsModal";
import { Loader } from "./Loader"; import { Loader } from "./Loader";
import { useCart } from "@root/utils/hooks/useCart"; import { useCart } from "@root/utils/hooks/useCart";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { closeCartDrawer, openCartDrawer, setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart";
import { setUserAccount, useUserStore } from "@root/stores/user"; import { setUserAccount, useUserStore } from "@root/stores/user";
import { useTicketStore } from "@root/stores/tickets"; import { useTicketStore } from "@root/stores/tickets";
@ -20,6 +19,7 @@ import { Link, useNavigate } from "react-router-dom";
import { withErrorBoundary } from "react-error-boundary"; import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError"; import { handleComponentError } from "@root/utils/handleComponentError";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart";
function Drawers() { function Drawers() {
const [openNotificationsModal, setOpenNotificationsModal] = useState<boolean>(false); const [openNotificationsModal, setOpenNotificationsModal] = useState<boolean>(false);
@ -29,7 +29,7 @@ function Drawers() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isDrawerOpen = useCartStore((state) => state.isDrawerOpen); const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
const cart = useCart(); const cart = useCart();
const userAccount = useUserStore((state) => state.userAccount); const userAccount = useUserStore((state) => state.userAccount);
const tickets = useTicketStore((state) => state.tickets); const tickets = useTicketStore((state) => state.tickets);
@ -61,11 +61,11 @@ function Drawers() {
} }
setLoading(false); setLoading(false);
closeCartDrawer(); setIsDrawerOpen(false);
} }
function handleReplenishWallet() { function handleReplenishWallet() {
closeCartDrawer(); setIsDrawerOpen(false);
navigate("/payment", { state: { notEnoughMoneyAmount } }); navigate("/payment", { state: { notEnoughMoneyAmount } });
} }
@ -134,7 +134,7 @@ function Drawers() {
}))} }))}
/> />
<IconButton <IconButton
onClick={openCartDrawer} onClick={() => setIsDrawerOpen(true)}
component="div" component="div"
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
@ -172,7 +172,7 @@ function Drawers() {
<CartIcon /> <CartIcon />
</Badge> </Badge>
</IconButton> </IconButton>
<Drawer anchor={"right"} open={isDrawerOpen} onClose={closeCartDrawer} sx={{ background: "rgba(0, 0, 0, 0.55)" }}> <Drawer anchor={"right"} open={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} sx={{ background: "rgba(0, 0, 0, 0.55)" }}>
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{ sx={{
@ -204,15 +204,16 @@ function Drawers() {
> >
Корзина Корзина
</Typography> </Typography>
<IconButton onClick={closeCartDrawer} sx={{ p: 0 }}> <IconButton onClick={() => setIsDrawerOpen(false)} sx={{ p: 0 }}>
<CrossIcon /> <CrossIcon />
</IconButton> </IconButton>
</Box> </Box>
<Box sx={{ pl: "20px", pr: "20px" }}> <Box sx={{ pl: "20px", pr: "20px" }}>
{cart.services.map((serviceData) => { {cart.services.map((serviceData) => {
return ( return (
<CustomWrapperDrawer key={serviceData.serviceKey} outsideFactor={(cart.appliedCartPurchasesDiscount?.Target?.Factor || 1)*(cart.appliedLoyaltyDiscount?.Target?.Factor || 1)} serviceData={serviceData} /> <CustomWrapperDrawer key={serviceData.serviceKey} serviceData={serviceData} />
)})} );
})}
<Box <Box
sx={{ sx={{
mt: "40px", mt: "40px",
@ -256,8 +257,7 @@ function Drawers() {
}} }}
> >
- -
{`${ {`${((cart.priceBeforeDiscounts - cart.priceAfterDiscounts) / (cart.priceBeforeDiscounts / 100)).toFixed(0)
((cart.priceBeforeDiscounts - cart.priceAfterDiscounts) / (cart.priceBeforeDiscounts / 100)).toFixed(0)
}%`} }%`}
</span> </span>
) : null ) : null
@ -333,7 +333,7 @@ function Drawers() {
disabled={cart.priceAfterDiscounts === 0} disabled={cart.priceAfterDiscounts === 0}
variant="pena-contained-dark" variant="pena-contained-dark"
onClick={() => (notEnoughMoneyAmount === 0 ? !loading && handlePayClick() : handleReplenishWallet())} onClick={() => (notEnoughMoneyAmount === 0 ? !loading && handlePayClick() : handleReplenishWallet())}
sx={{ display: "block" }} sx={{ display: "block" }}
> >
{loading ? <Loader size={24} /> : notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"} {loading ? <Loader size={24} /> : notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}
</Button> </Button>

@ -2,113 +2,114 @@ import { useState } from "react"
import { InputAdornment, TextField, Typography, useTheme } from "@mui/material" import { InputAdornment, TextField, Typography, useTheme } from "@mui/material"
import type { ChangeEvent } from "react" import type { ChangeEvent } from "react"
import {Privilege} from "@frontend/kitui" import { CustomPrivilege } from "@frontend/kitui"
interface Props { interface Props {
id: string; id: string
value: number; value: number
adornmentText: string; adornmentText: string
privilege: Privilege; privilege: CustomPrivilege
onChange: (value: number) => void; onChange: (value: number) => void
} }
const sliderSettingsByType = { const sliderSettingsByType = {
день: { max: 365, min: 0 }, день: { max: 365, min: 0 },
шаблон: { max: 5000, min: 0 }, шаблон: { max: 5000, min: 0 },
МБ: { max: 5000, min: 0 }, МБ: { max: 5000, min: 0 },
заявка: { max: 5000, min: 0 } заявка: { max: 5000, min: 0 }
} }
export default function NumberInputWithUnitAdornment({ id, value, adornmentText, privilege, onChange }: Props) { export default function NumberInputWithUnitAdornment({ id, value, adornmentText, privilege, onChange }: Props) {
const theme = useTheme() const theme = useTheme()
const [changed, setChanged] = useState<boolean>(false) const [changed, setChanged] = useState<boolean>(false)
return ( return (
<TextField <TextField
type="number" type="number"
size="small" size="small"
placeholder="Введите вручную" placeholder="Введите вручную"
id={id} id={id}
onBlur={(e) => {e.target.value = String(Number(String(e.target.value).replace(/^0+(?=\d\.)/, ''))) onBlur={(e) => {
console.log("сработало", e.target.value) e.target.value = String(Number(String(e.target.value).replace(/^0+(?=\d\.)/, "")))
}} console.log("сработало", e.target.value)
value={changed ? (value !== sliderSettingsByType[privilege.value]?.min ? parseInt(String(value), 10) : sliderSettingsByType[privilege.value]?.min) : ""} }}
onChange={({ target }: ChangeEvent<HTMLInputElement>) => { value={changed ? (value !== sliderSettingsByType[privilege.value]?.min ? parseInt(String(value), 10) : sliderSettingsByType[privilege.value]?.min) : ""}
if (!changed) { onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
setChanged(true) if (!changed) {
} setChanged(true)
}
if (Number(target.value) > 999999) { if (Number(target.value) > 999999) {
target.value = "999999" target.value = "999999"
} }
const newNumber = parseInt(target.value) const newNumber = parseInt(target.value)
if (!isFinite(newNumber) || newNumber < 0) { if (!isFinite(newNumber) || newNumber < 0) {
onChange(sliderSettingsByType[privilege.value]?.min) onChange(sliderSettingsByType[privilege.value]?.min)
return return
} }
onChange(newNumber) onChange(newNumber)
}} }}
sx={{ sx={{
maxWidth: "200px", maxWidth: "200px",
minWidth: "200px", minWidth: "200px",
".MuiInputBase-root": { ".MuiInputBase-root": {
display: "flex", display: "flex",
pr: 0, pr: 0,
height: "48px", height: "48px",
borderRadius: "8px", borderRadius: "8px",
backgroundColor: "#F2F3F7", backgroundColor: "#F2F3F7",
fieldset: { fieldset: {
border: "1px solid" + theme.palette.gray.main, border: "1px solid" + theme.palette.gray.main,
}, },
"&.Mui-focused fieldset": { "&.Mui-focused fieldset": {
borderColor: theme.palette.purple.main, borderColor: theme.palette.purple.main,
}, },
input: { input: {
height: "31px", height: "31px",
borderRight: !changed ? "none" : "1px solid #9A9AAF", borderRight: !changed ? "none" : "1px solid #9A9AAF",
}, },
"&.Mui-focused input": { "&.Mui-focused input": {
borderRight: "1px solid #9A9AAF", borderRight: "1px solid #9A9AAF",
}, },
"&:not(.Mui-focused) .MuiInputAdornment-root": { "&:not(.Mui-focused) .MuiInputAdornment-root": {
display: !changed ? "none" : undefined, display: !changed ? "none" : undefined,
}, },
"&.Mui-focused ::-webkit-input-placeholder": { "&.Mui-focused ::-webkit-input-placeholder": {
color: "transparent", color: "transparent",
}, },
// Hiding arrows // Hiding arrows
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": { "input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": {
WebkitAppearance: "none", WebkitAppearance: "none",
margin: 0, margin: 0,
}, },
"input[type = number]": { "input[type = number]": {
MozAppearance: "textfield", MozAppearance: "textfield",
}, },
}, },
}} }}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<InputAdornment <InputAdornment
position="end" position="end"
sx={{ sx={{
userSelect: "none", userSelect: "none",
pointerEvents: "none", pointerEvents: "none",
pl: "2px", pl: "2px",
pr: "13px", pr: "13px",
}} }}
> >
<Typography variant="body2" color="#4D4D4D"> <Typography variant="body2" color="#4D4D4D">
{adornmentText} {adornmentText}
</Typography> </Typography>
</InputAdornment> </InputAdornment>
), ),
}} }}
/> />
) )
} }

@ -8,7 +8,6 @@ import { Loader } from "./Loader";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { payCart } from "@root/api/cart"; import { payCart } from "@root/api/cart";
import { setUserAccount } from "@root/stores/user"; import { setUserAccount } from "@root/stores/user";
import { useCart } from "@root/utils/hooks/useCart";
import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart"; import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart";
interface Props { interface Props {
@ -20,8 +19,7 @@ interface Props {
export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts, isConstructor = false }: Props) { export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts, isConstructor = false }: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const cart = useCart(); const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount);
const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount)
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const navigate = useNavigate(); const navigate = useNavigate();
@ -169,7 +167,7 @@ export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts,
)} )}
<Button <Button
variant="pena-contained-dark" variant="pena-contained-dark"
disabled = {cart.priceAfterDiscounts === 0} disabled = {priceAfterDiscounts === 0}
onClick={() => (notEnoughMoneyAmount === 0 ? !loading && handlePayClick() : handleReplenishWallet())} onClick={() => (notEnoughMoneyAmount === 0 ? !loading && handlePayClick() : handleReplenishWallet())}
> >
{loading ? <Loader size={24} /> : notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"} {loading ? <Loader size={24} /> : notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}

@ -1,4 +1,3 @@
import { PrivilegeWithAmount } from "@frontend/kitui"
import { PrivilegeWithoutPrice } from "./privilege" import { PrivilegeWithoutPrice } from "./privilege"

@ -1,6 +1,6 @@
import { Privilege, PrivilegeWithAmount } from "@frontend/kitui" import { CustomPrivilegeWithAmount } from "@frontend/kitui";
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>; export type ServiceKeyToPrivilegesMap = Record<string, CustomPrivilegeWithAmount[]>;
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">; export type PrivilegeWithoutPrice = Omit<CustomPrivilegeWithAmount, "price">;

@ -4,9 +4,4 @@ import { Tariff } from "@frontend/kitui"
export interface GetTariffsResponse { export interface GetTariffsResponse {
totalPages: number; totalPages: number;
tariffs: Tariff[]; tariffs: Tariff[];
} }
export interface FullTariff extends Tariff {
description?: string;
order?: number;
}

@ -1,82 +1,79 @@
import { Box, IconButton, Typography, Badge, useMediaQuery, useTheme } from "@mui/material" import { Box, IconButton, Typography, Badge, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper" import SectionWrapper from "@components/SectionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack" import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@components/TotalPrice" import TotalPrice from "@components/TotalPrice";
import CustomWrapper from "./CustomWrapper" import CustomWrapper from "./CustomWrapper";
import { useCart } from "@root/utils/hooks/useCart" import { useCart } from "@root/utils/hooks/useCart";
import { useLocation } from "react-router-dom" import { useLocation } from "react-router-dom";
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation" import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
import { handleComponentError } from "@root/utils/handleComponentError" import { handleComponentError } from "@root/utils/handleComponentError";
import { withErrorBoundary } from "react-error-boundary" import { withErrorBoundary } from "react-error-boundary";
function Cart() { function Cart() {
const theme = useTheme() const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")) const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(550)) const isMobile = useMediaQuery(theme.breakpoints.down(550));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)) const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const cart = useCart() const cart = useCart();
const location = useLocation() const location = useLocation();
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts;
const totalPriceAfterDiscounts = cart.priceAfterDiscounts const totalPriceAfterDiscounts = cart.priceAfterDiscounts;
console.log('CART', totalPriceAfterDiscounts, totalPriceBeforeDiscounts, cart) const handleCustomBackNavigation = usePrevLocation(location);
const handleCustomBackNavigation = usePrevLocation(location)
return (
return ( <SectionWrapper
<SectionWrapper maxWidth="lg"
maxWidth="lg" sx={{
sx={{ mt: upMd ? "25px" : "20px",
mt: upMd ? "25px" : "20px", px: isTablet ? (upMd ? "40px" : "18px") : "20px",
px: isTablet ? (upMd ? "40px" : "18px") : "20px", mb: upMd ? "70px" : "37px",
mb: upMd ? "70px" : "37px", }}
}} >
> <Box
<Box sx={{
sx={{ mt: "20px",
mt: "20px", mb: upMd ? "40px" : "20px",
mb: upMd ? "40px" : "20px", display: "flex",
display: "flex", alignItems: "center",
alignItems: "center", gap: "10px",
gap: "10px", }}
}} >
> {isMobile && (
{isMobile && ( <IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}> <ArrowBackIcon />
<ArrowBackIcon /> </IconButton>
</IconButton> )}
)} <Typography
<Typography sx={{
sx={{ fontSize: isMobile ? "24px" : "36px",
fontSize: isMobile ? "24px" : "36px", fontWeight: "500",
fontWeight: "500", }}
}} >
> Корзина
Корзина </Typography>
</Typography> </Box>
</Box> <Box
<Box sx={{
sx={{ mt: upMd ? "27px" : "10px",
mt: upMd ? "27px" : "10px", }}
}} >
> {cart.services.map((serviceData, index) => (
{cart.services.map((serviceData, index) => ( <CustomWrapper
<CustomWrapper key={serviceData.serviceKey}
key={serviceData.serviceKey} serviceData={serviceData}
serviceData={serviceData} first={index === 0}
outsideFactor={(cart.appliedCartPurchasesDiscount?.Target?.Factor || 1)*(cart.appliedLoyaltyDiscount?.Target?.Factor || 1)} last={index === cart.services.length - 1}
first={index === 0} />
last={index === cart.services.length - 1} ))}
/> </Box>
))} <TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} />
</Box> </SectionWrapper>
<TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} /> );
</SectionWrapper>
)
} }
export default withErrorBoundary(Cart, { export default withErrorBoundary(Cart, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении корзины</Typography>, fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении корзины</Typography>,
onError: handleComponentError, onError: handleComponentError,
}) });

@ -5,7 +5,7 @@ import ClearIcon from "@mui/icons-material/Clear"
import { currencyFormatter } from "@root/utils/currencyFormatter" import { currencyFormatter } from "@root/utils/currencyFormatter"
import { removeTariffFromCart } from "@root/stores/user" import { removeTariffFromCart } from "@root/stores/user"
import { enqueueSnackbar } from "notistack" import { enqueueSnackbar } from "notistack"
import { CloseButton, ServiceCartData, getMessageFromFetchError } from "@frontend/kitui" import { CloseButton, ServiceCartData, getMessageFromFetchError } from "@frontend/kitui"
import type { MouseEvent } from "react" import type { MouseEvent } from "react"
import CustomTariffAccordion from "@root/components/CustomTariffAccordion" import CustomTariffAccordion from "@root/components/CustomTariffAccordion"
@ -19,12 +19,11 @@ const name: Record<string, string> = {
interface Props { interface Props {
serviceData: ServiceCartData; serviceData: ServiceCartData;
outsideFactor: number;
last?: boolean; last?: boolean;
first?: boolean; first?: boolean;
} }
export default function CustomWrapper({ serviceData, last, first, outsideFactor }: Props) { export default function CustomWrapper({ serviceData, last, first }: Props) {
const theme = useTheme() const theme = useTheme()
const upMd = useMediaQuery(theme.breakpoints.up("md")) const upMd = useMediaQuery(theme.breakpoints.up("md"))
const upSm = useMediaQuery(theme.breakpoints.up("sm")) const upSm = useMediaQuery(theme.breakpoints.up("sm"))
@ -55,7 +54,6 @@ export default function CustomWrapper({ serviceData, last, first, outsideFactor
enqueueSnackbar("Тарифы удалены") enqueueSnackbar("Тарифы удалены")
} }
console.log('SDDD', serviceData)
return ( return (
<Box <Box
sx={{ sx={{
@ -128,7 +126,7 @@ console.log('SDDD', serviceData)
fontWeight: 500, fontWeight: 500,
}} }}
> >
{currencyFormatter.format(serviceData.price * outsideFactor / 100)} {currencyFormatter.format(serviceData.price / 100)}
</Typography> </Typography>
</Box> </Box>
<CloseButton style={{ height: "22 px", width: "22px" }} onClick={deleteService} /> <CloseButton style={{ height: "22 px", width: "22px" }} onClick={deleteService} />
@ -139,7 +137,7 @@ console.log('SDDD', serviceData)
return tariff.privileges.length > 1 ? ( return tariff.privileges.length > 1 ? (
<CustomTariffAccordion outsideFactor={outsideFactor} key={tariff.id} tariffCartData={tariff} /> <CustomTariffAccordion key={tariff.id} tariffCartData={tariff} />
) : ( ) : (
<Box <Box
key={tariff.id + privilege.privilegeId} key={tariff.id + privilege.privilegeId}
@ -178,7 +176,7 @@ console.log('SDDD', serviceData)
fontWeight: 500, fontWeight: 500,
}} }}
> >
{currencyFormatter.format(tariff.price * outsideFactor / 100)} {currencyFormatter.format(tariff.price / 100)}
</Typography> </Typography>
</Box> </Box>
<Box <Box

@ -4,11 +4,11 @@ import { enqueueSnackbar } from "notistack"
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material" import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material"
import { addTariffToCart } from "@root/stores/user" import { addTariffToCart } from "@root/stores/user"
import { PrivilegeWithAmount, getMessageFromFetchError } from "@frontend/kitui" import { Privilege, getMessageFromFetchError } from "@frontend/kitui"
import CustomSaveAccordion from "../../components/CustomSaveAccordion" import CustomSaveAccordion from "../../components/CustomSaveAccordion"
interface Props { interface Props {
privileges: PrivilegeWithAmount[]; privileges: Privilege[];
tariffId: string; tariffId: string;
createdAt: string | undefined; createdAt: string | undefined;
first?: boolean; first?: boolean;

@ -1,27 +1,27 @@
import { CustomPrivilegeWithAmount } from "@frontend/kitui";
import { import {
Box, Badge,
Button, Box,
Divider, Button,
Badge, Divider,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material" } from "@mui/material";
import TariffPrivilegeSlider from "./TariffItem"
import { import {
createAndSendTariff, createAndSendTariff,
useCustomTariffsStore, useCustomTariffsStore,
} from "@root/stores/customTariffs" } from "@root/stores/customTariffs";
import { cardShadow } from "@root/utils/theme" import { updateTariffs } from "@root/stores/tariffs";
import { currencyFormatter } from "@root/utils/currencyFormatter" import { addTariffToCart } from "@root/stores/user";
import { Privilege, getMessageFromFetchError } from "@frontend/kitui" import { currencyFormatter } from "@root/utils/currencyFormatter";
import { enqueueSnackbar } from "notistack" import { cardShadow } from "@root/utils/theme";
import { updateTariffs } from "@root/stores/tariffs" import { enqueueSnackbar } from "notistack";
import { addTariffToCart } from "@root/stores/user" import TariffPrivilegeSlider from "./TariffItem";
interface Props { interface Props {
serviceKey: string; serviceKey: string;
privileges: Privilege[]; privileges: CustomPrivilegeWithAmount[];
} }
export default function CustomTariffCard({ serviceKey, privileges }: Props) { export default function CustomTariffCard({ serviceKey, privileges }: Props) {

@ -1,92 +1,94 @@
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material" import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Link } from "react-router-dom" import { Link } from "react-router-dom";
import SectionWrapper from "@components/SectionWrapper" import SectionWrapper from "@components/SectionWrapper";
import { useCustomTariffsStore } from "@root/stores/customTariffs" import { useCustomTariffsStore } from "@root/stores/customTariffs";
import ComplexHeader from "@root/components/ComplexHeader" import ComplexHeader from "@root/components/ComplexHeader";
import CustomTariffCard from "./CustomTariffCard" import CustomTariffCard from "./CustomTariffCard";
import ArrowBackIcon from "@mui/icons-material/ArrowBack" import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@root/components/TotalPrice" import TotalPrice from "@root/components/TotalPrice";
import { serviceNameByKey } from "@root/utils/serviceKeys" import { serviceNameByKey } from "@root/utils/serviceKeys";
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker" import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import { withErrorBoundary } from "react-error-boundary" import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError" import { handleComponentError } from "@root/utils/handleComponentError";
import { useCart } from "@root/utils/hooks/useCart"; import { useCart } from "@root/utils/hooks/useCart";
function TariffConstructor() { function TariffConstructor() {
const theme = useTheme() const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")) const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)) const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const cart = useCart(); const cart = useCart();
const customTariffs = useCustomTariffsStore((state) => state.privilegeByService) const customTariffs = useCustomTariffsStore((state) => state.privilegeByService);
const basePrice = cart.priceBeforeDiscounts const basePrice = cart.priceBeforeDiscounts;
const discountedPrice = cart.priceAfterDiscounts const discountedPrice = cart.priceAfterDiscounts;
const handleCustomBackNavigation = useHistoryTracker() const handleCustomBackNavigation = useHistoryTracker();
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{ sx={{
mt: upMd ? "25px" : "20px", mt: upMd ? "25px" : "20px",
px: isTablet ? (upMd ? "40px" : "18px") : "20px", px: isTablet ? (upMd ? "40px" : "18px") : "20px",
mb: upMd ? "93px" : "48px", mb: upMd ? "93px" : "48px",
}} }}
> >
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "80px", gap: "80px",
}} }}
> >
{Object.entries(customTariffs).filter(([serviceKey]) => serviceKey === "squiz").map(([serviceKey, privileges], index) => { {Object.entries(customTariffs).filter(([serviceKey]) => serviceKey === "squiz").map(([serviceKey, privileges], index) => {
return <Box key={index}> return (
<Box <Box key={serviceKey}>
sx={{ <Box
mb: "40px", sx={{
display: "flex", mb: "40px",
gap: "10px", display: "flex",
}} gap: "10px",
> }}
{!upMd && index === 0 && ( >
<IconButton {!upMd && index === 0 && (
onClick={handleCustomBackNavigation} <IconButton
sx={{ onClick={handleCustomBackNavigation}
p: 0, sx={{
height: "28px", p: 0,
width: "28px", height: "28px",
color: "black", width: "28px",
}} color: "black",
> }}
<ArrowBackIcon /> >
</IconButton> <ArrowBackIcon />
)} </IconButton>
<ComplexHeader text1="Мой тариф " text2={serviceNameByKey[serviceKey] === "Опросник" ? "PenaQuiz" : serviceNameByKey[serviceKey]} /> )}
</Box> <ComplexHeader text1="Мой тариф " text2={serviceNameByKey[serviceKey] === "Опросник" ? "PenaQuiz" : serviceNameByKey[serviceKey]} />
<CustomTariffCard serviceKey={serviceKey} privileges={privileges} /> </Box>
</Box> <CustomTariffCard serviceKey={serviceKey} privileges={privileges} />
})} </Box>
</Box> );
<Link })}
to="/tariffconstructor/savedtariffs" </Box>
style={{ <Link
display: "block", to="/tariffconstructor/savedtariffs"
marginTop: "50px", style={{
textUnderlinePosition: "under", display: "block",
color: theme.palette.purple.main, marginTop: "50px",
textDecorationColor: theme.palette.purple.main, textUnderlinePosition: "under",
}} color: theme.palette.purple.main,
> textDecorationColor: theme.palette.purple.main,
Ваши сохраненные тарифы }}
</Link> >
<TotalPrice priceBeforeDiscounts={basePrice} priceAfterDiscounts={discountedPrice} isConstructor={true} /> Ваши сохраненные тарифы
</SectionWrapper> </Link>
) <TotalPrice priceBeforeDiscounts={basePrice} priceAfterDiscounts={discountedPrice} isConstructor={true} />
</SectionWrapper>
);
} }
export default withErrorBoundary(TariffConstructor, { export default withErrorBoundary(TariffConstructor, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении моих тарифов</Typography>, fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении моих тарифов</Typography>,
onError: handleComponentError, onError: handleComponentError,
}) });

@ -1,173 +1,169 @@
import { Privilege, PrivilegeValueType, useThrottle } from "@frontend/kitui" import { CustomPrivilege, Privilege, useThrottle } from "@frontend/kitui";
import { Box, SliderProps, Typography, useMediaQuery, useTheme } from "@mui/material" import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
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";
import PieChartIcon from "@root/components/icons/PieChartIcon" import PieChartIcon from "@root/components/icons/PieChartIcon";
import { useCartStore } from "@root/stores/cart" import { setCustomTariffsUserValue, useCustomTariffsStore } from "@root/stores/customTariffs";
import { setCustomTariffsUserValue, useCustomTariffsStore } from "@root/stores/customTariffs" import { useUserStore } from "@root/stores/user";
import { useDiscountStore } from "@root/stores/discounts" import { getDeclension } from "@root/utils/declension";
import { useUserStore } from "@root/stores/user" import { useCartTariffs } from "@root/utils/hooks/useCartTariffs";
import { getDeclension } from "@root/utils/declension" import { useEffect, useState } from "react";
import { useEffect, useState } from "react" import { useDebouncedCallback } from "use-debounce";
import { useDebouncedCallback } from 'use-debounce';
const sliderSettingsByType = { const sliderSettingsByType = {
день: { max: 365, min: 0 }, день: { max: 365, min: 0 },
шаблон: { max: 5000, min: 0 }, шаблон: { max: 5000, min: 0 },
МБ: { max: 5000, min: 0 }, МБ: { max: 5000, min: 0 },
заявка: { max: 5000, min: 0 } заявка: { max: 5000, min: 0 }
} };
type PrivilegeName = "день" | "шаблон" | "МБ" | "заявка"
type PrivilegeName = "день" | "шаблон" | "МБ" | "заявка";
interface Props { interface Props {
privilege: Privilege; privilege: CustomPrivilege;
} }
export default function TariffPrivilegeSlider({ privilege }: Props) { export default function TariffPrivilegeSlider({ privilege }: Props) {
const theme = useTheme() const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")) const upMd = useMediaQuery(theme.breakpoints.up("md"));
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 discounts = useDiscountStore((state) => state.discounts) const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? sliderSettingsByType[privilege.value]?.min;
const currentCartTotal = useCartStore((state) => state.cart.priceAfterDiscounts) const cartTariffs = useCartTariffs();
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? sliderSettingsByType[privilege.value]?.min const [value, setValue] = useState<number>(userValue);
const isUserNko = useUserStore(state => state.userAccount?.status) === "nko" const throttledValue = useThrottle(value, 200);
const [value, setValue] = useState<number>(userValue)
const throttledValue = useThrottle(value, 200)
useEffect( useEffect(
function setStoreValue() { function setStoreValue() {
setCustomTariffsUserValue( setCustomTariffsUserValue(
privilege.serviceKey, cartTariffs ?? [],
privilege._id, privilege.serviceKey,
throttledValue, privilege._id,
discounts, throttledValue,
currentCartTotal, purchasesAmount,
purchasesAmount, );
isUserNko, },
) [cartTariffs, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue]
}, );
[currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue, isUserNko]
)
function handleSliderChange(measurement: PrivilegeName) { function handleSliderChange(measurement: PrivilegeName) {
return (value: number | number[]) => { return (value: number | number[]) => {
if (Number(value) < Number(sliderSettingsByType[measurement]?.min)) { if (Number(value) < Number(sliderSettingsByType[measurement]?.min)) {
setValue(0) setValue(0);
return return;
} }
if (Array.isArray(value)) throw new Error("Slider uses multiple values instead of one") if (Array.isArray(value)) throw new Error("Slider uses multiple values instead of one");
setValue(value) setValue(value);
} };
} }
const quantityText = `${value} ${getDeclension(value, privilege.value)}` const quantityText = `${value} ${getDeclension(value, privilege.value)}`;
const setNotSmallNumber = useDebouncedCallback(() => {
if (value === sliderSettingsByType[privilege.value]?.min) return
if (Number(value) < Number(sliderSettingsByType[privilege.value]?.min)) {
setValue(sliderSettingsByType[privilege.value]?.min)
}
if (privilege.value === "день" && Number(value) < 30 && Number(value) !== 0) {
setValue(30)
}
if (privilege.value !== "день" && Number(value) < 100 && Number(value) !== 0) { const setNotSmallNumber = useDebouncedCallback(() => {
setValue(100) if (value === sliderSettingsByType[privilege.value]?.min) return;
} if (Number(value) < Number(sliderSettingsByType[privilege.value]?.min)) {
}, 600) setValue(sliderSettingsByType[privilege.value]?.min);
}
if (privilege.value === "день" && Number(value) < 30 && Number(value) !== 0) {
setValue(30);
}
const quantityElement = ( if (privilege.value !== "день" && Number(value) < 100 && Number(value) !== 0) {
<Box setValue(100);
sx={{ }
display: "flex", }, 600);
gap: "15px",
alignItems: "center",
justifyContent: upMd ? "end" : undefined,
flexWrap: "wrap",
mt: upMd ? undefined : "12px",
}}
>
<Typography variant="p1" color={theme.palette.purple.main} textAlign="end">
{quantityText}
</Typography>
<Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
flexWrap: "wrap",
}}
>
<Typography sx={{ fontSize: "16px", lineHeight: "19px", mt: "1px" }}>или</Typography>
<NumberInputWithUnitAdornment
id={"privilege_input_" + privilege._id}
value={value}
privilege={privilege}
adornmentText={getDeclension(0, privilege.value)}
onChange={(value) => {
setValue(value)
setNotSmallNumber()
}}
/>
</Box>
</Box>
)
const icon = const quantityElement = (
privilege.type === "day" ? ( <Box
<CalendarIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" /> sx={{
) : ( display: "flex",
<PieChartIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" /> gap: "15px",
) alignItems: "center",
justifyContent: upMd ? "end" : undefined,
flexWrap: "wrap",
mt: upMd ? undefined : "12px",
}}
>
<Typography variant="p1" color={theme.palette.purple.main} textAlign="end">
{quantityText}
</Typography>
<Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
flexWrap: "wrap",
}}
>
<Typography sx={{ fontSize: "16px", lineHeight: "19px", mt: "1px" }}>или</Typography>
<NumberInputWithUnitAdornment
id={"privilege_input_" + privilege._id}
value={value}
privilege={privilege}
adornmentText={getDeclension(0, privilege.value)}
onChange={(value) => {
setValue(value);
setNotSmallNumber();
}}
/>
</Box>
</Box>
);
return ( const icon =
<Box> privilege.type === "day" ? (
<Typography sx={{ color: theme.palette.gray.dark, mb: "auto" }}>{privilege.description}</Typography> <CalendarIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />
<Box ) : (
sx={{ <PieChartIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />
display: "flex", );
flexDirection: "column",
mt: "40px", return (
}} <Box>
> <Typography sx={{ color: theme.palette.gray.dark, mb: "auto" }}>{privilege.description}</Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
// flexWrap: "wrap", flexDirection: "column",
alignItems: "center", mt: "40px",
mb: "8px", }}
justifyContent: "space-between", >
gap: "10px", <Box
}} sx={{
> display: "flex",
<Box // flexWrap: "wrap",
sx={{ alignItems: "center",
display: "flex", mb: "8px",
alignItems: "center", justifyContent: "space-between",
gap: "22px", gap: "10px",
}} }}
> >
{icon} <Box
<Typography sx={{ maxWidth: "100px" }} variant="h5"> sx={{
{privilege.name} display: "flex",
</Typography> alignItems: "center",
</Box> gap: "22px",
{upMd && quantityElement} }}
</Box> >
<CustomSlider {icon}
value={value} <Typography sx={{ maxWidth: "100px" }} variant="h5">
min={sliderSettingsByType[privilege.value]?.min } {privilege.name}
max={sliderSettingsByType[privilege.value]?.max || 100} </Typography>
onChange={handleSliderChange(privilege.value)} </Box>
firstStep={privilege.value === "день" ? 30 : 100} {upMd && quantityElement}
/> </Box>
{!upMd && quantityElement} <CustomSlider
</Box> value={value}
</Box> min={sliderSettingsByType[privilege.value]?.min}
) max={sliderSettingsByType[privilege.value]?.max || 100}
onChange={handleSliderChange(privilege.value)}
firstStep={privilege.value === "день" ? 30 : 100}
/>
{!upMd && quantityElement}
</Box>
</Box>
);
} }

@ -1,10 +1,8 @@
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import { getMessageFromFetchError } from "@frontend/kitui"; 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 { FullTariff } from "@root/model/tariff";
import { useCartStore } from "@root/stores/cart";
import { useDiscountStore } from "@root/stores/discounts"; 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";
@ -12,7 +10,7 @@ import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { handleComponentError } from "@root/utils/handleComponentError"; import { handleComponentError } from "@root/utils/handleComponentError";
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"; import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
import { useRecentlyPurchasedTariffs } from "@utils/hooks/useRecentlyPurchasedTariffs"; import { useCartTariffs } from "@root/utils/hooks/useCartTariffs";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useState } from "react"; import { useState } from "react";
import { withErrorBoundary } from "react-error-boundary"; import { withErrorBoundary } from "react-error-boundary";
@ -39,14 +37,12 @@ function TariffPage() {
const [selectedItem, setSelectedItem] = useState<number>(0); const [selectedItem, setSelectedItem] = useState<number>(0);
const discounts = useDiscountStore((state) => state.discounts); const discounts = useDiscountStore((state) => state.discounts);
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0; const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0;
const cartTariffMap = useCartStore((state) => state.cartTariffMap);
const isUserNko = useUserStore((state) => state.userAccount?.status) === "nko"; const isUserNko = useUserStore((state) => state.userAccount?.status) === "nko";
const currentTariffs = useCartTariffs();
const { recentlyPurchased } = useRecentlyPurchasedTariffs();
const handleCustomBackNavigation = usePrevLocation(location); const handleCustomBackNavigation = usePrevLocation(location);
const unit: string = String(location.pathname).slice(9); const unit: string = String(location.pathname).slice(9);
const currentTariffs = Object.values(cartTariffMap).filter((tariff): tariff is FullTariff => typeof tariff === "object");
function handleTariffItemClick(tariffId: string) { function handleTariffItemClick(tariffId: string) {
addTariffToCart(tariffId) addTariffToCart(tariffId)
@ -72,7 +68,8 @@ function TariffPage() {
return false return false
}); });
const createTariffElements = (filteredTariffs: FullTariff[], addFreeTariff = false) => { const createTariffElements = (filteredTariffs: Tariff[], addFreeTariff = false) => {
console.log(filteredTariffs)
const tariffElements = filteredTariffs const tariffElements = filteredTariffs
.filter((tariff) => tariff.privileges.length > 0) .filter((tariff) => tariff.privileges.length > 0)
.map((tariff, index) => { .map((tariff, index) => {
@ -80,7 +77,7 @@ function TariffPage() {
tariff, tariff,
discounts, discounts,
purchasesAmount, purchasesAmount,
currentTariffs, currentTariffs ?? [],
isUserNko isUserNko
); );

@ -1,95 +1,22 @@
import { CartData, Discount, Tariff } from "@frontend/kitui" import { create } from "zustand";
import { calcCart } from "@root/utils/calcCart/calcCart" import { devtools } from "zustand/middleware";
import { produce } from "immer"
import { create } from "zustand"
import { devtools } from "zustand/middleware"
interface CartStore { interface CartStore {
cartTariffMap: Record<string, Tariff | "loading" | "not found">; notEnoughMoneyAmount: number;
cart: CartData;
isDrawerOpen: boolean;
notEnoughMoneyAmount: number;
} }
export const useCartStore = create<CartStore>()( export const useCartStore = create<CartStore>()(
devtools( devtools(
(get, set) => ({ (get, set) => ({
cartTariffMap: {},
cart: {
services: [],
priceBeforeDiscounts: 0,
priceAfterDiscounts: 0,
itemCount: 0,
allAppliedDiscounts: [],
appliedCartPurchasesDiscount: null,
appliedLoyaltyDiscount: null,
},
isDrawerOpen: false,
notEnoughMoneyAmount: 0, notEnoughMoneyAmount: 0,
}), }),
{ {
name: "Cart", name: "Cart",
enabled: process.env.NODE_ENV === "development", enabled: process.env.NODE_ENV === "development",
trace: true, trace: true,
actionsBlacklist: "rejected", actionsBlacklist: "rejected",
} }
) )
) );
export const setCartTariffStatus = (tariffId: string, status: "loading" | "not found") => export const setNotEnoughMoneyAmount = (amount: number) => useCartStore.setState({ notEnoughMoneyAmount: amount });
useCartStore.setState(
produce<CartStore>((state) => {
state.cartTariffMap[tariffId] = status
}),
false,
{
type: "setCartTariffStatus",
tariffId,
status,
}
)
export const addCartTariffs = (tariffs: Tariff[], discounts: Discount[], purchasesAmount: number, isUserNko: boolean) =>
useCartStore.setState(
produce<CartStore>((state) => {
tariffs.forEach((tariff) => {
// if (tariff.isCustom && tariff.privileges[0].serviceKey !== "custom") {
// tariff.privileges[0].serviceKey = "custom"
// }
//
state.cartTariffMap[tariff._id] = tariff
})
const cartTariffs = Object.values(state.cartTariffMap).filter(
(tariff): tariff is Tariff => typeof tariff === "object"
)
state.cart = calcCart(cartTariffs, discounts, purchasesAmount, isUserNko)
}),
false,
{
type: tariffs.length > 0 ? "addCartTariffs" : "rejected",
tariffs,
}
)
export const removeMissingCartTariffs = (tariffIds: string[], discounts: Discount[], purchasesAmount: number, isUserNko: boolean) =>
useCartStore.setState(
produce<CartStore>((state) => {
for (const key in state.cartTariffMap) {
if (!tariffIds.includes(key)) delete state.cartTariffMap[key]
}
const cartTariffs = Object.values(state.cartTariffMap).filter(
(tariff): tariff is Tariff => typeof tariff === "object"
)
state.cart = calcCart(cartTariffs, discounts, purchasesAmount, isUserNko)
}),
false,
{
type: "rejected",
}
)
export const openCartDrawer = () => useCartStore.setState({ isDrawerOpen: true })
export const closeCartDrawer = () => useCartStore.setState({ isDrawerOpen: false })
export const setNotEnoughMoneyAmount = (amount: number) => useCartStore.setState({ notEnoughMoneyAmount: amount })

@ -1,13 +1,13 @@
import { createTariff } from "@root/api/tariff" import { CustomPrivilegeWithAmount, Tariff } from "@frontend/kitui";
import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs" import { createTariff } from "@root/api/tariff";
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege" import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs";
import { produce } from "immer" import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
import { create } from "zustand" import { calcCustomTariffPrice } from "@root/utils/calcCart/calcCustomTariffPrice";
import { devtools, persist } from "zustand/middleware" import { produce } from "immer";
import { Discount, PrivilegeWithAmount/* , findCartDiscount */, findDiscountFactor/* , findLoyaltyDiscount *//* , findPrivilegeDiscount *//* , findServiceDiscount */} from "@frontend/kitui" import { create } from "zustand";
import { findNkoDiscount, calcCart } from "@root/utils/calcCart/calcCart" import { devtools, persist } from "zustand/middleware";
import { useCartStore } from "./cart" import { useDiscountStore } from "./discounts";
import { FullTariff } from "@root/model/tariff" import { useUserStore } from "./user";
interface CustomTariffsStore { interface CustomTariffsStore {
@ -18,229 +18,90 @@ interface CustomTariffsStore {
} }
const initialState: CustomTariffsStore = { const initialState: CustomTariffsStore = {
privilegeByService: {}, privilegeByService: {},
userValuesMap: {}, userValuesMap: {},
summaryPriceBeforeDiscountsMap: {}, summaryPriceBeforeDiscountsMap: {},
summaryPriceAfterDiscountsMap: {}, summaryPriceAfterDiscountsMap: {},
} };
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;
}
function findServiceDiscount(
serviceKey: string,
currentPrice: number,
discounts: Discount[],
): Discount | null {
const applicableDiscounts = discounts.filter(discount => {
return (
discount.Layer === 2 &&
discount.Condition.Group === serviceKey &&
currentPrice >= Number(discount.Condition.PriceFrom)
);
});
if (!applicableDiscounts.length) return null;
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
return Number(current.Condition.PriceFrom) > Number(prev.Condition.PriceFrom) ? current : prev;
});
return maxValueDiscount;
}
export const useCustomTariffsStore = create<CustomTariffsStore>()( export const useCustomTariffsStore = create<CustomTariffsStore>()(
persist( persist(
devtools( devtools(
(set, get) => initialState, (set, get) => initialState,
{ {
name: "Custom tariffs", name: "Custom tariffs",
enabled: process.env.NODE_ENV === "development", enabled: process.env.NODE_ENV === "development",
}), }),
{ {
name: "customTariffs", name: "customTariffs",
partialize: state => ({ partialize: state => ({
userValuesMap: state.userValuesMap, userValuesMap: state.userValuesMap,
summaryPriceBeforeDiscountsMap: state.summaryPriceBeforeDiscountsMap, summaryPriceBeforeDiscountsMap: state.summaryPriceBeforeDiscountsMap,
summaryPriceAfterDiscountsMap: state.summaryPriceAfterDiscountsMap, summaryPriceAfterDiscountsMap: state.summaryPriceAfterDiscountsMap,
}), }),
migrate: () => initialState, migrate: () => initialState,
} }
) )
) );
function findPrivilegeDiscount( export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ privilegeByService: customTariffs });
privilegeId: string,
privilegePrice: number,
discounts: Discount[],
): Discount | null {
const applicableDiscounts = discounts.filter(discount => {
const conditionMinPrice = parseFloat(discount.Condition.Term);
if (!isFinite(conditionMinPrice)) throw new Error(`Couldn't parse Discount.Condition.Term: ${discount.Condition.Term}`);
return (
discount.Layer === 1 &&
privilegeId === discount.Condition.Product &&
privilegePrice >= conditionMinPrice
);
});
if (!applicableDiscounts.length) return null;
const maxValueDiscount = applicableDiscounts.reduce((prev, current) =>
parseFloat(current.Condition.Term) > parseFloat(prev.Condition.Term) ? current : prev
);
return maxValueDiscount;
}
export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ privilegeByService: customTariffs })
function findCartDiscount(
cartPurchasesAmount: number,
discounts: Discount[],
): Discount | null {
const applicableDiscounts = discounts.filter(discount => {
return (
discount.Layer === 3 &&
cartPurchasesAmount >= 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 const setCustomTariffsUserValue = ( export const setCustomTariffsUserValue = (
serviceKey: string, cartTariffs: Tariff[],
privilegeId: string, serviceKey: string,
value: number, privilegeId: string,
discounts: Discount[], value: number,
currentCartTotal: number, purchasesAmount: number,
purchasesAmount: number,
isUserNko: boolean,
) => useCustomTariffsStore.setState( ) => useCustomTariffsStore.setState(
produce<CustomTariffsStore>(state => { produce<CustomTariffsStore>(state => {
state.userValuesMap[serviceKey] ??= {} state.userValuesMap[serviceKey] ??= {};
state.userValuesMap[serviceKey][privilegeId] = value state.userValuesMap[serviceKey][privilegeId] = value;
const isUserNko = useUserStore.getState().userAccount?.status === "nko";
const discounts = useDiscountStore.getState().discounts;
const privilegeWithoutAmount = state.privilegeByService[serviceKey].find(p => p._id === privilegeId) const { priceBeforeDiscounts, priceAfterDiscounts } = calcCustomTariffPrice(
if (!privilegeWithoutAmount) throw new Error(`Privilege not found: ${privilegeId}`) state.userValuesMap[serviceKey],
let priceWithoutDiscounts = 0 state.privilegeByService[serviceKey],
let priceAfterDiscounts = 0 cartTariffs,
const privileges = new Array<PrivilegeWithAmount>() discounts,
purchasesAmount,
isUserNko
);
Object.keys(state.userValuesMap[serviceKey]).forEach(e => { state.summaryPriceBeforeDiscountsMap[serviceKey] = priceBeforeDiscounts;
const pwa = state.privilegeByService[serviceKey].find(p => p._id === e) state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
if (!pwa) return
if (state.userValuesMap[serviceKey][e] > 0)
privileges.push({
...pwa,
amount: state.userValuesMap[serviceKey][e]
})
}) })
);
let tariff: FullTariff = {
_id: "",
name: "",
price: 0,
description: "",
isCustom: true,
isDeleted: false,
privileges: privileges,
}
const cart = calcCart([...Object.values(useCartStore.getState().cartTariffMap as Record<string, FullTariff>), tariff], discounts, purchasesAmount, isUserNko)
const nkoDiscount = findNkoDiscount(discounts)
if (isUserNko && nkoDiscount) {
state.privilegeByService[serviceKey].forEach(privilege => {
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0
priceWithoutDiscounts += privilege.price * amount
})
priceAfterDiscounts = priceWithoutDiscounts * findDiscountFactor(nkoDiscount)
} else {
state.privilegeByService[serviceKey].forEach(privilege => {
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0
priceWithoutDiscounts += privilege.price * amount
const discount = cart.allAppliedDiscounts?.find(e => e.Condition.Product === privilege.privilegeId)
//const discount = findPrivilegeDiscount(privilege.privilegeId, amount, discounts)
priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount)
})
const serviceDiscount = cart.allAppliedDiscounts?.find(e => e.Condition.Group === serviceKey)
priceAfterDiscounts *= findDiscountFactor(serviceDiscount)
const cartDiscount = findCartDiscount(currentCartTotal+priceAfterDiscounts, discounts)
priceAfterDiscounts *= findDiscountFactor(cartDiscount)
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts)
priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount)
}
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts
})
)
export const createAndSendTariff = (serviceKey: string) => { export const createAndSendTariff = (serviceKey: string) => {
const state = useCustomTariffsStore.getState() const state = useCustomTariffsStore.getState();
const privileges: PrivilegeWithAmount[] = [] const privileges: CustomPrivilegeWithAmount[] = [];
Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => { Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => {
if (userValue === 0) return if (userValue === 0) return;
const privilegeWithoutAmount = state.privilegeByService[serviceKey].find(p => p._id === privilegeId) const privilegeWithoutAmount = state.privilegeByService[serviceKey].find(p => p._id === privilegeId);
if (!privilegeWithoutAmount) throw new Error(`Privilege not found: ${privilegeId}`) if (!privilegeWithoutAmount) throw new Error(`Privilege not found: ${privilegeId}`);
const privilege: PrivilegeWithAmount = { const privilege: CustomPrivilegeWithAmount = {
...privilegeWithoutAmount, ...privilegeWithoutAmount,
privilegeId: privilegeWithoutAmount.privilegeId, privilegeId: privilegeWithoutAmount.privilegeId,
amount: userValue, amount: userValue,
} };
privileges.push(privilege) privileges.push(privilege);
}) });
const name = [...privileges.map(p => p.name), new Date().toISOString()].join(", ") const name = [...privileges.map(p => p.name), new Date().toISOString()].join(", ");
const tariff = { const tariff = {
name, name,
isCustom: true, isCustom: true,
privileges, privileges,
} };
return createTariff(tariff) return createTariff(tariff);
} };
export const clearCustomTariffs = () => useCustomTariffsStore.setState({ ...initialState }) export const clearCustomTariffs = () => useCustomTariffsStore.setState({ ...initialState });

@ -1,10 +1,10 @@
import { PrivilegeWithAmount } from "@frontend/kitui" import { Privilege } from "@frontend/kitui";
import { create } from "zustand" import { create } from "zustand"
import { devtools } from "zustand/middleware" import { devtools } from "zustand/middleware"
interface PrivilegeStore { interface PrivilegeStore {
privileges: PrivilegeWithAmount[]; privileges: Privilege[];
} }
const initialState: PrivilegeStore = { const initialState: PrivilegeStore = {

@ -108,7 +108,7 @@ export const updateTicket = <T extends AuthData>(
} }
); );
function setProducedState<A extends string | { type: unknown }>( function setProducedState<A extends string | { type: string }>(
recipe: (state: TicketStore) => void, recipe: (state: TicketStore) => void,
action?: A action?: A
) { ) {

@ -14,6 +14,7 @@ import { patchUser } from "@root/api/user"
import { UserAccountSettingsFieldStatus, VerificationStatus } from "@root/model/account" import { UserAccountSettingsFieldStatus, VerificationStatus } from "@root/model/account"
import { patchCurrency, deleteCart, patchCart } from "@root/api/cart" import { patchCurrency, deleteCart, patchCart } from "@root/api/cart"
import { User, UserAccount, UserName, getInitials, patchUserAccount } from "@frontend/kitui" import { User, UserAccount, UserName, getInitials, patchUserAccount } from "@frontend/kitui"
import { setNotEnoughMoneyAmount } from "./cart";
interface UserStore { interface UserStore {
userId: string | null; userId: string | null;
@ -286,6 +287,7 @@ export const addTariffToCart = async (tariffId: string) => {
} }
export const removeTariffFromCart = async (tariffId: string) => { export const removeTariffFromCart = async (tariffId: string) => {
setNotEnoughMoneyAmount(0);
const [deleteCartResponse, deleteCartError] = await deleteCart(tariffId) const [deleteCartResponse, deleteCartError] = await deleteCart(tariffId)
if (!deleteCartError) { if (!deleteCartError) {

File diff suppressed because it is too large Load Diff

@ -1,149 +1,51 @@
import { CartData, Discount, PrivilegeCartData, Tariff, TariffCartData, findPrivilegeDiscount, findDiscountFactor, applyLoyaltyDiscount} from "@frontend/kitui"; import {
CartData,
Discount,
PrivilegeCartData,
Tariff,
TariffCartData,
findCartDiscount,
findDiscountFactor,
findLoyaltyDiscount,
findNkoDiscount,
findPrivilegeDiscount,
findServiceDiscount,
} from "@frontend/kitui";
function applyPrivilegeDiscounts( export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number, isUserNko?: boolean, userId: string = ""): CartData {
cartData: CartData,
discounts: Discount[],
) {
const privMap = new Map()
cartData.services.forEach(service => service.tariffs.forEach(tariff => tariff.privileges.forEach(p => {
privMap.set(p.privilegeId, p.amount + (privMap.get(p.privilegeId)||0))
})))
cartData.services.forEach(service => {
service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => {
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privMap.get(privilege.privilegeId)||0, discounts);
if (!privilegeDiscount) return;
const discountAmount = privilege.price * (1 - findDiscountFactor(privilegeDiscount));
privilege.price -= discountAmount;
cartData.allAppliedDiscounts.push(privilegeDiscount);
privilege.appliedPrivilegeDiscount = privilegeDiscount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
});
});
});
}
function findServiceDiscount(
serviceKey: string,
currentPrice: number,
discounts: Discount[],
): Discount | null {
const applicableDiscounts = discounts.filter(discount => {
return (
discount.Layer === 2 &&
discount.Condition.Group === serviceKey &&
currentPrice >= Number(discount.Condition.PriceFrom)
);
});
if (!applicableDiscounts.length) return null;
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
return Number(current.Condition.PriceFrom) > Number(prev.Condition.PriceFrom) ? current : prev;
});
return maxValueDiscount;
}
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;
}
function applyCartDiscount(
cartData: CartData,
discounts: Discount[],
) {
const cartDiscount = findCartDiscount(cartData.priceAfterDiscounts, discounts);
if (!cartDiscount) return;
cartData.priceAfterDiscounts *= findDiscountFactor(cartDiscount);
cartData.allAppliedDiscounts.push(cartDiscount);
cartData.appliedCartPurchasesDiscount = cartDiscount;
}
function applyServiceDiscounts(
cartData: CartData,
discounts: Discount[],
) {
const privMap = new Map()
cartData.services.forEach(service => {
service.tariffs.forEach(tariff => tariff.privileges.forEach(p => {
privMap.set(p.serviceKey, p.price + (privMap.get(p.serviceKey)||0))
}))
})
cartData.services.forEach(service => {
service.tariffs.map(tariff => {
tariff.privileges.forEach(privilege => {
const privilegeDiscount = findServiceDiscount(privilege.serviceKey, privMap.get(privilege.serviceKey), discounts);
if (!privilegeDiscount) return;
const discountAmount = privilege.price * (1 - findDiscountFactor(privilegeDiscount));
privilege.price -= discountAmount;
cartData.allAppliedDiscounts.push(privilegeDiscount);
service.appliedServiceDiscount = privilegeDiscount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
});
});
});
}
export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number, isUserNko?: boolean): CartData {
const cartData: CartData = { const cartData: CartData = {
services: [], services: [],
priceBeforeDiscounts: 0, priceBeforeDiscounts: 0,
priceAfterDiscounts: 0, priceAfterDiscounts: 0,
itemCount: 0,
appliedCartPurchasesDiscount: null, appliedCartPurchasesDiscount: null,
appliedLoyaltyDiscount: null, appliedLoyaltyDiscount: null,
allAppliedDiscounts: [], allAppliedDiscounts: [],
} appliedDiscountsByPrivilegeId: new Map(),
};
const privilegeAmountById = new Map<string, number>();
const servicePriceByKey = new Map<string, number>();
const allAppliedDiscounts = new Set<Discount>();
// Формируем корзину
tariffs.forEach(tariff => { tariffs.forEach(tariff => {
if (tariff.privileges === undefined) return if (tariff.privileges === undefined) return;
if ( if (
(tariff.price || 0) > 0 (tariff.price || 0) > 0
&& tariff.privileges.length !== 1 && tariff.privileges.length !== 1
) throw new Error("Price is defined for tariff with several") ) throw new Error("Price is defined for tariff with several privileges");
let serviceData =cartData.services.find(service => (service.serviceKey === "custom" && tariff.isCustom)) let serviceData = cartData.services.find(service => (service.serviceKey === "custom" && tariff.isCustom));
if (!serviceData && !tariff.isCustom) serviceData = cartData.services.find(service => service.serviceKey === tariff.privileges[0]?.serviceKey) if (!serviceData && !tariff.isCustom) serviceData = cartData.services.find(service => service.serviceKey === tariff.privileges[0]?.serviceKey);
if (!serviceData) { if (!serviceData) {
serviceData = { serviceData = {
serviceKey: tariff.isCustom ?"custom" :tariff.privileges[0]?.serviceKey, serviceKey: tariff.isCustom ? "custom" : tariff.privileges[0]?.serviceKey,
tariffs: [], tariffs: [],
price: 0, price: 0,
appliedServiceDiscount: null, appliedServiceDiscount: null,
} };
cartData.services.push(serviceData) cartData.services.push(serviceData);
} }
const tariffCartData: TariffCartData = { const tariffCartData: TariffCartData = {
@ -152,13 +54,12 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
privileges: [], privileges: [],
id: tariff._id, id: tariff._id,
name: tariff.name, name: tariff.name,
} };
serviceData.tariffs.push(tariffCartData) serviceData.tariffs.push(tariffCartData);
tariff.privileges.forEach(privilege => { tariff.privileges.forEach(privilege => {
let privilegePrice = privilege.amount * privilege.price let privilegePrice = privilege.amount * privilege.price;
if (!tariff.price) tariffCartData.price += privilegePrice if (tariff.price) privilegePrice = tariff.price;
else privilegePrice = tariff.price
const privilegeCartData: PrivilegeCartData = { const privilegeCartData: PrivilegeCartData = {
serviceKey: privilege.serviceKey, serviceKey: privilege.serviceKey,
@ -166,46 +67,158 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
description: privilege.description, description: privilege.description,
price: privilegePrice, price: privilegePrice,
amount: privilege.amount, amount: privilege.amount,
appliedPrivilegeDiscount: null, };
}
tariffCartData.privileges.push(privilegeCartData) privilegeAmountById.set(
cartData.priceAfterDiscounts += privilegePrice privilege.privilegeId,
cartData.itemCount++ privilege.amount + (privilegeAmountById.get(privilege.privilegeId) || 0)
}) );
servicePriceByKey.set(
privilege.serviceKey,
privilegePrice + (servicePriceByKey.get(privilege.serviceKey) || 0)
);
cartData.appliedDiscountsByPrivilegeId.set(privilege.privilegeId, new Set());
cartData.priceBeforeDiscounts += tariffCartData.price tariffCartData.privileges.push(privilegeCartData);
serviceData.price += tariffCartData.price });
}) });
const nkoDiscount = findNkoDiscount(discounts) cartData.priceBeforeDiscounts = Array.from(servicePriceByKey.values()).reduce((a, b) => a + b, 0);
const nkoDiscount = findNkoDiscount(discounts);
if (isUserNko && nkoDiscount) { if (isUserNko && nkoDiscount) {
applyNkoDiscount(cartData, nkoDiscount) cartData.priceAfterDiscounts *= nkoDiscount.Target.Factor;
} else { allAppliedDiscounts.add(nkoDiscount);
applyPrivilegeDiscounts(cartData, discounts)
applyServiceDiscounts(cartData, discounts) cartData.services.forEach(service => {
applyCartDiscount(cartData, discounts) service.tariffs.forEach(tariff => {
applyLoyaltyDiscount(cartData, discounts, purchasesAmount) tariff.privileges.forEach(privilege => {
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId);
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
privilegeAppliedDiscounts.add(nkoDiscount);
});
});
});
applyDiscountsToCart(cartData);
cartData.allAppliedDiscounts = Array.from(allAppliedDiscounts);
return cartData;
} }
cartData.allAppliedDiscounts = Array.from(new Set(cartData.allAppliedDiscounts)) // Ищем и собираем скидки в appliedDiscountsByPrivilegeId
return cartData cartData.services.forEach(service => {
service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => {
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilegeAmountById.get(privilege.privilegeId) || 0, discounts, userId);
if (!privilegeDiscount) return;
allAppliedDiscounts.add(privilegeDiscount);
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId);
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
privilegeAppliedDiscounts.add(privilegeDiscount);
});
});
});
cartData.services.forEach(service => {
service.tariffs.map(tariff => {
tariff.privileges.forEach(privilege => {
const servicePrice = servicePriceByKey.get(privilege.serviceKey);
if (!servicePrice) return;
const serviceDiscount = findServiceDiscount(privilege.serviceKey, servicePrice, discounts, userId);
if (!serviceDiscount) return;
allAppliedDiscounts.add(serviceDiscount);
service.appliedServiceDiscount = serviceDiscount;
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId);
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
privilegeAppliedDiscounts.add(serviceDiscount);
});
});
});
const intermediateCartData = structuredClone(cartData);
applyDiscountsToCart(intermediateCartData);
const intermediateCartPriceAfterDiscounts = intermediateCartData.priceAfterDiscounts;
const cartDiscount = findCartDiscount(intermediateCartPriceAfterDiscounts, discounts);
if (cartDiscount) {
cartData.services.forEach(service => {
service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => {
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId);
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
if (!Array.from(privilegeAppliedDiscounts)[0]?.Condition.User) privilegeAppliedDiscounts.add(cartDiscount);
});
});
});
allAppliedDiscounts.add(cartDiscount);
cartData.appliedCartPurchasesDiscount = cartDiscount;
}
const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
if (loyalDiscount) {
cartData.services.forEach(service => {
service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => {
const privilegeAppliedDiscounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId);
if (!privilegeAppliedDiscounts) throw new Error(`Privilege id ${privilege.privilegeId} not found in appliedDiscountsByPrivilegeId`);
if (!Array.from(privilegeAppliedDiscounts)[0]?.Condition.User) privilegeAppliedDiscounts.add(loyalDiscount);
});
});
});
allAppliedDiscounts.add(loyalDiscount);
cartData.appliedLoyaltyDiscount = loyalDiscount;
}
// Применяем скидки
applyDiscountsToCart(cartData);
cartData.allAppliedDiscounts = Array.from(allAppliedDiscounts);
return Object.freeze(cartData);
} }
function applyNkoDiscount(cartData: CartData, discount: Discount) { function applyDiscountsToCart(cartData: CartData) {
cartData.priceAfterDiscounts *= discount.Target.Factor cartData.services.forEach(service => {
cartData.allAppliedDiscounts.push(discount) let servicePrice = 0;
}
export function findNkoDiscount(discounts: Discount[]): Discount | null { service.tariffs.forEach(tariff => {
const applicableDiscounts = discounts.filter(discount => discount.Condition.UserType === "nko") let privilegePriceSum = 0;
let tariffDiscountFactor = 1;
if (!applicableDiscounts.length) return null tariff.privileges.forEach(privilege => {
const discounts = cartData.appliedDiscountsByPrivilegeId.get(privilege.privilegeId) ?? [];
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => { const discountsFactor = Array.from(discounts).reduce((factor, discount) => factor * findDiscountFactor(discount), 1);
return current.Condition.CartPurchasesAmount > prev.Condition.CartPurchasesAmount ? current : prev privilege.price *= discountsFactor;
}) tariffDiscountFactor *= discountsFactor;
return maxValueDiscount privilegePriceSum += privilege.price;
} });
if (tariff.price) tariff.price *= tariffDiscountFactor;
else tariff.price = privilegePriceSum;
servicePrice += tariff.price;
});
service.price = servicePrice;
cartData.priceAfterDiscounts += servicePrice;
});
}

@ -0,0 +1,49 @@
import { CustomPrivilegeWithAmount, Discount, Tariff } from "@frontend/kitui";
import { CustomTariffUserValues } from "@root/model/customTariffs";
import { calcCart } from "./calcCart";
export function calcCustomTariffPrice(
customTariffUserValues: CustomTariffUserValues,
servicePrivileges: CustomPrivilegeWithAmount[],
cartTariffs: Tariff[],
discounts: Discount[],
purchasesAmount: number,
isUserNko: boolean,
) {
const privileges = new Array<CustomPrivilegeWithAmount>();
const priceBeforeDiscounts = servicePrivileges.reduce((price, privilege) => {
const amount = customTariffUserValues?.[privilege._id] ?? 0;
return price + privilege.price * amount;
}, 0);
Object.keys(customTariffUserValues).forEach(privilegeId => {
const pwa = servicePrivileges.find(p => p._id === privilegeId);
if (!pwa) return;
if (customTariffUserValues[privilegeId] > 0) privileges.push({
...pwa,
amount: customTariffUserValues[privilegeId]
});
});
const customTariff: Tariff = {
_id: crypto.randomUUID(),
name: "",
price: 0,
description: "",
isCustom: true,
isDeleted: false,
privileges: privileges,
};
const cart = calcCart([...cartTariffs, customTariff], discounts, purchasesAmount, isUserNko);
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");
return {
priceBeforeDiscounts,
priceAfterDiscounts: customService.price,
};
}

@ -0,0 +1,900 @@
import { Discount } from "@frontend/kitui";
export const testDiscounts: Discount[] = [
{
"ID": "6521d98b166f36879928ebbf",
"Name": "NKO",
"Layer": 4,
"Description": "скидка ветеранам НКО",
"Condition": {
"Period": {
"From": "2023-10-07T21:00:45.829Z",
"To": "2023-11-06T21:00:45.829Z"
},
"User": "",
"UserType": "nko",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [],
"Factor": 0.4,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": true
},
"Audit": {
"UpdatedAt": "2023-11-10T23:20:32.619Z",
"CreatedAt": "2023-09-16T20:10:26.048Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657b9b73153787e41052c25b",
"Name": "1000 шаблонов",
"Layer": 1,
"Description": "Тариф на 1000 шаблонов",
"Condition": {
"Period": {
"From": "2023-12-15T00:18:57.999Z",
"To": "2024-01-14T00:18:58Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "templateCnt",
"Term": "1000",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "templateCnt",
"Factor": 0.7,
"Overhelm": false
}
],
"Factor": 0.7,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-15T00:18:59.138Z",
"CreatedAt": "2023-12-15T00:18:59.138Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657b9cb0153787e41052c25c",
"Name": "десять тысяч шаблонов",
"Layer": 1,
"Description": "Тариф 10 000",
"Condition": {
"Period": {
"From": "2023-12-15T00:24:15.555Z",
"To": "2024-01-14T00:24:15.555Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "templateCnt",
"Term": "10000",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "templateCnt",
"Factor": 0.5,
"Overhelm": false
}
],
"Factor": 0.5,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-15T00:24:16.562Z",
"CreatedAt": "2023-12-15T00:24:16.562Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657b9e67153787e41052c25d",
"Name": "3 месяца",
"Layer": 1,
"Description": "Тариф 3 месяца",
"Condition": {
"Period": {
"From": "2023-12-15T00:31:34.807Z",
"To": "2024-01-14T00:31:34.807Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "templateUnlimTime",
"Term": "90",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "templateUnlimTime",
"Factor": 0.8,
"Overhelm": false
}
],
"Factor": 0.8,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-15T00:31:35.601Z",
"CreatedAt": "2023-12-15T00:31:35.601Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657b9e8a153787e41052c25e",
"Name": "год",
"Layer": 1,
"Description": "Тариф год",
"Condition": {
"Period": {
"From": "2023-12-15T00:32:09.329Z",
"To": "2024-01-14T00:32:09.329Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "templateUnlimTime",
"Term": "365",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "templateUnlimTime",
"Factor": 0.65,
"Overhelm": false
}
],
"Factor": 0.65,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-15T00:32:10.123Z",
"CreatedAt": "2023-12-15T00:32:10.123Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657b9eb4153787e41052c25f",
"Name": "3 года",
"Layer": 1,
"Description": "Тариф 3 года",
"Condition": {
"Period": {
"From": "2023-12-15T00:32:51.379Z",
"To": "2024-01-14T00:32:51.379Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "templateUnlimTime",
"Term": "1095",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "templateUnlimTime",
"Factor": 0.5,
"Overhelm": false
}
],
"Factor": 0.5,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-15T00:32:52.174Z",
"CreatedAt": "2023-12-15T00:32:52.174Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657f5028153787e41052c266",
"Name": "10т.р",
"Layer": 4,
"Description": "купил больше чем на 10 тыров",
"Condition": {
"Period": {
"From": "2023-12-17T19:48:47.466Z",
"To": "2024-01-16T19:48:47.466Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "10000",
"CartPurchasesAmount": "0",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.98,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-17T19:48:46.072Z",
"CreatedAt": "2023-12-17T19:46:48.854Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657f50c3153787e41052c268",
"Name": "1т.р",
"Layer": 4,
"Description": "купил больше чем на 1 тыр",
"Condition": {
"Period": {
"From": "2023-12-17T19:49:24.782Z",
"To": "2024-01-16T19:49:24.782Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "100000",
"CartPurchasesAmount": "0",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.99,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-17T19:49:23.384Z",
"CreatedAt": "2023-12-17T19:49:23.384Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657f50e4153787e41052c269",
"Name": "100т.р",
"Layer": 4,
"Description": "купил больше чем на 100 тыров",
"Condition": {
"Period": {
"From": "2023-12-17T19:49:57.462Z",
"To": "2024-01-16T19:49:57.462Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "10000000",
"CartPurchasesAmount": "0",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.95,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-17T19:49:56.066Z",
"CreatedAt": "2023-12-17T19:49:56.066Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657f511b153787e41052c26a",
"Name": "1 т.р",
"Layer": 3,
"Description": "Больще 1т.р",
"Condition": {
"Period": {
"From": "2023-12-17T19:50:52.764Z",
"To": "2024-01-16T19:50:52.764Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "100000",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.95,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-17T19:50:51.408Z",
"CreatedAt": "2023-12-17T19:50:51.408Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657f512d153787e41052c26b",
"Name": "5 т.р",
"Layer": 3,
"Description": "Больще 5т.р",
"Condition": {
"Period": {
"From": "2023-12-17T19:51:11.104Z",
"To": "2024-01-16T19:51:11.104Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "500000",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.93,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-17T19:51:09.707Z",
"CreatedAt": "2023-12-17T19:51:09.707Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657f5144153787e41052c26c",
"Name": "10 т.р",
"Layer": 3,
"Description": "Больше 10т.р",
"Condition": {
"Period": {
"From": "2023-12-17T19:51:33.502Z",
"To": "2024-01-16T19:51:33.502Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "1000000",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.91,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-17T19:51:32.105Z",
"CreatedAt": "2023-12-17T19:51:32.105Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "657f515a153787e41052c26d",
"Name": "50 т.р",
"Layer": 3,
"Description": "Больше 50т.р",
"Condition": {
"Period": {
"From": "2023-12-17T19:51:56.316Z",
"To": "2024-01-16T19:51:56.316Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "5000000",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.89,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-17T19:51:54.919Z",
"CreatedAt": "2023-12-17T19:51:54.919Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "65872fb4153787e41052c26e",
"Name": "Лямчик",
"Layer": 4,
"Description": "Купил больше чем на миллиона",
"Condition": {
"Period": {
"From": "2023-12-23T19:06:27.521Z",
"To": "2024-01-22T19:06:27.521Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "100000000",
"CartPurchasesAmount": "0",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.9,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-23T19:06:28.253Z",
"CreatedAt": "2023-12-23T19:06:28.253Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "6588c6e9153787e41052c26f",
"Name": "больше 5т.р",
"Layer": 2,
"Description": "Шаблонизатор:Больше 5т.р",
"Condition": {
"Period": {
"From": "2023-12-25T00:03:53.024Z",
"To": "2024-01-24T00:03:53.024Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "500000",
"Group": "templategen"
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.98,
"TargetScope": "Sum",
"TargetGroup": "templategen",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-25T00:03:53.269Z",
"CreatedAt": "2023-12-25T00:03:53.269Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "6588c70c153787e41052c270",
"Name": "больше 10 т.р",
"Layer": 2,
"Description": "Шаблонизатор:Больше 10 т.р",
"Condition": {
"Period": {
"From": "2023-12-25T00:04:28.279Z",
"To": "2024-01-24T00:04:28.279Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "1000000",
"Group": "templategen"
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.97,
"TargetScope": "Sum",
"TargetGroup": "templategen",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-25T00:04:28.442Z",
"CreatedAt": "2023-12-25T00:04:28.442Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "6588c724153787e41052c271",
"Name": "больше 100 т.р",
"Layer": 2,
"Description": "Шаблонизатор:Больше 100 т.р",
"Condition": {
"Period": {
"From": "2023-12-25T00:04:52.461Z",
"To": "2024-01-24T00:04:52.461Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "",
"Term": "0",
"Usage": "0",
"PriceFrom": "10000000",
"Group": "templategen"
},
"Target": {
"Products": [
{
"ID": "",
"Factor": 0,
"Overhelm": false
}
],
"Factor": 0.95,
"TargetScope": "Sum",
"TargetGroup": "templategen",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2023-12-25T00:04:52.625Z",
"CreatedAt": "2023-12-25T00:04:52.625Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "65a49c215389294d1c348511",
"Name": "1000 заявок",
"Layer": 1,
"Description": "Полное прохождение 1000 опросов респондентом",
"Condition": {
"Period": {
"From": "2024-01-15T02:44:49.156Z",
"To": "2024-02-14T02:44:49.156Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "quizCnt",
"Term": "1000",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "quizCnt",
"Factor": 0.7,
"Overhelm": false
}
],
"Factor": 0.7,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2024-01-15T02:44:48.995Z",
"CreatedAt": "2024-01-15T02:44:48.995Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "65a49c4f5389294d1c348512",
"Name": "10000 заявок",
"Layer": 1,
"Description": "Полное прохождение 10000 опросов респондентом",
"Condition": {
"Period": {
"From": "2024-01-15T02:45:37.236Z",
"To": "2024-02-14T02:45:37.236Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "quizCnt",
"Term": "10000",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "quizCnt",
"Factor": 0.5,
"Overhelm": false
}
],
"Factor": 0.5,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2024-01-15T02:45:35.984Z",
"CreatedAt": "2024-01-15T02:45:35.984Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "65a49d1a5389294d1c348513",
"Name": "3 месяца",
"Layer": 1,
"Description": "3 Месяца безлимита",
"Condition": {
"Period": {
"From": "2024-01-15T02:48:59.617Z",
"To": "2024-02-14T02:48:59.617Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "quizUnlimTime",
"Term": "90",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "quizUnlimTime",
"Factor": 0.8,
"Overhelm": false
}
],
"Factor": 0.8,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2024-01-15T02:48:58.560Z",
"CreatedAt": "2024-01-15T02:48:58.560Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "65a49d335389294d1c348514",
"Name": "Год",
"Layer": 1,
"Description": "Год безлимита",
"Condition": {
"Period": {
"From": "2024-01-15T02:49:24.266Z",
"To": "2024-02-14T02:49:24.266Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "quizUnlimTime",
"Term": "365",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "quizUnlimTime",
"Factor": 0.65,
"Overhelm": false
}
],
"Factor": 0.65,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2024-01-15T02:49:23.024Z",
"CreatedAt": "2024-01-15T02:49:23.024Z",
"Deleted": false
},
"Deprecated": false
},
{
"ID": "65a49d4f5389294d1c348515",
"Name": "3 Года",
"Layer": 1,
"Description": "3 Года безлимита",
"Condition": {
"Period": {
"From": "2024-01-15T02:49:52.264Z",
"To": "2024-02-14T02:49:52.264Z"
},
"User": "",
"UserType": "",
"Coupon": "",
"PurchasesAmount": "0",
"CartPurchasesAmount": "0",
"Product": "quizUnlimTime",
"Term": "1095",
"Usage": "0",
"PriceFrom": "0",
"Group": ""
},
"Target": {
"Products": [
{
"ID": "quizUnlimTime",
"Factor": 0.5,
"Overhelm": false
}
],
"Factor": 0.5,
"TargetScope": "Sum",
"TargetGroup": "",
"Overhelm": false
},
"Audit": {
"UpdatedAt": "2024-01-15T02:49:51.024Z",
"CreatedAt": "2024-01-15T02:49:51.024Z",
"Deleted": false
},
"Deprecated": false
}
];

@ -0,0 +1,492 @@
type CartTestResult = [
/** Cart price */
number,
/** Used tariff mask, last number shows if nko applied */
number[],
];
export const cartTestResults: CartTestResult[] = [
[
750184.5,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0,
],
],
[
232560,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
956819.5,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0,
],
],
[
1274000,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
],
],
[
96900,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
[
916051.5,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 1, 0, 0, 0,
],
],
[
508524,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 1, 0, 0, 0, 0,
],
],
[
466860,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1763580,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
963690,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0,
1, 1, 0, 1, 0, 0, 0, 0,
],
],
[
848285.55,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
922862.85,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
[
383158.75,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1101077.25,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0,
],
],
[
602756.25,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
469952.25,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
[
910113.75,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 1, 0, 0, 0, 0,
],
],
[
916665.75,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1, 1, 0, 0, 0, 0, 0, 0,
],
],
[
116280,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
864016.5,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0,
],
],
[
348840,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
1068203.5,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0,
],
],
[
938255.5,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 1, 0, 0, 0,
],
],
[
527496,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 0,
],
],
[
489552,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
[
911092,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1,
1, 1, 0, 1, 0, 0, 0, 0,
],
],
[
935844,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1, 1, 0, 0, 0, 0, 0, 0,
],
],
[
479145.765,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
706809.765,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
966429.555,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 1, 0, 0, 0, 0,
],
],
[
925661.555,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
],
],
[
903678.675,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
911544.725,
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
637980,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
865644,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
939848,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 1, 0, 0, 0, 0,
],
],
[
917280,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0,
],
],
[
973904.9775,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1196672.9775,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
749535.36,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
956184.3200000001,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
915416.3200000001,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1081947.4575,
[
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
468595.57499999995,
[
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1182916.735,
[
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0,
],
],
[
686394.405,
[
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
553590.405,
[
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
[
946453.235,
[
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 1, 0, 0, 0, 0,
],
],
[
998505.235,
[
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1, 1, 0, 0, 0, 0, 0, 0,
],
],
[
905167.14,
[
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1108469.18,
[
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
978521.18,
[
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
[
760745.5800000001,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
967153.4600000001,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
926385.4600000001,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1092804.6675,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1315572.6675,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
872300.9400000001,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1076309.78,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
946361.78,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
[
1200847.1475,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1423615.1475,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
909407.01,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1112617.87,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
982669.87,
[
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
[
1354679.69,
[
0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
907241.9700000001,
[
0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
],
[
1110499.3900000001,
[
0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
],
],
[
980551.39,
[
0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0,
],
],
];

@ -0,0 +1,811 @@
import { Tariff } from "@frontend/kitui";
export const testTariffs: Tariff[] = [
{
"_id": "64f06be63fae7d590bf6426c",
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:02.472Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "64e88d30c4c82e949d5c443d",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 10,
"amount": 130
},
{
"name": "Количество Шаблонов",
"privilegeId": "64e88d30c4c82e949d5c443e",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 5,
"amount": 2100
}
],
"isDeleted": false,
"createdAt": "2023-08-31T10:31:02.570Z",
"updatedAt": "2023-08-31T10:31:02.570Z"
},
{
"_id": "64f06be93fae7d590bf64271",
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:05.703Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "64e88d30c4c82e949d5c443d",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 10,
"amount": 130
},
{
"name": "Количество Шаблонов",
"privilegeId": "64e88d30c4c82e949d5c443e",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 5,
"amount": 2100
}
],
"isDeleted": false,
"createdAt": "2023-08-31T10:31:05.732Z",
"updatedAt": "2023-08-31T10:31:05.732Z"
},
{
"_id": "64f06be93fae7d590bf64276",
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:05.874Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "64e88d30c4c82e949d5c443d",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 10,
"amount": 130
},
{
"name": "Количество Шаблонов",
"privilegeId": "64e88d30c4c82e949d5c443e",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 5,
"amount": 2100
}
],
"isDeleted": false,
"createdAt": "2023-08-31T10:31:05.884Z",
"updatedAt": "2023-08-31T10:31:05.884Z"
},
{
"_id": "64f06c103fae7d590bf6427b",
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:44.015Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "64e88d30c4c82e949d5c443d",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 10,
"amount": 130
},
{
"name": "Количество Шаблонов",
"privilegeId": "64e88d30c4c82e949d5c443e",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 5,
"amount": 2100
}
],
"isDeleted": false,
"createdAt": "2023-08-31T10:31:44.198Z",
"updatedAt": "2023-08-31T10:31:44.198Z"
},
{
"_id": "64f06c123fae7d590bf64280",
"name": "Безлимит, Количество Шаблонов, 2023-08-31T10:31:16.087Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "64e88d30c4c82e949d5c443d",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 10,
"amount": 130
},
{
"name": "Количество Шаблонов",
"privilegeId": "64e88d30c4c82e949d5c443e",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 5,
"amount": 2100
}
],
"isDeleted": false,
"createdAt": "2023-08-31T10:31:46.954Z",
"updatedAt": "2023-08-31T10:31:46.954Z"
},
{
"_id": "64f61a713fae7d590bf6494c",
"name": "Размер Диска, Безлимит, 2023-09-04T17:57:05.862Z",
"isCustom": true,
"privileges": [
{
"name": "Размер Диска",
"privilegeId": "64e88d30c4c82e949d5c443c",
"serviceKey": "templategen",
"description": "Обьём ПенаДиска для хранения шаблонов и результатов шаблонизации",
"type": "count",
"value": "МБ",
"price": 555,
"amount": 1500
},
{
"name": "Безлимит",
"privilegeId": "64e88d30c4c82e949d5c443d",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 10,
"amount": 220
}
],
"isDeleted": false,
"createdAt": "2023-09-04T17:57:05.987Z",
"updatedAt": "2023-09-04T17:57:05.987Z"
},
{
"_id": "64ff6eb75913fc89c5667d85",
"name": "Безлимит, 2023-09-11T19:47:03.383Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "64e88d30c4c82e949d5c443d",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 10,
"amount": 170
}
],
"isDeleted": false,
"createdAt": "2023-09-11T19:47:03.546Z",
"updatedAt": "2023-09-11T19:47:03.546Z"
},
{
"_id": "657b9b11215b615d2e35741f",
"name": "100 шаблонов",
"price": 0,
"isCustom": false,
"privileges": [
{
"name": "Количество Шаблонов",
"privilegeId": "templateCnt",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 1000,
"amount": 100
}
],
"isDeleted": false,
"createdAt": "2023-12-15T00:17:21.104Z",
"updatedAt": "2023-12-15T00:17:21.104Z"
},
{
"_id": "657b9b1a215b615d2e357424",
"name": "1000 шаблонов",
"price": 0,
"isCustom": false,
"privileges": [
{
"name": "Количество Шаблонов",
"privilegeId": "templateCnt",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 1000,
"amount": 1000
}
],
"isDeleted": false,
"createdAt": "2023-12-15T00:17:30.813Z",
"updatedAt": "2023-12-15T00:17:30.813Z"
},
{
"_id": "657b9b98215b615d2e35743a",
"name": "10000 шаблонов",
"price": 0,
"isCustom": false,
"privileges": [
{
"name": "Количество Шаблонов",
"privilegeId": "templateCnt",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 1000,
"amount": 10000
}
],
"isDeleted": false,
"createdAt": "2023-12-15T00:19:36.848Z",
"updatedAt": "2023-12-15T00:19:36.848Z"
},
{
"_id": "657b9bd0215b615d2e357445",
"name": "1 день",
"price": 10000,
"isCustom": false,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "templateUnlimTime",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 1700,
"amount": 1
}
],
"isDeleted": false,
"createdAt": "2023-12-15T00:20:32.586Z",
"updatedAt": "2023-12-15T00:20:32.586Z"
},
{
"_id": "657b9c15215b615d2e35745f",
"name": "Месяц",
"price": 0,
"isCustom": false,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "templateUnlimTime",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 1700,
"amount": 30
}
],
"isDeleted": false,
"createdAt": "2023-12-15T00:21:41.878Z",
"updatedAt": "2023-12-15T00:21:41.878Z"
},
{
"_id": "657b9c25215b615d2e357464",
"name": "3 месяца",
"price": 0,
"isCustom": false,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "templateUnlimTime",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 1700,
"amount": 90
}
],
"isDeleted": false,
"createdAt": "2023-12-15T00:21:57.114Z",
"updatedAt": "2023-12-15T00:21:57.114Z"
},
{
"_id": "657b9c4d215b615d2e357469",
"name": "Год",
"price": 0,
"isCustom": false,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "templateUnlimTime",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 1700,
"amount": 365
}
],
"isDeleted": false,
"createdAt": "2023-12-15T00:22:37.456Z",
"updatedAt": "2023-12-15T00:22:37.456Z"
},
{
"_id": "657b9c59215b615d2e35746e",
"name": "3 года",
"price": 0,
"isCustom": false,
"privileges": [
{
"name": "Безлимит",
"privilegeId": "templateUnlimTime",
"serviceKey": "templategen",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 1700,
"amount": 1095
}
],
"isDeleted": false,
"createdAt": "2023-12-15T00:22:49.492Z",
"updatedAt": "2023-12-15T00:22:49.492Z"
},
{
"_id": "65af14358507c326f5a2db91",
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T01:19:49.676Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 30
},
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 100
}
],
"isDeleted": false,
"createdAt": "2024-01-23T01:19:49.897Z",
"updatedAt": "2024-01-23T01:19:49.897Z"
},
{
"_id": "65af1b978507c326f5a2dbaa",
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T01:51:19.622Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 3
},
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 100
}
],
"isDeleted": false,
"createdAt": "2024-01-23T01:51:19.814Z",
"updatedAt": "2024-01-23T01:51:19.814Z"
},
{
"_id": "65afd0518507c326f5a2e59d",
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T14:42:25.093Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 70
},
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 850
}
],
"isDeleted": false,
"createdAt": "2024-01-23T14:42:25.154Z",
"updatedAt": "2024-01-23T14:42:25.154Z"
},
{
"_id": "65afd05e8507c326f5a2e5a2",
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T14:42:38.254Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 70
},
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 850
}
],
"isDeleted": false,
"createdAt": "2024-01-23T14:42:38.338Z",
"updatedAt": "2024-01-23T14:42:38.339Z"
},
{
"_id": "65afd0738507c326f5a2e5a7",
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T14:42:58.966Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 70
},
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 850
}
],
"isDeleted": false,
"createdAt": "2024-01-23T14:42:59.050Z",
"updatedAt": "2024-01-23T14:42:59.050Z"
},
{
"_id": "65afd08a8507c326f5a2e5ac",
"name": "Безлимит Опросов, Количество Заявок, 2024-01-23T14:43:22.214Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 30
},
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 100
}
],
"isDeleted": false,
"createdAt": "2024-01-23T14:43:22.284Z",
"updatedAt": "2024-01-23T14:43:22.284Z"
},
{
"_id": "65b2d740c644401f2ff3ad26",
"name": "Безлимит Опросов, Количество Заявок, 2024-01-25T21:48:47.933Z",
"isCustom": true,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 100
},
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 3480
}
],
"isDeleted": false,
"createdAt": "2024-01-25T21:48:48.015Z",
"updatedAt": "2024-01-25T21:48:48.015Z"
},
{
"_id": "657b9b06215b615d2e35741a",
"name": "10 шаблонов",
"price": 20000,
"order": 1,
"isCustom": false,
"privileges": [
{
"name": "Количество Шаблонов",
"privilegeId": "templateCnt",
"serviceKey": "templategen",
"description": "Количество шаблонов, которые может сделать пользователь сервиса",
"type": "count",
"value": "шаблон",
"price": 1000,
"amount": 0
}
],
"isDeleted": false,
"createdAt": "2024-01-14T19:22:07.206Z",
"updatedAt": "2024-01-14T19:22:07.206Z"
},
{
"_id": "65a493550089bcd87ba53d4b",
"name": "10 заявок",
"description": "Полное прохождение 10 опросов респондентом",
"price": 0,
"order": 1,
"isCustom": false,
"privileges": [
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 10
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:07:17.403Z",
"updatedAt": "2024-01-15T02:07:17.403Z"
},
{
"_id": "65a493b60089bcd87ba53d5f",
"name": "1 день",
"description": "день безлимитного пользования сервисом",
"price": 10000,
"order": 1,
"isCustom": false,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 1
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:08:54.275Z",
"updatedAt": "2024-01-15T02:08:54.275Z"
},
{
"_id": "65a493640089bcd87ba53d50",
"name": "100 заявок",
"description": "Полное прохождение 100 опросов респондентом",
"price": 0,
"order": 2,
"isCustom": false,
"privileges": [
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 100
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:07:32.444Z",
"updatedAt": "2024-01-15T02:07:32.444Z"
},
{
"_id": "65a497890089bcd87ba53d64",
"name": "Месяц",
"description": "Месяц безлимитного пользования сервисом",
"price": 0,
"order": 2,
"isCustom": false,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 30
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:25:13.366Z",
"updatedAt": "2024-01-15T02:25:13.366Z"
},
{
"_id": "65a493740089bcd87ba53d55",
"name": "1000 заявок",
"description": "Полное прохождение 1000 опросов респондентом",
"price": 0,
"order": 3,
"isCustom": false,
"privileges": [
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 1000
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:07:48.095Z",
"updatedAt": "2024-01-15T02:07:48.095Z"
},
{
"_id": "65a4987e0089bcd87ba53d75",
"name": "3 Месяца",
"description": "3 Месяца безлимитного пользования сервисом",
"price": 0,
"order": 3,
"isCustom": false,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 90
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:29:18.577Z",
"updatedAt": "2024-01-15T02:29:18.577Z"
},
{
"_id": "65a498cc0089bcd87ba53d92",
"name": "Год",
"description": "Год безлимитного пользования сервисом",
"price": 0,
"order": 3,
"isCustom": false,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 365
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:30:36.131Z",
"updatedAt": "2024-01-15T02:30:36.131Z"
},
{
"_id": "65a493830089bcd87ba53d5a",
"name": "10000 заявок",
"description": "Полное прохождение 10000 опросов респондентом",
"price": 0,
"order": 4,
"isCustom": false,
"privileges": [
{
"name": "Количество Заявок",
"privilegeId": "quizCnt",
"serviceKey": "squiz",
"description": "Количество полных прохождений опросов",
"type": "count",
"value": "заявка",
"price": 2000,
"amount": 10000
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:08:03.341Z",
"updatedAt": "2024-01-15T02:08:03.341Z"
},
{
"_id": "65a498f80089bcd87ba53d97",
"name": "3 Года",
"description": "3 Года безлимитного пользования сервисом",
"price": 0,
"order": 4,
"isCustom": false,
"privileges": [
{
"name": "Безлимит Опросов",
"privilegeId": "quizUnlimTime",
"serviceKey": "squiz",
"description": "Количество дней, в течении которых пользование сервисом безлимитно",
"type": "day",
"value": "день",
"price": 3400,
"amount": 1095
}
],
"isDeleted": false,
"createdAt": "2024-01-15T02:31:20.448Z",
"updatedAt": "2024-01-15T02:31:20.448Z"
}
];

@ -1,40 +1,33 @@
import { Discount, Tariff, findDiscountFactor } from "@frontend/kitui" import { Discount, Tariff, findDiscountFactor } from "@frontend/kitui";
import { calcCart } from "./calcCart/calcCart" import { calcCart } from "./calcCart/calcCart";
export function calcIndividualTariffPrices( export function calcIndividualTariffPrices(
tariff: Tariff, targetTariff: Tariff,
discounts: Discount[], discounts: Discount[],
purchasesAmount: number, purchasesAmount: number,
currentTariffs: Tariff[], currentTariffs: Tariff[],
isUserNko?: boolean, isUserNko?: boolean,
): { ): {
priceBeforeDiscounts: number; priceBeforeDiscounts: number;
priceAfterDiscounts: number; priceAfterDiscounts: number;
} { } {
const priceBeforeDiscounts = const priceBeforeDiscounts = targetTariff.price || targetTariff.privileges.reduce(
tariff.price || tariff.privileges.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0) (sum, privilege) => sum + privilege.amount * privilege.price,
let priceAfterDiscounts = 0 0
);
const cart = calcCart([...currentTariffs, tariff], discounts, purchasesAmount, isUserNko) const cart = calcCart([...currentTariffs, targetTariff], discounts, purchasesAmount, isUserNko);
if (cart.allAppliedDiscounts[0]?.Target.Overhelm) return {priceBeforeDiscounts: priceBeforeDiscounts, priceAfterDiscounts: priceBeforeDiscounts * cart.allAppliedDiscounts[0].Target.Factor }
cart.services.forEach(s => {
if (s.serviceKey === tariff.privileges[0].serviceKey) {
let processed = false
s.tariffs.forEach(t => {
if (t.id === tariff._id && !processed) {
processed = true
t.privileges.forEach(p => priceAfterDiscounts+=p.price)
}
})
}
})
priceAfterDiscounts*=findDiscountFactor(cart.appliedLoyaltyDiscount)
priceAfterDiscounts*=findDiscountFactor(cart.appliedCartPurchasesDiscount)
if (priceAfterDiscounts === 0) priceAfterDiscounts = cart.priceAfterDiscounts const tariffCartData = cart.services.flatMap(service => service.tariffs).find(tariff => tariff.id === targetTariff._id);
// cart.allAppliedDiscounts.forEach((discount) => { if (!tariffCartData) throw new Error(`Target tariff ${targetTariff._id} not found in cart`);
// priceAfterDiscounts *= findDiscountFactor(discount)
// }) if (cart.allAppliedDiscounts[0]?.Target.Overhelm) return {
//priceAfterDiscounts = cart.priceAfterDiscounts priceBeforeDiscounts: priceBeforeDiscounts,
return { priceBeforeDiscounts, priceAfterDiscounts } priceAfterDiscounts: priceBeforeDiscounts * cart.allAppliedDiscounts[0].Target.Factor
};
return {
priceBeforeDiscounts,
priceAfterDiscounts: tariffCartData.price,
};
} }

@ -6,7 +6,7 @@ interface ComponentError {
timestamp: number; timestamp: number;
message: string; message: string;
callStack: string | undefined; callStack: string | undefined;
componentStack: string; componentStack: string | null | undefined;
} }
export function handleComponentError(error: Error, info: ErrorInfo) { export function handleComponentError(error: Error, info: ErrorInfo) {

@ -1,85 +1,20 @@
import { Tariff, devlog } from "@frontend/kitui" import { useDiscountStore } from "@root/stores/discounts";
import { getTariffById } from "@root/api/tariff" import { useUserStore } from "@root/stores/user";
import { import { useMemo } from "react";
addCartTariffs, import { calcCart } from "../calcCart/calcCart";
removeMissingCartTariffs, import { useCartTariffs } from "./useCartTariffs";
setCartTariffStatus, import { CartData } from "@frontend/kitui";
setNotEnoughMoneyAmount,
useCartStore,
} from "@root/stores/cart"
import { calcCart } from "@root/utils/calcCart/calcCart"
import { useDiscountStore } from "@root/stores/discounts"
import { useTariffStore } from "@root/stores/tariffs"
import { removeTariffFromCart, useUserStore } from "@root/stores/user"
import { useEffect } from "react"
export function useCart() { export function useCart(): CartData {
const tariffs = useTariffStore((state) => state.tariffs) const cartTariffs = useCartTariffs();
const cartTariffMap = useCartStore((state) => state.cartTariffMap) const discounts = useDiscountStore((state) => state.discounts);
const cartTariffIds = useUserStore((state) => state.userAccount?.cart) const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0;
const cart = useCartStore((state) => state.cart) const isUserNko = useUserStore(state => state.userAccount?.status) === "nko";
const discounts = useDiscountStore((state) => state.discounts)
const purchasesAmount =
useUserStore((state) => state.userAccount?.wallet.spent) ?? 0
const isUserNko = useUserStore(state => state.userAccount?.status) === "nko"
useEffect(() => { const cart = useMemo(() => {
function addTariffsToCart() { return calcCart(cartTariffs ?? [], discounts, purchasesAmount, isUserNko);
const knownTariffs: Tariff[] = [] }, [cartTariffs, discounts, purchasesAmount, isUserNko]);
setNotEnoughMoneyAmount(0);
cartTariffIds?.forEach(async (tariffId) => { return cart;
if (typeof cartTariffMap[tariffId] === "object") return
const tariff = tariffs.find((tariff) => tariff._id === tariffId)
if (tariff) return knownTariffs.push(tariff)
if (!cartTariffMap[tariffId]) {
setCartTariffStatus(tariffId, "loading")
const [tariffByIdResponse, tariffByIdError, tariffByIdStatus] =
await getTariffById(tariffId)
if (tariffByIdError) {
devlog(tariffByIdError)
setCartTariffStatus(tariffId, "not found")
if (tariffByIdStatus === 404) {
try {
await removeTariffFromCart(tariffId)
devlog(
`Unexistant tariff with id ${tariffId} deleted from cart`
)
} catch (error) {
devlog("Error deleting unexistant tariff from cart", error)
}
}
return
}
if (tariffByIdResponse) {
addCartTariffs([tariffByIdResponse], discounts, purchasesAmount, isUserNko)
}
}
})
if (knownTariffs.length > 0)
addCartTariffs(knownTariffs, discounts, purchasesAmount, isUserNko)
}
addTariffsToCart()
}, [cartTariffIds, cartTariffMap, discounts, isUserNko, purchasesAmount, tariffs])
useEffect(
function cleanUpCart() {
if (cartTariffIds)
removeMissingCartTariffs(cartTariffIds, discounts, purchasesAmount, isUserNko)
},
[cartTariffIds, discounts, isUserNko, purchasesAmount]
)
const currentTariffs = Object.values(cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
return calcCart(currentTariffs, discounts,purchasesAmount,isUserNko)
} }

@ -0,0 +1,17 @@
import { getTariffArray } from "@root/api/tariff";
import { useUserStore } from "@root/stores/user";
import useSWR from "swr";
export function useCartTariffs() {
const cartTariffIds = useUserStore((state) => state.userAccount?.cart);
const { data } = useSWR(
["cartTariffs", cartTariffIds],
key => getTariffArray(key[1]),
{
keepPreviousData: true,
}
);
return data;
}

@ -1,7 +1,7 @@
{ {
"extends": "./tsconfig.extend.json", "extends": "./tsconfig.extend.json",
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "ESNext",
"lib": ["es5", "dom", "dom.iterable", "esnext"], "lib": ["es5", "dom", "dom.iterable", "esnext"],
"types": ["node"], "types": ["node"],
"allowJs": true, "allowJs": true,

24617
yarn.lock

File diff suppressed because it is too large Load Diff