Compare commits

...

123 Commits

Author SHA1 Message Date
d949deb784 v2.0.3 fix token
Some checks failed
Deploy / Publish (push) Failing after 20s
2025-05-11 20:27:34 +03:00
2b5ae862de v2.0.3 fix usetoken in makerequest
All checks were successful
Deploy / Publish (push) Successful in 21s
2025-05-10 23:42:53 +03:00
c022f5ee0a v2.0.2 pipeline publishing 2025-05-10 23:41:03 +03:00
24fa6510ad v2.0.2 add console.log 2025-05-10 23:36:42 +03:00
7ce1904cb5 v2.0.1 replace webpack env to vite env 2025-05-10 23:31:17 +03:00
3f6601f33b update all release v.2.0.0 2025-05-10 23:12:48 +03:00
3ef7ad7791 v1.0.92 2025-03-07 15:10:24 +03:00
6142f5acdd create ticket with System 2025-03-07 15:09:52 +03:00
3fc33544f2 registry rules gitea 2025-02-18 23:43:56 +03:00
3f0431437d fix useAllTariffsFetcher 2025-02-18 23:09:55 +03:00
978c4d66eb fix useAllTariffsFetcher 2025-02-17 03:17:10 +03:00
3d0850ebab update version 1.0.88 2025-01-24 19:49:47 +03:00
98b2a6c1d1 v1.0.87 2025-01-24 12:50:30 +03:00
047c21bd06 тарифы получаются по totalPage, а не по своим расчётам 2025-01-24 12:49:55 +03:00
2e849a8d11 upgrade types of useUserAccountFetcher 2024-08-21 11:12:29 +03:00
25e80c6196 v1.0.85 2024-07-28 00:23:37 +03:00
fd445ce49d fix verif status of user 2024-07-28 00:22:44 +03:00
6b256f37b9 v1.0.84 2024-07-24 21:11:55 +03:00
0ca1fcf98c customer fire with version 2024-07-24 21:11:22 +03:00
aacb44fd00 refresh post without / 2024-04-16 13:09:45 +03:00
f3290036d1 bearer для токена рефреша 2024-04-16 11:41:01 +03:00
08a3edb3e3 responsetype в конфиг 2024-04-15 23:14:39 +03:00
230451ff78 makerequest принимает response type 2024-04-15 21:45:44 +03:00
be6f3cdce6 refresh+/ 2024-04-06 11:22:23 +03:00
nflnkr
79adf3ca86 add cart privilege applied discounts field
change allAppliedDiscounts cart field type to Set
2024-03-27 17:29:32 +03:00
nflnkr
761a2d7cea 1.0.76
fix exports
2024-03-27 16:11:37 +03:00
nflnkr
2db710de79 fix exports 2024-03-27 15:09:54 +03:00
nflnkr
21a3323af9 1.0.74
fix exports
add calcTariffPrice
2024-03-27 13:29:34 +03:00
nflnkr
341bee7ea9 1.0.73
add cart calculation functions and tests
add custom tariff types
2024-03-27 13:21:49 +03:00
nflnkr
a3a934e404 remove fields from cart types 2024-03-26 17:27:58 +03:00
nflnkr
3a8b8b1b42 1.0.70 change discount type: most fields are optional 2024-03-22 20:33:19 +03:00
nflnkr
5f17e1c730 1.0.69
fix cart types and functions
2024-03-19 18:30:11 +03:00
nflnkr
e3281be590 add CustomPrivilegeWithAmount type 2024-03-19 18:10:18 +03:00
nflnkr
897316cbc6 1.0.67
split privilege and custom privilege types
2024-03-19 18:04:48 +03:00
nflnkr
e0a88efeaf publish 1.0.66
fix package.json version
2024-03-16 15:45:09 +03:00
nflnkr
b3729eb344 add description and order fields to Tariff type 2024-03-16 15:43:35 +03:00
IlyaDoronin
ced4f8c613 feat: Ticket interface updated 2024-03-11 11:50:05 +03:00
Tamara
b24edcad37 общая переменная для запросов из любого проекта 2024-01-19 21:01:24 +03:00
nflnkr
3280b82e7c fix user account creation url 2023-12-19 17:57:44 +03:00
5327da77ea fix: privilege discount on amount 2023-12-14 01:28:48 +03:00
nflnkr
3aff04bf4a fix nko discount applying to non-nko user 2023-10-26 13:53:14 +03:00
nflnkr
4a21fecdac add nko status to user account type 2023-10-14 13:07:01 +03:00
nflnkr
349cd20480 fix external deps config 2023-09-16 15:20:56 +03:00
8730135e3a privilegie заменён на privilege 2023-09-16 12:24:33 +03:00
nflnkr
9b333f5126 fix navitem variant text wrap 2023-09-04 17:13:14 +03:00
nflnkr
664a0cfb31 add button variant 2023-09-04 16:00:45 +03:00
nflnkr
cdf7bc4ed4 minor fixes 2023-09-01 15:31:11 +03:00
nflnkr
7af34954c9 mix theme with reducer theme 2023-09-01 15:27:35 +03:00
nflnkr
3d80cf1ea9 add button variant 2023-09-01 13:18:57 +03:00
nflnkr
f574b4822b add pagination style variant 2023-08-30 18:52:27 +03:00
nflnkr
a9c3a7bb31 fix imports 2023-08-29 16:25:57 +03:00
nflnkr
56afbbefe2 fix hooks 2023-08-29 16:24:51 +03:00
nflnkr
8b469c1b2d fix hooks 2023-08-29 14:16:10 +03:00
nflnkr
b2e9eeff44 add auth types 2023-08-28 17:52:17 +03:00
nflnkr
f16a659a61 fix hooks args 2023-08-28 17:30:43 +03:00
nflnkr
eb7814f3e3 add auth hooks 2023-08-28 17:29:26 +03:00
nflnkr
7f2ab8c5a5 fix import paths 2023-08-28 17:20:06 +03:00
nflnkr
31058d1604 add account api functions 2023-08-28 17:18:03 +03:00
nflnkr
77a8ff5cc0 add user account type 2023-08-28 17:14:39 +03:00
nflnkr
64028a50e2 add user type 2023-08-28 17:07:52 +03:00
nflnkr
a72b951202 add button variant & fix variant style 2023-08-28 15:57:28 +03:00
nflnkr
23d41d5ff0 fix theme colors 2023-08-24 16:11:19 +03:00
nflnkr
732004c45d fix button style 2023-08-23 15:18:11 +03:00
nflnkr
b3d04f4f61 fix button type 2023-08-23 14:14:40 +03:00
nflnkr
30c5bcc6bd fix button types 2023-08-23 13:56:45 +03:00
nflnkr
5e4262ef49 minor fixes 2023-08-22 15:39:03 +03:00
nflnkr
b24265246f fix colors 2023-08-22 14:28:38 +03:00
nflnkr
eaa062302e fix link type 2023-08-22 14:03:11 +03:00
nflnkr
e6ae21082d fix colors 2023-08-22 13:59:11 +03:00
nflnkr
b97c70a5f2 fix theme 2023-08-22 12:44:16 +03:00
nflnkr
49efa9c3b2 fix version 2023-08-22 12:31:00 +03:00
nflnkr
77a9346b5a change theme colors 2023-08-21 16:37:25 +03:00
nflnkr
0fadd8fc76 add ui components 2023-08-21 14:10:34 +03:00
nflnkr
f3b5c1862a change cart types 2023-08-18 13:17:12 +03:00
nflnkr
06f55de0c3 add button styles 2023-08-14 17:09:38 +03:00
nflnkr
6b71828653 fix npmrc and update readme instructions 2023-08-14 16:16:49 +03:00
nflnkr
5231e805bc fix readme publish command 2023-08-14 16:01:46 +03:00
nflnkr
d215cdc007 fix readme publish command 2023-08-14 16:00:59 +03:00
nflnkr
9d155a083a fix eslint 2023-08-14 16:00:20 +03:00
nflnkr
9988be9ea3 fix component export 2023-08-14 15:53:46 +03:00
nflnkr
0aafce2c5d minor fixes 2023-08-14 15:31:22 +03:00
nflnkr
9cd8d50475 init 2023-08-14 15:03:23 +03:00
nflnkr
e59386d60a Revert "add serviceKey field to tariff cart data"
This reverts commit e13590d05534314fd19aa0fc2ca8fc469d0f8909.
2023-08-12 19:42:06 +03:00
nflnkr
e13590d055 add serviceKey field to tariff cart data 2023-08-12 19:32:22 +03:00
nflnkr
2018fa85dd fix cart types and functions 2023-08-12 19:17:29 +03:00
nflnkr
0524a39318 minor fix 2023-08-12 18:43:24 +03:00
nflnkr
25b24fbeb0 inc version 2023-08-11 12:34:19 +03:00
nflnkr
211acfbd98 add find discount factor function 2023-08-11 12:32:54 +03:00
nflnkr
c15d17e77d fix exports 2023-08-08 14:31:27 +03:00
nflnkr
84c7efe6b5 increment version 2023-08-08 14:24:44 +03:00
nflnkr
96edf676f4 add usePrivileges hook 2023-08-08 14:23:11 +03:00
nflnkr
45b532415b add tariff type clarification comment 2023-08-08 13:57:39 +03:00
nflnkr
264b982ba5 fix privilege type 2023-07-28 17:03:04 +03:00
nflnkr
40c598ce9b change types to include applied discounts
rename fields
2023-07-24 15:36:08 +03:00
nflnkr
19cfdbd74e fix version 2023-07-22 17:03:52 +03:00
nflnkr
551ada4f5b patch version 2023-07-22 17:03:13 +03:00
nflnkr
d5f35ed623 fix apply discounts functions 2023-07-22 17:02:07 +03:00
nflnkr
8433c7adcd add type field clarifications 2023-07-22 17:01:08 +03:00
nflnkr
bdeb1d9765 change discount field type 2023-07-19 14:51:35 +03:00
nflnkr
6b39f34884 rename cart field 2023-07-18 14:54:59 +03:00
nflnkr
460a9f28d1 add envolvedDiscounts to CartData type 2023-07-14 19:13:49 +03:00
nflnkr
fb9f17798c rename functions 2023-07-14 19:13:24 +03:00
nflnkr
7151dc25aa add factor to find discount return values 2023-07-13 17:14:59 +03:00
nflnkr
197b9c247a publish 1.0.10 2023-07-13 16:45:58 +03:00
nflnkr
dd62433c6b rework cart functions 2023-07-13 16:45:38 +03:00
nflnkr
51a471e8dc publish 1.0.9 2023-07-12 10:31:01 +03:00
nflnkr
4956543b7e refactor hooks 2023-07-10 20:48:09 +03:00
nflnkr
b0f2e94ebf add cart stuff 2023-07-10 20:41:55 +03:00
nflnkr
4c96954764 refactor auth store 2023-07-10 19:17:54 +03:00
nflnkr
f6bc1dbad3 add node env type 2023-07-10 19:13:05 +03:00
nflnkr
7063bafb28 version patch 2023-06-24 17:46:11 +03:00
nflnkr
c1aae18bf8 fix console log duplication 2023-06-24 17:45:29 +03:00
nflnkr
7c7edd9fe9 version patch 2023-06-24 13:51:22 +03:00
nflnkr
e74c7b0fd3 minor fixes 2023-06-24 13:50:00 +03:00
nflnkr
20687d8551 minor fixes 2023-06-17 17:27:22 +03:00
nflnkr
6b2803864e Merge branch 'dev' of penahub.gitlab.yandexcloud.net:frontend/kitui into dev 2023-06-17 17:25:15 +03:00
nflnkr
02184d3084 add tariffs types/hook 2023-06-17 17:25:05 +03:00
nflnkr
2b8978567b add tariffs types/hook 2023-06-17 17:24:12 +03:00
nflnkr
9e28031d9b refactor makeRequest 2023-06-11 12:55:06 +03:00
nflnkr
faa2cdb62a update readme 2023-06-06 16:10:57 +03:00
nflnkr
8909a32576 fix exports 2023-06-06 14:22:22 +03:00
nflnkr
c59be041e7 fix readme 2023-06-06 14:15:47 +03:00
nflnkr
a9ca5294dd add stuff 2023-06-06 13:02:17 +03:00
76 changed files with 14663 additions and 14 deletions

25
.eslintrc.cjs Normal file

@ -0,0 +1,25 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs', "vite.config.ts"],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn",
"no-restricted-exports": ["error", {
"restrictDefaultExports": {
direct: true,
}
}]
},
};

@ -0,0 +1,34 @@
name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry
on:
push:
branches:
- 'main'
- 'staging'
- 'dev'
jobs:
Publish:
runs-on: ["skeris"]
conainer:
image: gitea.pena:3000/penadevops/container-images/node-compose:main
steps:
- name: Check out repository code
uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
- name: Publish
run: |
git config --global user.email "kotilion.95@gmail.com"
git config --global user.name "skeris"
npm config set @frontend:registry=http://gitea.pena/api/packages/skeris/npm/
npm config set registry=https://registry.npmjs.org/
npm config set -- '//gitea.pena/api/packages/skeris/npm/:_authToken' "1856e802057f59193ca6fdb4068cbea44982bcc2"
npm install --force
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
npm version major --no-git-tag-version
elif [ "${{ github.ref }}" == "refs/heads/staging" ]; then
npm version minor --no-git-tag-version
else
npm version patch --no-git-tag-version
fi
npm publish

24
.gitignore vendored Normal file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

4
.npmrc

@ -1,4 +0,0 @@
@frontend:kitui=https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/
'//penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken'="${GITLAB_AUTH_TOKEN}"
'//penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/:_authToken'="${GITLAB_AUTH_TOKEN}"

3
.yarnrc Normal file

@ -0,0 +1,3 @@
"@frontend:registry" "http://gitea.pena/api/packages/skeris/npm/"
# Для всех остальных - стандартный npmjs
"registry" "https://registry.npmjs.org/"

4
CHANGELOG.md Normal file

@ -0,0 +1,4 @@
- Бог в помощь
- Обновлены все пакеты
- Убран yarn
#v2.0.0

19
README.md Normal file

@ -0,0 +1,19 @@
## Перед использованием и публикацией
```bash
# заменить TOKEN на актуальный токен
npm config set //penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken=TOKEN
npm config set //penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/:_authToken=TOKEN
```
## Публикация
```bash
yarn publish
```
## Использование
Добавить в корень проекта файл .yarnrc с содержимым
```
"@frontend:registry" "https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/"
```
Установка
```bash
yarn add @frontend/kitui
```

11
env.d.ts vendored Normal file

@ -0,0 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_REACT_APP_DOMAIN: string;
readonly VITE_API_KEY: string;
// Добавьте другие переменные окружения, которые вы используете
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

17
index.html Normal file

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

27
lib/api/account.ts Normal file

@ -0,0 +1,27 @@
import { UserAccount, UserName } from "../model/account";
import { makeRequest } from "./makeRequest";
const apiUrl = import.meta.env.VITE__APP_DOMAIN + "/customer";
export function patchUserAccount(user: UserName, version:string | undefined) {
return makeRequest<UserName, UserAccount>({
url: `${apiUrl + (version ? `/${version}` : "")}/account`,
contentType: true,
method: "PATCH",
useToken: true,
withCredentials: false,
body: user,
});
}
export function createUserAccount(signal: AbortSignal, url: string | undefined, version: string) {
return makeRequest<never, UserAccount>({
url: url || `${apiUrl + (version.length > 0 ? `/${version}` : "")}/account`,
contentType: true,
method: "POST",
useToken: true,
withCredentials: false,
signal,
});
}

4
lib/api/index.ts Normal file

@ -0,0 +1,4 @@
export * from "./account";
export * from "./makeRequest";
export * from "./tariff";
export * from "./tickets";

76
lib/api/makeRequest.ts Normal file

@ -0,0 +1,76 @@
import axios, { AxiosResponse, Method, ResponseType } from "axios";
import { getAuthToken, setAuthToken } from "../stores/auth";
export async function makeRequest<TRequest = unknown, TResponse = unknown>({
method = "post",
url,
body,
useToken = true,
contentType = false,
responseType = "json",
signal,
withCredentials,
}: {
method?: Method;
url: string;
body?: TRequest;
/** Send access token */
useToken?: boolean;
contentType?: boolean;
responseType?: ResponseType;
signal?: AbortSignal;
/** Send refresh token */
withCredentials?: boolean;
}): Promise<TResponse> {
const headers: Record<string, string> = {};
if (useToken) headers["Authorization"] = getAuthToken() ? `Bearer ${getAuthToken()}` : "";
if (contentType) headers["Content-Type"] = "application/json";
try {
const response = await axios<TRequest, AxiosResponse<TResponse & { accessToken?: string; }>>({
url,
method,
headers,
data: body,
signal,
responseType,
withCredentials,
});
if (response.data?.accessToken) {
setAuthToken(response.data.accessToken);
}
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401 && !withCredentials) {
const refreshResponse = await refresh(getAuthToken());
if (refreshResponse.data?.accessToken) setAuthToken(refreshResponse.data.accessToken);
headers["Authorization"] = refreshResponse.data.accessToken;
const response = await axios.request<TRequest, AxiosResponse<TResponse>>({
url,
method,
headers,
data: body,
signal,
});
return response.data;
}
throw error;
}
}
function refresh(token: string) {
return axios<never, AxiosResponse<{ accessToken: string; }>>(import.meta.env.VITE__APP_DOMAIN + "/auth/refresh", { headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "post"
});
}

10
lib/api/tariff.ts Normal file

@ -0,0 +1,10 @@
import { Tariff } from "../model/tariff";
import { makeRequest } from "./makeRequest";
export function getTariffById(tariffId:string){
return makeRequest<never, Tariff>({
url: import.meta.env.VITE__APP_DOMAIN + `/strator/tariff/${tariffId}`, method: "get",
useToken: true,
});
}

17
lib/api/tickets.ts Normal file

@ -0,0 +1,17 @@
import { CreateTicketRequest, CreateTicketResponse } from "../model/ticket";
import { makeRequest } from "./makeRequest";
export function createTicket({ url, body, useToken = true }: {
url: string;
body: CreateTicketRequest;
useToken?: boolean;
}): Promise<CreateTicketResponse> {
return makeRequest({
url,
method: "POST",
useToken,
body,
withCredentials: true,
});
}

@ -0,0 +1,59 @@
import { Avatar, IconButton, IconButtonProps, Typography, useTheme } from "@mui/material";
import { deepmerge } from "@mui/utils";
import { ForwardRefExoticComponent, RefAttributes } from "react";
import { LinkProps } from "react-router-dom";
type Props = IconButtonProps & {
component?: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>>;
to?: string;
};
export function AvatarButton(props: Props) {
const theme = useTheme();
return (
<IconButton
{...deepmerge({
sx: {
height: 36,
width: 36,
p: 0,
"&:hover .MuiAvatar-root": {
border: `2px solid ${theme.palette.gray.main}`,
},
"&:active .MuiAvatar-root": {
backgroundColor: theme.palette.purple.main,
color: theme.palette.purple.main,
border: "1px solid black",
},
}
}, props)}
>
<Avatar sx={{
height: "100%",
width: "100%",
backgroundColor: theme.palette.orange.main,
color: theme.palette.orange.light,
transition: "all 100ms",
}}>
<Typography sx={{
fontWeight: 500,
fontSize: "14px",
lineHeight: "20px",
zIndex: 1,
textTransform: "uppercase",
position: "absolute",
color: "white",
}}>
{props.children ?? "AA"}
</Typography>
<svg width="100%" height="100%" viewBox="0 0 36 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M15.348 16.146c.1-5.981 6.823-10.034 12.62-11.479 6.055-1.508 13.264-.719 17.31 4.023 3.673 4.303.83 10.565-.085 16.16-.678 4.152-1.209 8.41-4.383 11.171-3.418 2.973-8.742 6.062-12.43 3.452-3.663-2.593 1.412-8.88-.78-12.8-2.764-4.95-12.347-4.85-12.252-10.527Z" fill="currentColor" />
<circle cx="28.052" cy="-3.333" r="5.519" transform="rotate(-32.339 28.052 -3.333)" fill="currentColor" />
<circle cx="24.363" cy="29.03" r="1.27" transform="rotate(-32.339 24.363 29.03)" fill="currentColor" />
</svg>
</Avatar>
</IconButton>
);
}

@ -0,0 +1,31 @@
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
import { deepmerge } from "@mui/utils";
export function BurgerButton(props: IconButtonProps) {
const theme = useTheme();
return (
<IconButton
{...deepmerge({
sx: {
height: 30,
width: 30,
p: 0,
color: "black",
"&:hover": {
color: theme.palette.purple.main,
backgroundColor: "rgb(0 0 0 / 0)",
},
"&:active": {
color: theme.palette.purple.main,
},
},
}, props)}
>
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28 8.005H3M28 16.005H3M28 24.005H3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
</IconButton>
);
}

@ -0,0 +1,31 @@
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
import { deepmerge } from "@mui/utils";
export function CloseButton(props: IconButtonProps) {
const theme = useTheme();
return (
<IconButton
{...deepmerge({
sx: {
height: 30,
width: 30,
p: 0,
color: "black",
"&:hover": {
color: theme.palette.orange.main,
backgroundColor: "rgb(0 0 0 / 0)",
},
"&:active": {
color: theme.palette.orange.main,
},
},
}, props)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 30 31" fill="none">
<path d="m3 3.605 24 24m-24 0 24-24" stroke="currentColor" />
</svg>
</IconButton>
);
}

@ -0,0 +1,31 @@
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
import { deepmerge } from "@mui/utils";
export function CloseButtonSmall(props: IconButtonProps) {
const theme = useTheme();
return (
<IconButton
{...deepmerge({
sx: {
height: 12,
width: 12,
p: 0,
color: theme.palette.purple.main,
"&:hover": {
color: theme.palette.orange.main,
backgroundColor: "rgb(0 0 0 / 0)",
},
"&:active": {
color: theme.palette.orange.main,
},
},
}, props)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 14 14" fill="none">
<path d="m13 1.176-12 12M13 13.176l-12-12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</IconButton>
);
}

@ -0,0 +1,39 @@
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
import { deepmerge } from "@mui/utils";
export function LogoutButton(props: IconButtonProps) {
const theme = useTheme();
return (
<IconButton
{...deepmerge({
sx: {
height: 36,
width: 36,
p: 0,
borderRadius: "6px",
backgroundColor: theme.palette.background.default,
color: theme.palette.gray.main,
"&:hover": {
color: theme.palette.background.default,
backgroundColor: theme.palette.gray.main,
},
"&:active": {
backgroundColor: theme.palette.purple.main,
color: "white",
},
}
}, props)}
>
<svg width="100%" height="100%" viewBox="0 0 36 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M22.559 22.052v2.95a2 2 0 0 1-2 2h-6.865a2 2 0 0 1-2-2v-12.5a2 2 0 0 1 2-2h6.865a2 2 0 0 1 2 2v2.95M25.067 21.227l1.786-1.763a1 1 0 0 0 0-1.423l-1.786-1.764M26.737 18.752H16.71"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
</IconButton>
);
}

@ -0,0 +1,34 @@
import { Link, LinkProps, LinkTypeMap, Typography, useTheme } from "@mui/material";
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { deepmerge } from "@mui/utils";
import { OverridableComponent } from "@mui/material/OverridableComponent";
export const PenaLink: OverridableComponent<LinkTypeMap<object, "a">> = (props: LinkProps) => {
const theme = useTheme();
return (
<Link
{...deepmerge({
sx: {
display: "flex",
gap: "3px",
textUnderlinePosition: "under",
color: theme.palette.purple.light,
textDecorationColor: theme.palette.purple.main,
textUnderlineOffset: "2px",
"&:hover": {
textDecorationColor: theme.palette.purple.light,
},
"&:active": {
color: "white",
textDecorationColor: "white",
},
}
}, props)}
>
<Typography variant="body2">{props.children}</Typography>
<ArrowForwardIcon sx={{ height: "20px", width: "20px" }} />
</Link>
);
};

@ -0,0 +1,120 @@
import { FormControl, InputLabel, SxProps, TextField, TextFieldProps, Theme, useMediaQuery, useTheme } from "@mui/material";
interface Props {
id?: string;
label?: string;
labelSx?: SxProps<Theme>;
bold?: boolean;
gap?: string;
backgroundColor?: string;
FormControlSx?: SxProps<Theme>;
TextFieldSx?: SxProps<Theme>;
placeholder?: TextFieldProps["placeholder"];
value?: TextFieldProps["value"];
helperText?: TextFieldProps["helperText"];
error?: TextFieldProps["error"];
type?: TextFieldProps["type"];
onBlur?: TextFieldProps["onBlur"];
onChange?: TextFieldProps["onChange"];
fullWidth?: boolean;
}
export function PenaTextField({
id = "pena-textfield",
label,
labelSx,
bold = false,
gap = "10px",
onChange,
error,
helperText,
onBlur,
placeholder,
type,
value,
backgroundColor,
FormControlSx,
TextFieldSx,
fullWidth = true,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const labelFont = upMd
? bold
? theme.typography.p1
: { ...theme.typography.body1, fontWeight: 500 }
: theme.typography.body2;
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
return (
<FormControl
fullWidth={fullWidth}
variant="standard"
sx={{
gap,
...FormControlSx,
}}
>
{label && (
<InputLabel
shrink
htmlFor={id}
sx={{
position: "inherit",
color: "black",
transform: "none",
...labelFont,
...labelSx,
}}
>
{label}
</InputLabel>
)}
<TextField
fullWidth
id={id}
error={error}
helperText={helperText}
onBlur={onBlur}
placeholder={placeholder}
type={type}
value={value}
sx={{
"& .MuiInputBase-root": {
height: "48px",
borderRadius: "8px",
"& fieldset": {
border: `1px solid ${theme.palette.gray.main}`,
},
"&:hover fieldset": {
border: `1px solid ${theme.palette.gray.dark}`,
},
"&.Mui-focused fieldset": {
border: `2px solid ${theme.palette.purple.main}`,
},
},
"& .MuiFormHelperText-root.MuiFormHelperText-contained.MuiFormHelperText-filled.Mui-error": {
position: "absolute",
top: "45px",
},
...TextFieldSx,
}}
inputProps={{
sx: {
boxSizing: "border-box",
backgroundColor: backgroundColor ?? theme.palette.background.default,
borderRadius: "8px",
height: "48px",
py: 0,
color: theme.palette.gray.dark,
...placeholderFont,
},
}}
onChange={onChange}
/>
</FormControl>
);
}

@ -0,0 +1,41 @@
import { IconButton, IconButtonProps, useTheme } from "@mui/material";
import { ForwardRefExoticComponent, RefAttributes } from "react";
import { LinkProps } from "react-router-dom";
import { deepmerge } from "@mui/utils";
type Props = IconButtonProps & {
component?: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>>;
to?: string;
};
export function WalletButton(props: Props) {
const theme = useTheme();
return (
<IconButton
{...deepmerge({
sx: {
height: 36,
width: 36,
p: 0,
borderRadius: "6px",
backgroundColor: theme.palette.background.default,
color: theme.palette.gray.main,
"&:hover": {
color: theme.palette.background.default,
backgroundColor: theme.palette.gray.main,
},
"&:active": {
backgroundColor: theme.palette.purple.main,
color: "white",
},
},
}, props)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 36 37" fill="none">
<path d="M26.571 16.051v-5c0-.789-.64-1.428-1.428-1.428H9.429c-.79 0-1.429.64-1.429 1.428v14.286c0 .789.64 1.428 1.429 1.428h15.714c.789 0 1.428-.64 1.428-1.428v-5m1.33-5h-7.044a2.857 2.857 0 0 0 0 5.714h7.044a.099.099 0 0 0 .099-.099v-5.516a.1.1 0 0 0-.099-.1Z" stroke="currentColor" strokeWidth="1.5" />
</svg>
</IconButton>
);
}

9
lib/components/index.ts Normal file

@ -0,0 +1,9 @@
export * from "./AvatarButton";
export * from "./BurgerButton";
export * from "./CloseButton";
export * from "./CloseButtonSmall";
export * from "./LogoutButton";
export * from "./PenaLink";
export * from "./PenaTextField";
export * from "./theme";
export * from "./WalletButton";

612
lib/components/theme.ts Normal file

@ -0,0 +1,612 @@
import { createTheme } from "@mui/material";
export const penaMuiTheme = createTheme({
breakpoints: {
values: {
xs: 300,
sm: 560,
md: 900,
lg: 1200,
xl: 1536,
},
},
palette: {
mode: "light",
primary: {
main: "#000000",
},
secondary: {
main: "#252734",
},
text: {
primary: "#000000",
secondary: "#7E2AEA",
},
background: {
default: "#F2F3F7",
},
purple: {
main: "#7E2AEA",
dark: "#581CA7",
light: "#944FEE",
},
bg: {
main: "#333647",
dark: "#252734",
},
gray: {
main: "#9A9AAF",
dark: "#4D4D4D",
},
orange: {
main: "#FB5607",
light: "#FC712F",
},
},
components: {
MuiButton: {
variants: [
{
props: { variant: "pena-contained-dark" },
style: ({ theme }) => theme.unstable_sx({
minWidth: "180px",
py: "9px",
px: "43px",
borderRadius: "8px",
boxShadow: "none",
fontSize: "18px",
lineHeight: "24px",
fontWeight: 400,
textTransform: "none",
color: "white",
backgroundColor: theme.palette.purple.main,
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
color: "white",
backgroundColor: theme.palette.purple.light,
border: `1px solid ${theme.palette.purple.light}`,
},
"&:active": {
color: theme.palette.purple.main,
backgroundColor: "white",
border: `1px solid ${theme.palette.purple.main}`,
}
}),
},
{
props: { variant: "pena-outlined-dark" },
style: ({ theme }) => theme.unstable_sx({
minWidth: "180px",
py: "9px",
px: "43px",
borderRadius: "8px",
boxShadow: "none",
fontSize: "18px",
lineHeight: "24px",
fontWeight: 400,
textTransform: "none",
color: "white",
backgroundColor: "rgb(0 0 0 / 0)",
border: `1px solid white`,
"&:hover": {
color: "white",
backgroundColor: theme.palette.bg.dark,
border: `1px solid white`,
},
"&:active": {
color: "white",
backgroundColor: theme.palette.purple.main,
border: `1px solid ${theme.palette.purple.main}`,
}
}),
},
{
props: { variant: "pena-contained-light" },
style: ({ theme }) => theme.unstable_sx({
minWidth: "180px",
py: "9px",
px: "43px",
borderRadius: "8px",
boxShadow: "none",
fontSize: "18px",
lineHeight: "24px",
fontWeight: 400,
textTransform: "none",
color: theme.palette.bg.dark,
backgroundColor: "white",
border: `1px solid white`,
"&:hover": {
color: theme.palette.bg.dark,
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.background.default}`,
},
"&:active": {
color: "white",
backgroundColor: "black",
border: `1px solid black`,
}
}),
},
{
props: { variant: "pena-outlined-light" },
style: ({ theme }) => theme.unstable_sx({
minWidth: "180px",
py: "9px",
px: "43px",
borderRadius: "8px",
boxShadow: "none",
fontSize: "18px",
lineHeight: "24px",
fontWeight: 400,
textTransform: "none",
color: "white",
backgroundColor: "rgb(0 0 0 / 0)",
border: `1px solid white`,
"&:hover": {
color: "white",
backgroundColor: "#581CA7",
border: `1px solid white`,
},
"&:active": {
color: "white",
backgroundColor: "black",
border: `1px solid black`,
}
}),
},
{
props: { variant: "pena-outlined-purple" },
style: ({ theme }) => theme.unstable_sx({
minWidth: "180px",
py: "9px",
px: "43px",
borderRadius: "8px",
boxShadow: "none",
fontSize: "18px",
lineHeight: "24px",
fontWeight: 400,
textTransform: "none",
color: theme.palette.purple.main,
backgroundColor: "rgb(0 0 0 / 0)",
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
color: theme.palette.purple.main,
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.purple.main}`,
},
"&:active": {
color: "white",
backgroundColor: theme.palette.purple.dark,
border: `1px solid ${theme.palette.purple.dark}`,
}
}),
},
{
props: { variant: "pena-navitem-dark" },
style: ({ theme }) => theme.unstable_sx({
p: 0,
boxShadow: "none",
fontSize: "16px",
lineHeight: "20px",
fontWeight: 500,
textTransform: "none",
whiteSpace: "nowrap",
color: "white",
backgroundColor: "rgb(0 0 0 / 0)",
"&:hover": {
color: theme.palette.purple.light,
backgroundColor: "rgb(0 0 0 / 0)",
},
"&:active": {
color: theme.palette.purple.light,
backgroundColor: "rgb(0 0 0 / 0)",
}
}),
},
{
props: { variant: "pena-navitem-light" },
style: ({ theme }) => theme.unstable_sx({
p: 0,
boxShadow: "none",
fontSize: "16px",
lineHeight: "20px",
fontWeight: 500,
textTransform: "none",
whiteSpace: "nowrap",
color: "black",
backgroundColor: "rgb(0 0 0 / 0)",
"&:hover": {
color: theme.palette.purple.main,
backgroundColor: "rgb(0 0 0 / 0)",
},
"&:active": {
color: theme.palette.purple.main,
backgroundColor: "rgb(0 0 0 / 0)",
}
}),
},
{
props: { variant: "pena-contained-white1" },
style: ({ theme }) => theme.unstable_sx({
minWidth: "180px",
py: "9px",
px: "43px",
borderRadius: "8px",
boxShadow: "none",
fontSize: "18px",
lineHeight: "24px",
fontWeight: 400,
textTransform: "none",
color: theme.palette.purple.main,
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.gray.main}`,
"&:hover": {
color: "white",
backgroundColor: theme.palette.purple.main,
border: `1px solid ${theme.palette.purple.main}`,
},
"&:active": {
color: "white",
backgroundColor: "black",
border: `1px solid black`,
}
}),
},
{
props: { variant: "pena-contained-white2" },
style: ({ theme }) => theme.unstable_sx({
minWidth: "180px",
py: "9px",
px: "43px",
borderRadius: "8px",
boxShadow: "none",
fontSize: "18px",
lineHeight: "24px",
fontWeight: 400,
textTransform: "none",
color: "black",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.background.default}`,
"&:hover": {
color: "white",
backgroundColor: theme.palette.purple.light,
border: `1px solid white`,
},
"&:active": {
color: theme.palette.purple.main,
backgroundColor: "white",
border: `1px solid ${theme.palette.purple.light}`,
}
}),
},
{
props: {
variant: "pena-text",
},
style: ({ theme }) => ({
color: theme.palette.purple.main,
padding: 0,
textTransform: "none",
textDecoration: "underline",
textUnderlineOffset: "7px",
fontSize: "16px",
fontWeight: 500,
lineHeight: "20px",
}),
},
],
defaultProps: {
disableTouchRipple: true,
},
},
MuiIconButton: {
defaultProps: {
disableTouchRipple: true,
},
},
MuiTypography: {
defaultProps: {
variantMapping: {
p1: "p",
t1: "p",
"pena-h1": "h1",
"pena-card-header1": "h5",
}
},
},
MuiAlert: {
styleOverrides: {
filledError: {
backgroundColor: "#FB5607",
},
root: {
borderRadius: "8px",
}
}
},
MuiPagination: {
variants: [
{
props: { variant: "pena-pagination" },
style: {
marginRight: "-15px",
marginLeft: "-15px",
"& .MuiPaginationItem-root": {
height: "30px",
width: "30px",
minWidth: "30px",
marginLeft: "5px",
marginRight: "5px",
backgroundColor: "white",
color: "black",
fontSize: "16px",
lineHeight: "20px",
fontWeight: 400,
borderRadius: "5px",
"&.Mui-selected": {
backgroundColor: "white",
color: "#7E2AEA",
fontWeight: 500,
},
"&:hover": {
backgroundColor: "#ffffff55",
},
"&:active": {
backgroundColor: "#7F2CEA",
color: "white",
},
boxShadow: `
0px 77.2727px 238.773px rgba(210, 208, 225, 0.24),
0px 32.2827px 99.7535px rgba(210, 208, 225, 0.172525),
0px 17.2599px 53.333px rgba(210, 208, 225, 0.143066),
0px 9.67574px 29.8981px rgba(210, 208, 225, 0.12),
0px 5.13872px 15.8786px rgba(210, 208, 225, 0.0969343),
0px 2.13833px 6.60745px rgba(210, 208, 225, 0.0674749)
`,
},
"& .MuiPaginationItem-previousNext": {
backgroundColor: "#7E2AEA",
color: "white",
marginLeft: "15px",
marginRight: "15px",
"&:hover": {
backgroundColor: "#995DED",
},
},
}
}
],
},
MuiSwitch: {
styleOverrides: {
root: {
color: "#7E2AEA",
height: "50px",
width: "69px",
"& .MuiSwitch-switchBase.Mui-checked+.MuiSwitch-track": {
backgroundColor: "#7E2AEA",
opacity: 1,
},
},
track: {
height: "12px",
alignSelf: "center",
backgroundColor: "#00000000",
opacity: 1,
border: "1px solid #9A9AAF",
},
thumb: {
height: "32px",
width: "32px",
border: `6px solid #7E2AEA`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD
`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD
`,
},
},
},
},
},
typography: palette => ({
h5: {
fontSize: "24px",
lineHeight: "28.44px",
fontWeight: 500,
},
button: {
fontSize: "18px",
lineHeight: "24px",
fontWeight: 400,
textTransform: "none",
},
body1: {
fontSize: "18px",
lineHeight: "21.33px",
fontWeight: 400,
},
body2: {
fontSize: "16px",
lineHeight: "20px",
fontWeight: 500,
},
p1: {
fontSize: "20px",
lineHeight: "24px",
fontWeight: 500,
},
price: {
fontWeight: 500,
fontSize: "20px",
lineHeight: "24px",
color: palette.gray.dark,
},
oldPrice: {
fontWeight: 400,
fontSize: "18px",
lineHeight: "21px",
textDecorationLine: "line-through",
color: palette.orange.main,
},
t1: {
display: "block",
fontWeight: 400,
fontSize: "18px",
lineHeight: "21.33px",
},
fontFamily: [
"Rubik",
"-apple-system",
"BlinkMacSystemFont",
'"Segoe UI"',
'"Helvetica Neue"',
"Arial",
"sans-serif",
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(","),
}),
});
penaMuiTheme.typography["pena-h1"] = {
fontSize: "70px",
fontWeight: 500,
lineHeight: "100%",
[penaMuiTheme.breakpoints.down("md")]: {
fontSize: "36px",
lineHeight: "100%",
},
};
penaMuiTheme.typography["pena-h3"] = {
color: "#000000",
fontWeight: 500,
fontSize: "36px",
lineHeight: "100%",
[penaMuiTheme.breakpoints.down("md")]: {
fontSize: "30px",
lineHeight: "100%",
},
};
penaMuiTheme.typography["pena-card-header1"] = {
fontWeight: 500,
fontSize: "24px",
lineHeight: "100%",
[penaMuiTheme.breakpoints.down("md")]: {
fontSize: "21px",
lineHeight: "100%",
},
};
penaMuiTheme.typography.h2 = {
fontSize: "70px",
lineHeight: "70px",
fontWeight: 500,
[penaMuiTheme.breakpoints.down("md")]: {
fontSize: "42px",
lineHeight: "50px",
}
};
penaMuiTheme.typography.h4 = {
fontSize: "36px",
lineHeight: "42.66px",
fontWeight: 500,
[penaMuiTheme.breakpoints.down("md")]: {
fontSize: "24px",
lineHeight: "28.44px",
}
};
penaMuiTheme.typography.infographic = {
fontSize: "80px",
lineHeight: "94.8px",
fontWeight: 400,
[penaMuiTheme.breakpoints.down("md")]: {
fontSize: "50px",
lineHeight: "59px",
fontWeight: 400,
}
};
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
"pena-contained-light": true;
"pena-outlined-light": true;
"pena-contained-dark": true;
"pena-outlined-dark": true;
"pena-outlined-purple": true;
"pena-navitem-light": true;
"pena-navitem-dark": true;
"pena-contained-white1": true;
"pena-contained-white2": true;
"pena-text": true;
}
}
declare module '@mui/material/Pagination' {
interface PaginationPropsVariantOverrides {
"pena-pagination": true;
}
}
declare module "@mui/material/styles" {
interface Palette {
purple: Palette["primary"],
bg: Palette["primary"],
gray: Palette["primary"],
orange: Palette["primary"],
}
interface PaletteOptions {
purple?: PaletteOptions["primary"],
bg?: PaletteOptions["primary"],
gray?: PaletteOptions["primary"],
orange?: PaletteOptions["primary"],
}
interface TypographyVariants {
infographic: React.CSSProperties;
p1: React.CSSProperties;
price: React.CSSProperties;
oldPrice: React.CSSProperties;
t1: React.CSSProperties;
"pena-card-header1": React.CSSProperties;
"pena-h1": React.CSSProperties;
"pena-h3": React.CSSProperties;
}
interface TypographyVariantsOptions {
infographic?: React.CSSProperties;
p1?: React.CSSProperties;
price?: React.CSSProperties;
oldPrice?: React.CSSProperties;
t1?: React.CSSProperties;
"pena-card-header1"?: React.CSSProperties;
"pena-h1"?: React.CSSProperties;
"pena-h3"?: React.CSSProperties;
}
}
declare module "@mui/material/Typography" {
interface TypographyPropsVariantOverrides {
infographic: true;
p1: true;
price: true;
oldPrice: true;
t1: true;
"pena-card-header1": true;
"pena-h1": true;
"pena-h3": true;
}
}
declare module "@mui/material/Switch" {
interface SwitchPropsVariantOverrides {
"pena-switch": true;
}
}

1
lib/decorators/index.ts Normal file

@ -0,0 +1 @@
export * from "./throttle";

@ -0,0 +1,29 @@
export type ThrottledFunction<T extends (...args: any) => any> = (...args: Parameters<T>) => void;
export function throttle<T extends (...args: any) => any>(func: T, ms: number): ThrottledFunction<T> {
let isThrottled = false;
let savedArgs: Parameters<T> | null;
let savedThis: any;
function wrapper(this: any, ...args: Parameters<T>) {
if (isThrottled) {
savedArgs = args;
savedThis = this;
return;
}
func.apply(this, args);
isThrottled = true;
setTimeout(function () {
isThrottled = false;
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}

7
lib/env.d.ts vendored Normal file

@ -0,0 +1,7 @@
export declare global {
namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test';
}
}
}

12
lib/hooks/index.ts Normal file

@ -0,0 +1,12 @@
export * from "./useAllTariffsFetcher";
export * from "./useDebounce";
export * from "./useEventListener";
export * from "./usePrivilegeFetcher";
export * from "./useSSESubscription";
export * from "./usePaginatedTariffsFetcher";
export * from "./useThrottle";
export * from "./useTicketMessages";
export * from "./useTickets";
export * from "./useToken";
export * from "./useUserAccountFetcher";
export * from "./useUserFetcher";

@ -0,0 +1,59 @@
import { useRef, useLayoutEffect, useEffect } from "react";
import { GetTariffsResponse, Tariff } from "../model/tariff";
import { makeRequest } from "../api/makeRequest";
export function useAllTariffsFetcher({
enabled = true,
baseUrl = import.meta.env.VITE__APP_DOMAIN + "/strator/tariff", onSuccess,
onError,
}: {
enabled?: boolean;
baseUrl?: string;
onSuccess: (response: Tariff[]) => void;
onError?: (error: Error) => void;
}) {
const onNewTariffsRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewTariffsRef.current = onSuccess;
onErrorRef.current = onError;
}, [onError, onSuccess]);
useEffect(() => {
if (!enabled) return;
const controller = new AbortController();
async function getPaginatedTariffs() {
let apiPage = 1;
const tariffsPerPage = 100;
let isDone = false;
while (!isDone) {
try {
const result = await makeRequest<never, GetTariffsResponse>({
url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get",
useToken: true,
signal: controller.signal,
});
if (result) onNewTariffsRef.current(result.tariffs);
if (result.totalPages < apiPage) {
apiPage++;
} else {
isDone = true;
}
} catch (error) {
onErrorRef.current?.(error as Error);
isDone = true;
}
}
}
getPaginatedTariffs();
return () => controller.abort();
}, [baseUrl, enabled]);
}

15
lib/hooks/useDebounce.ts Normal file

@ -0,0 +1,15 @@
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delay: number) {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}

@ -0,0 +1,80 @@
import { useEffect, useRef, RefObject, useLayoutEffect } from "react";
// https://usehooks-ts.com/react-hook/use-event-listener
// MediaQueryList Event based useEventListener interface
export function useEventListener<K extends keyof MediaQueryListEventMap>(
eventName: K,
handler: (event: MediaQueryListEventMap[K]) => void,
element: RefObject<MediaQueryList>,
options?: boolean | AddEventListenerOptions,
): void;
// Window Event based useEventListener interface
export function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element?: undefined,
options?: boolean | AddEventListenerOptions,
): void;
// Element Event based useEventListener interface
export function useEventListener<
K extends keyof HTMLElementEventMap,
T extends HTMLElement = HTMLDivElement,
>(
eventName: K,
handler: (event: HTMLElementEventMap[K]) => void,
element: RefObject<T>,
options?: boolean | AddEventListenerOptions,
): void;
// Document Event based useEventListener interface
export function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (event: DocumentEventMap[K]) => void,
element: RefObject<Document>,
options?: boolean | AddEventListenerOptions,
): void;
export function useEventListener<
KW extends keyof WindowEventMap,
KH extends keyof HTMLElementEventMap,
KM extends keyof MediaQueryListEventMap,
T extends HTMLElement | MediaQueryList | void = void,
>(
eventName: KW | KH | KM,
handler: (
event:
| WindowEventMap[KW]
| HTMLElementEventMap[KH]
| MediaQueryListEventMap[KM]
| Event,
) => void,
element?: RefObject<T>,
options?: boolean | AddEventListenerOptions,
) {
// Create a ref that stores handler
const savedHandler = useRef(handler);
useLayoutEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current ?? window;
if (!(targetElement && targetElement.addEventListener)) return;
// Create event listener that calls handler export function stored in ref
const listener: typeof handler = event => savedHandler.current(event);
targetElement.addEventListener(eventName, listener, options);
// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, listener, options);
};
}, [eventName, element, options]);
}

@ -0,0 +1,51 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { makeRequest } from "../api";
import { Tariff, GetTariffsResponse } from "../model/tariff";
import { devlog } from "../utils";
import { FetchState } from "../model/fetchState";
export function usePaginatedTariffsFetcher({ enabled = true, url, tariffsPerPage, apiPage, onSuccess, onError, onFetchStateChange }: {
enabled?: boolean;
url: string;
tariffsPerPage: number;
apiPage: number;
onSuccess: (response: Tariff[]) => void;
onError?: (error: Error) => void;
onFetchStateChange?: (state: FetchState) => void;
}) {
const onNewTariffsRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
const onFetchStateChangeRef = useRef(onFetchStateChange);
useLayoutEffect(() => {
onNewTariffsRef.current = onSuccess;
onErrorRef.current = onError;
onFetchStateChangeRef.current = onFetchStateChange;
}, [onSuccess, onError, onFetchStateChange]);
useEffect(function fetchTickets() {
if (!enabled) return;
const controller = new AbortController();
onFetchStateChangeRef.current?.("fetching");
makeRequest<never, GetTariffsResponse>({
url,
method: "get",
useToken: true,
signal: controller.signal,
}).then((result) => {
devlog("GetTariffsResponse", result);
if (result.tariffs.length > 0) {
onNewTariffsRef.current(result.tariffs);
onFetchStateChangeRef.current?.("idle");
} else onFetchStateChangeRef.current?.("all fetched");
}).catch(error => {
devlog("Error fetching tariffs", error);
onErrorRef.current?.(error);
});
return () => controller.abort();
}, [apiPage, enabled, tariffsPerPage, url]);
}

@ -0,0 +1,38 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { makeRequest } from "../api";
import { Privilege } from "../model";
export function usePrivilegeFetcher({
onSuccess,
url = import.meta.env.VITE__APP_DOMAIN + "/strator/privilege", onError,
}: {
onSuccess: (response: Privilege[]) => void;
url?: string;
onError?: (error: Error) => void;
}) {
const onSuccessRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onSuccessRef.current = onSuccess;
onErrorRef.current = onError;
}, [onSuccess, onError]);
useEffect(function fetchTickets() {
const controller = new AbortController();
makeRequest<never, Privilege[]>({
url,
method: "get",
useToken: true,
signal: controller.signal,
}).then((result) => {
onSuccessRef.current(result);
}).catch(error => {
onErrorRef.current?.(error);
});
return () => controller.abort();
}, [url]);
}

@ -0,0 +1,47 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import ReconnectingEventSource from "reconnecting-eventsource";
import { devlog } from "../utils";
export function useSSESubscription<T>({ enabled = true, url, onNewData, onDisconnect, marker = "", consolelog = false }: { enabled?: boolean;
url: string;
onNewData: (data: T[]) => void;
onDisconnect?: () => void;
marker?: string;
consolelog?: boolean;
}) {
const onNewDataRef = useRef(onNewData);
const onDisconnectRef = useRef(onDisconnect);
useLayoutEffect(() => {
onNewDataRef.current = onNewData;
onDisconnectRef.current = onDisconnect;
}, [onNewData, onDisconnect]);
useEffect(() => {
if (!enabled) return;
const eventSource = new ReconnectingEventSource(url);
eventSource.addEventListener("open", () => devlog(`EventSource connected with ${url}`));
eventSource.addEventListener("close", () => devlog(`EventSource closed with ${url}`));
eventSource.addEventListener("message", event => {
try {
if (consolelog) console.log(event);
const newData = JSON.parse(event.data) as T;
devlog(`new SSE: ${marker}`, newData);
onNewDataRef.current([newData]);
} catch (error) {
devlog(`SSE parsing error: ${marker}`, event.data, error);
}
});
eventSource.addEventListener("error", event => {
devlog("SSE Error:", event);
});
return () => {
eventSource.close();
onDisconnectRef.current?.();
};
}, [enabled, marker, url]);
}

22
lib/hooks/useThrottle.ts Normal file

@ -0,0 +1,22 @@
import { useState, useEffect, useRef } from "react";
export function useThrottle<T>(value: T, delay: number) {
const [throttledValue, setThrottledValue] = useState<T>(value);
const time = useRef<number>(0);
useEffect(() => {
const now = Date.now();
if (now > time.current + delay) {
time.current = now;
setThrottledValue(value);
} else {
const handler = setTimeout(() => {
setThrottledValue(value);
}, delay);
return () => clearTimeout(handler);
}
}, [value, delay]);
return throttledValue;
}

@ -0,0 +1,58 @@
import { useEffect, useRef, useLayoutEffect } from "react";
import { TicketMessage, GetMessagesRequest, GetMessagesResponse } from "../model";
import { devlog } from "../utils";
import { makeRequest } from "../api";
import { FetchState } from "../model/fetchState";
export function useTicketMessages({ url, messageApiPage, messagesPerPage, ticketId, isUnauth = false, onSuccess, onError, onFetchStateChange }: {
url: string;
ticketId: string | undefined;
messagesPerPage: number;
messageApiPage: number;
isUnauth?: boolean;
onSuccess: (messages: TicketMessage[]) => void;
onError?: (error: Error) => void;
onFetchStateChange?: (state: FetchState) => void;
}) {
const onNewMessagesRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
const onFetchStateChangeRef = useRef(onFetchStateChange);
useLayoutEffect(() => {
onNewMessagesRef.current = onSuccess;
onErrorRef.current = onError;
onFetchStateChangeRef.current = onFetchStateChange;
}, [onSuccess, onError, onFetchStateChange]);
useEffect(function fetchTicketMessages() {
if (!ticketId) return;
const controller = new AbortController();
onFetchStateChangeRef.current?.("fetching");
makeRequest<GetMessagesRequest, GetMessagesResponse>({
url,
method: "POST",
useToken: !isUnauth,
body: {
amt: messagesPerPage,
page: messageApiPage,
ticket: ticketId,
},
signal: controller.signal,
withCredentials: isUnauth,
}).then(result => {
devlog("GetMessagesResponse", result);
if (result?.length > 0) {
onNewMessagesRef.current(result);
onFetchStateChangeRef.current?.("idle");
} else onFetchStateChangeRef.current?.("all fetched");
}).catch(error => {
devlog("Error fetching messages", error);
onErrorRef.current?.(error);
});
return () => controller.abort();
}, [isUnauth, messageApiPage, messagesPerPage, ticketId, url]);
}

56
lib/hooks/useTickets.ts Normal file

@ -0,0 +1,56 @@
import { useEffect, useRef, useLayoutEffect } from "react";
import { GetTicketsResponse, GetTicketsRequest } from "../model";
import { devlog } from "../utils";
import { makeRequest } from "../api";
import { FetchState } from "../model/fetchState";
export function useTicketsFetcher({ enabled = true, url, ticketsPerPage, ticketApiPage, onSuccess, onError, onFetchStateChange }: {
enabled?: boolean;
url: string;
ticketsPerPage: number;
ticketApiPage: number;
onSuccess: (response: GetTicketsResponse) => void;
onError?: (error: Error) => void;
onFetchStateChange?: (state: FetchState) => void;
}) {
const onNewTicketsRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
const onFetchStateChangeRef = useRef(onFetchStateChange);
useLayoutEffect(() => {
onNewTicketsRef.current = onSuccess;
onErrorRef.current = onError;
onFetchStateChangeRef.current = onFetchStateChange;
}, [onSuccess, onError, onFetchStateChange]);
useEffect(function fetchTickets() {
if (!enabled) return;
const controller = new AbortController();
onFetchStateChangeRef.current?.("fetching");
makeRequest<GetTicketsRequest, GetTicketsResponse>({
url,
method: "POST",
useToken: true,
body: {
amt: ticketsPerPage,
page: ticketApiPage,
status: "open",
},
signal: controller.signal,
}).then((result) => {
devlog("GetTicketsResponse", result);
if (result.data) {
onNewTicketsRef.current(result);
onFetchStateChangeRef.current?.("idle");
} else onFetchStateChangeRef.current?.("all fetched");
}).catch(error => {
devlog("Error fetching tickets", error);
onErrorRef.current?.(error);
});
return () => controller.abort();
}, [enabled, ticketApiPage, ticketsPerPage, url]);
}

6
lib/hooks/useToken.ts Normal file

@ -0,0 +1,6 @@
import { useAuthStore } from "../stores/auth";
export function useToken() {
return useAuthStore(state => state.token);
}

@ -0,0 +1,55 @@
import { isAxiosError } from "axios";
import { useEffect, useLayoutEffect, useRef } from "react";
import { UserAccount } from "../model/account";
import { makeRequest } from "../api/makeRequest";
import { devlog } from "../utils/devlog";
import { createUserAccount } from "../api/account";
export function useUserAccountFetcher<T = UserAccount>({ onError, onNewUserAccount, versionOfCustomer="v1.0.0", url, userId }: {
url: string;
userId: string | null;
versionOfCustomer?: string;
onNewUserAccount: (response: T) => void;
onError?: (error: any) => void;
}) {
const onNewUserAccountRef = useRef(onNewUserAccount);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewUserAccountRef.current = onNewUserAccount;
onErrorRef.current = onError;
}, [onError, onNewUserAccount]);
useEffect(() => {
if (!userId) return;
const controller = new AbortController();
makeRequest<never, T>({
url,
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
}).then(result => {
devlog("User account", result);
onNewUserAccountRef.current(result);
}).catch(error => {
devlog("Error fetching user account", error);
if (isAxiosError(error) && error.response?.status === 404) {
createUserAccount(controller.signal, url, versionOfCustomer).then(result => {
devlog("Created user account", result);
onNewUserAccountRef.current(result as T);
}).catch(error => {
devlog("Error creating user account", error);
onErrorRef.current?.(error);
});
} else {
onErrorRef.current?.(error);
}
});
return () => controller.abort();
}, [url, userId]);
}

@ -0,0 +1,43 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { User } from "../model/user";
import { devlog } from "../utils/devlog";
import { makeRequest } from "../api/makeRequest";
export function useUserFetcher({ onError, onNewUser, url, userId }: {
url: string;
userId: string | null;
onNewUser: (response: User) => void;
onError?: (error: any) => void;
}) {
const onNewUserRef = useRef(onNewUser);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewUserRef.current = onNewUser;
onErrorRef.current = onError;
}, [onError, onNewUser]);
useEffect(() => {
if (!userId) return;
const controller = new AbortController();
makeRequest<never, User>({
url,
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
}).then(result => {
devlog("User", result);
onNewUserRef.current(result);
}).catch(error => {
devlog("Error fetching user", error);
onErrorRef.current?.(error);
});
return () => controller.abort();
}, [url, userId]);
}

8
lib/index.ts Normal file

@ -0,0 +1,8 @@
export * from "./api";
export * from "./components";
export * from "./decorators";
export * from "./hooks";
export * from "./model";
export * from "./stores";
export * from "./utils";
export type * from "./model";

25
lib/model/account.ts Normal file

@ -0,0 +1,25 @@
export interface UserAccount {
_id: string;
userId: string;
name: UserName;
cart: string[];
wallet: {
currency: string;
cash: number;
purchasesAmount: number;
spent: number;
money: number;
};
status: "no" | "nko" | "org";
isDeleted: false;
createdAt: string;
updatedAt: string;
deletedAt: string;
}
export interface UserName {
firstname?: string;
secondname?: string;
middlename?: string;
orgname?: string;
}

21
lib/model/auth.ts Normal file

@ -0,0 +1,21 @@
export interface RegisterRequest {
login: string;
password: string;
phoneNumber: string;
}
export interface RegisterResponse {
accessToken: string;
login: string;
email: string;
phoneNumber: string;
refreshToken: string;
_id: string;
}
export interface LoginRequest {
login: string;
password: string;
}
export type LoginResponse = RegisterResponse;

32
lib/model/cart.ts Normal file

@ -0,0 +1,32 @@
import { Discount } from "./discount";
export type PrivilegeCartData = {
serviceKey: string;
privilegeId: string;
description: string;
price: number;
amount: number;
appliedDiscounts: Set<Discount>;
};
export type TariffCartData = {
name: string;
id: string;
price: number;
isCustom: boolean;
privileges: PrivilegeCartData[];
};
export type ServiceCartData = {
serviceKey: string;
tariffs: TariffCartData[];
price: number;
};
export type CartData = {
services: ServiceCartData[];
priceBeforeDiscounts: number;
priceAfterDiscounts: number;
allAppliedDiscounts: Set<Discount>;
};

@ -0,0 +1,12 @@
import { CustomPrivilegeWithAmount } from "./privilege";
type ServiceKey = string;
export type PrivilegeWithoutPrice = Omit<CustomPrivilegeWithAmount, "price">;
export type CustomTariffUserValues = Record<string, number>;
export type CustomTariffUserValuesMap = Record<ServiceKey, CustomTariffUserValues>;
export type ServiceKeyToPriceMap = Record<ServiceKey, number>;

40
lib/model/discount.ts Normal file

@ -0,0 +1,40 @@
export interface Discount {
ID: string;
Name: string;
Layer: number;
Description: string;
Condition: {
Period?: {
From: string;
To: string;
};
User: string;
UserType?: string;
Coupon: string;
PurchasesAmount?: string;
CartPurchasesAmount?: string;
Product?: string;
Term?: string;
Usage?: string;
PriceFrom?: string;
Group?: string;
};
Target: {
Products: {
ID: string;
Factor: number;
Overhelm: boolean;
}[];
Factor: number;
TargetScope: string;
TargetGroup: string;
Overhelm: boolean;
};
Audit: {
UpdatedAt: string;
CreatedAt: string;
DeletedAt?: string;
Deleted: boolean;
};
Deprecated: boolean;
}

1
lib/model/fetchState.ts Normal file

@ -0,0 +1 @@
export type FetchState = "fetching" | "idle" | "all fetched";

10
lib/model/index.ts Normal file

@ -0,0 +1,10 @@
export type * from "./account";
export type * from "./auth";
export type * from "./cart";
export type * from "./customTariffs";
export type * from "./discount";
export type * from "./fetchState";
export type * from "./privilege";
export type * from "./tariff";
export type * from "./ticket";
export type * from "./user";

30
lib/model/privilege.ts Normal file

@ -0,0 +1,30 @@
export interface Privilege {
name: string;
privilegeId: string;
serviceKey: string;
description: string;
type: "day" | "count";
value: PrivilegeValueType;
price: number;
amount: number;
}
export interface CustomPrivilege {
_id: string;
name: string;
privilegeId: string;
serviceKey: string;
description: string;
type: "day" | "count";
value: PrivilegeValueType;
price: number;
updatedAt?: string;
isDeleted?: boolean;
createdAt?: string;
}
export type PrivilegeMap = Record<string, CustomPrivilege[]>;
export type PrivilegeValueType = "шаблон" | "день" | "МБ" | "заявка";
export type CustomPrivilegeWithAmount = CustomPrivilege & { amount: number; };

21
lib/model/tariff.ts Normal file

@ -0,0 +1,21 @@
import { Privilege } from "./privilege";
export interface GetTariffsResponse {
totalPages: number;
tariffs: Tariff[];
}
export interface Tariff {
_id: string;
name: string;
description?: string;
order?: number;
price?: number;
isCustom: boolean;
privileges: Privilege[];
isDeleted: boolean;
createdAt?: string;
updatedAt?: string;
deletedAt?: string;
}

69
lib/model/ticket.ts Normal file

@ -0,0 +1,69 @@
export interface CreateTicketRequest {
Title: string;
Message: string;
System?: boolean;
}
export interface CreateTicketResponse {
Ticket: string;
sess: string;
}
export interface SendTicketMessageRequest {
message: string;
ticket: string;
lang: string;
files: string[];
}
export type TicketStatus = "open";
export interface GetTicketsRequest {
amt: number;
/** Пагинация начинается с индекса 0 */
page: number;
srch?: string;
status?: TicketStatus;
}
export interface GetTicketsResponse {
count: number;
data: Ticket[] | null;
}
export interface Ticket {
id: string;
user: string;
sess: string;
ans: string;
state: string;
top_message: TicketMessage;
title: string;
created_at: string;
updated_at: string;
rate: number;
origin: string;
}
export interface TicketMessage {
id: string;
ticket_id: string;
user_id: string,
session_id: string;
message: string;
files: string[],
shown: { [key: string]: number; },
request_screenshot: string,
created_at: string;
}
export interface GetMessagesRequest {
amt: number;
page: number;
srch?: string;
ticket: string;
}
export type GetMessagesResponse = TicketMessage[];

10
lib/model/user.ts Normal file

@ -0,0 +1,10 @@
export interface User {
_id: string;
login: string;
email: string;
phoneNumber: string;
isDeleted: boolean;
createdAt: string;
updatedAt: string;
deletedAt?: string;
}

24
lib/stores/auth.ts Normal file

@ -0,0 +1,24 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface AuthStore {
token: string;
}
export const useAuthStore = create<AuthStore>()(
persist(
(set, get) => ({
token: "",
}),
{
name: "token",
}
)
);
export const getAuthToken = () => useAuthStore.getState().token;
export const setAuthToken = (token: string) => useAuthStore.setState({ token });
export const clearAuthToken = () => useAuthStore.setState({ token: "" });

1
lib/stores/index.ts Normal file

@ -0,0 +1 @@
export * from "./auth";

@ -0,0 +1,30 @@
import { isAxiosError } from "axios";
const translateMessage: Record<string, string> = {
"user not found": "Пользователь не найден",
"invalid password": "Неправильный пароль",
"field <password> is empty": "Поле \"Пароль\" не заполнено",
"field <login> is empty": "Поле \"Логин\" не заполнено",
"field <email> is empty": "Поле \"E-mail\" не заполнено",
"field <phoneNumber> is empty": "Поле \"Номер телефона\" не заполнено",
"user with this email or login is exist": "Пользователь уже существует",
};
export function getMessageFromFetchError(error: any, defaultMessage = "Что-то пошло не так. Повторите попытку позже"): string | null {
const rawMessage = error.response?.data?.message;
const translatedMessage = translateMessage[rawMessage];
if (translatedMessage) return translatedMessage;
if (isAxiosError(error)) {
switch (error.code) {
case "ERR_NETWORK": return "Ошибка сети";
case "ERR_CANCELED": return null;
}
}
if (process.env.NODE_ENV === "development") return rawMessage ?? error.message ?? defaultMessage;
return defaultMessage;
}

@ -0,0 +1,21 @@
import { expect, test, describe } from "vitest";
import { calcCart } from "./calcCart";
import { testDiscounts } from "./mockData/discounts";
import { cartTestResults } from "./mockData/results";
import { testTariffs } from "./mockData/tariffs";
describe("Cart calculation", () => {
for (let i = 0; i < cartTestResults.length; i++) {
test(`Cart calculation №${i}`, () => {
const usedTariffsMask = cartTestResults[i][1];
const isNkoApplied = Boolean(usedTariffsMask.pop());
const tariffs = testTariffs.filter((_, index) => (usedTariffsMask[index] === 1));
const cart = calcCart(tariffs, testDiscounts, 0, "someuserid", isNkoApplied);
expect(cart.priceAfterDiscounts).toBeCloseTo(cartTestResults[i][0]);
});
}
});

192
lib/utils/cart/calcCart.ts Normal file

@ -0,0 +1,192 @@
import { CartData, PrivilegeCartData, TariffCartData } from "../../model/cart";
import { Discount } from "../../model/discount";
import { Tariff } from "../../model/tariff";
import { findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findNkoDiscount, findPrivilegeDiscount, findServiceDiscount } from "./utils";
export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number, userId: string, isUserNko?: boolean): CartData {
const cartData: CartData = {
services: [],
priceBeforeDiscounts: 0,
priceAfterDiscounts: 0,
allAppliedDiscounts: new Set(),
};
const privilegeAmountById = new Map<string, number>();
const servicePriceByKey = new Map<string, number>();
tariffs.forEach(tariff => {
if (tariff.privileges === undefined) return;
if (
(tariff.price || 0) > 0
&& tariff.privileges.length !== 1
) throw new Error("Price is defined for tariff with several privileges");
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) {
serviceData = {
serviceKey: tariff.isCustom ? "custom" : tariff.privileges[0]?.serviceKey,
tariffs: [],
price: 0,
};
cartData.services.push(serviceData);
}
const tariffCartData: TariffCartData = {
price: tariff.price ?? 0,
isCustom: tariff.isCustom,
privileges: [],
id: tariff._id,
name: tariff.name,
};
serviceData.tariffs.push(tariffCartData);
tariff.privileges.forEach(privilege => {
let privilegePrice = privilege.amount * privilege.price;
if (!tariff.price) tariffCartData.price += privilegePrice;
else privilegePrice = tariff.price;
const privilegeCartData: PrivilegeCartData = {
serviceKey: privilege.serviceKey,
privilegeId: privilege.privilegeId,
description: privilege.description,
price: privilegePrice,
amount: privilege.amount,
appliedDiscounts: new Set(),
};
privilegeAmountById.set(
privilege.privilegeId,
privilege.amount + (privilegeAmountById.get(privilege.privilegeId) ?? 0)
);
servicePriceByKey.set(
privilege.serviceKey,
privilegePrice + (servicePriceByKey.get(privilege.serviceKey) ?? 0)
);
tariffCartData.privileges.push(privilegeCartData);
});
serviceData.price += tariffCartData.price;
});
cartData.priceBeforeDiscounts = Array.from(servicePriceByKey.values()).reduce((a, b) => a + b, 0);
cartData.priceAfterDiscounts = cartData.priceBeforeDiscounts;
const nkoDiscount = findNkoDiscount(discounts);
if (isUserNko && nkoDiscount) {
cartData.allAppliedDiscounts.add(nkoDiscount);
cartData.services.forEach(service => {
service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => {
privilege.appliedDiscounts.add(nkoDiscount);
const discountAmount = privilege.price * (1 - findDiscountFactor(nkoDiscount));
privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
});
});
});
return cartData;
}
cartData.services.forEach(service => {
service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => {
const privilegeTotalAmount = privilegeAmountById.get(privilege.privilegeId) ?? 0;
const discount = findPrivilegeDiscount(privilege.privilegeId, privilegeTotalAmount, discounts, userId);
if (!discount) return;
cartData.allAppliedDiscounts.add(discount);
privilege.appliedDiscounts.add(discount);
const discountAmount = privilege.price * (1 - findDiscountFactor(discount));
privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
const serviceTotalPrice = servicePriceByKey.get(privilege.serviceKey);
if (!serviceTotalPrice) throw new Error(`Service key ${privilege.serviceKey} not found in servicePriceByKey`);
servicePriceByKey.set(privilege.serviceKey, serviceTotalPrice - discountAmount);
});
});
});
cartData.services.forEach(service => {
service.tariffs.map(tariff => {
tariff.privileges.forEach(privilege => {
const serviceTotalPrice = servicePriceByKey.get(privilege.serviceKey);
if (!serviceTotalPrice) throw new Error(`Service key ${privilege.serviceKey} not found in servicePriceByKey`);
const discount = findServiceDiscount(privilege.serviceKey, serviceTotalPrice, discounts, userId);
if (!discount) return;
cartData.allAppliedDiscounts.add(discount);
privilege.appliedDiscounts.add(discount);
const discountAmount = privilege.price * (1 - findDiscountFactor(discount));
privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
});
});
});
const userDiscount = discounts.find(discount => discount.Condition.User === userId);
const cartDiscount = findCartDiscount(cartData.priceAfterDiscounts, discounts);
if (cartDiscount) {
cartData.services.forEach(service => {
if (service.serviceKey === userDiscount?.Condition.Group) return;
service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => {
cartData.allAppliedDiscounts.add(cartDiscount);
privilege.appliedDiscounts.add(cartDiscount);
const discountAmount = privilege.price * (1 - findDiscountFactor(cartDiscount));
privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
});
});
});
}
const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
if (loyalDiscount) {
cartData.services.forEach(service => {
if (service.serviceKey === userDiscount?.Condition.Group) return;
service.tariffs.forEach(tariff => {
tariff.privileges.forEach(privilege => {
cartData.allAppliedDiscounts.add(loyalDiscount);
privilege.appliedDiscounts.add(loyalDiscount);
const discountAmount = privilege.price * (1 - findDiscountFactor(loyalDiscount));
privilege.price -= discountAmount;
tariff.price -= discountAmount;
service.price -= discountAmount;
cartData.priceAfterDiscounts -= discountAmount;
});
});
});
}
return cartData;
}

@ -0,0 +1,52 @@
import { CustomTariffUserValues } from "../../model/customTariffs";
import { Discount } from "../../model/discount";
import { CustomPrivilegeWithAmount } from "../../model/privilege";
import { Tariff } from "../../model/tariff";
import { calcCart } from "./calcCart";
export function calcCustomTariffPrice(
customTariffUserValues: CustomTariffUserValues,
servicePrivileges: CustomPrivilegeWithAmount[],
cartTariffs: Tariff[],
discounts: Discount[],
purchasesAmount: number,
isUserNko: boolean,
userId: string,
) {
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, userId, 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,31 @@
import { Discount } from "../../model/discount";
import { Tariff } from "../../model/tariff";
import { calcCart } from "./calcCart";
export function calcTariffPrice(
targetTariff: Tariff,
discounts: Discount[],
purchasesAmount: number,
currentTariffs: Tariff[],
isUserNko: boolean,
userId: string,
): {
priceBeforeDiscounts: number;
priceAfterDiscounts: number;
} {
const priceBeforeDiscounts = targetTariff.price || targetTariff.privileges.reduce(
(sum, privilege) => sum + privilege.amount * privilege.price,
0
);
const cart = calcCart([...currentTariffs, targetTariff], discounts, purchasesAmount, userId, isUserNko);
const tariffCartData = cart.services.flatMap(service => service.tariffs).find(tariff => tariff.id === targetTariff._id);
if (!tariffCartData) throw new Error(`Target tariff ${targetTariff._id} not found in cart`);
return {
priceBeforeDiscounts,
priceAfterDiscounts: tariffCartData.price,
};
}

4
lib/utils/cart/index.ts Normal file

@ -0,0 +1,4 @@
export * from "./calcCart";
export * from "./calcCustomTariffPrice";
export * from "./calcTariffPrice";
export * from "./utils";

@ -0,0 +1,901 @@
import { Discount } from "../../../model/discount";
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,84 @@
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]],
[680, [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, 0, 1]],
[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]],
[910227.5, [0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 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]],
[95000, [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, 0]],
[465000, [0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0]],
[1700, [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, 0, 0]],
[465093, [0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0]],
[910273, [0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0]],
[910000, [0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 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]],
[911207.5700000001, [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 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]],
[465069.75, [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[910887.25, [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]],
[910250.25, [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 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]],
[910591.5, [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0]],
[465186, [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0]],
[465000, [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0]],
[912912, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]],
[910000, [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]],
[465569.82499999995, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[683432.355, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[465068.35500000004, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
[910039.5850000001, [0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]],
[910089.635, [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 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]],
[910078.26, [1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[910076.4400000001, [0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 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]],
[910247.52, [0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]],
[903498.255, [0, 0, 0, 1, 1, 0, 1, 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]],
[911006.915, [1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[1180018.385, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]],
[797926.05, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[910668.85, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0]],
[1035015.8, [0, 0, 0, 0, 0, 0, 1, 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]],
[536665.8, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[910054.6, [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0]],
[910338.0650000001, [0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[877021.155, [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[1037969.66, [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[1147125.98, [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 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]],
[910182, [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 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]],
[1504998.2675, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 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]],
[910538.7200000001, [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
[1265735.3800000001, [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[902551.05, [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[1105909.35, [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[910080.0800000001, [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[1295334.95, [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 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]],
[910176.54, [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]],
[902190.2100000001, [0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[1105556.27, [0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[910063.7000000001, [0, 0, 0, 1, 1, 1, 1, 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]],
[902280.42, [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[1105644.54, [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
[1295070.1400000001, [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]],
];

@ -0,0 +1,812 @@
import { Tariff } from "../../../model/tariff";
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"
}
];

127
lib/utils/cart/utils.ts Normal file

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

3
lib/utils/devlog.ts Normal file

@ -0,0 +1,3 @@
export const devlog: typeof console.log = (...args) => {
if (process.env.NODE_ENV === "development") console.log(...args);
};

5
lib/utils/getInitials.ts Normal file

@ -0,0 +1,5 @@
export function getInitials(firstname: string, secondname: string) {
return firstname[0] && secondname[0]
? firstname[0] + secondname[0]
: "АА";
}

4
lib/utils/index.ts Normal file

@ -0,0 +1,4 @@
export * from "./backendMessageHandler";
export * from "./cart";
export * from "./devlog";
export * from "./getInitials";

9799
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

@ -1,12 +1,76 @@
{
"name": "@frontend/kitui",
"version": "1.0.0",
"description": "test",
"main": "index.js",
"repository": "git@penahub.gitlab.yandexcloud.net:frontend/kitui.git",
"author": "skeris <kotilion.95@gmail.com>",
"license": "MIT",
"publishConfig": {
"registry": "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/"
}
"name": "@frontend/kitui",
"version": "2.0.3",
"description": "test",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"repository": {
"type": "git",
"url": "git@gitea.pena:PenaSide/UIKit.git"
},
"author": "skeris <kotilion.95@gmail.com>",
"license": "MIT",
"type": "module",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.js"
}
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"test:cart": "vitest ./lib/utils/cart",
"prepublishOnly": "npm run build"
},
"publishConfig": {
"registry": "http://gitea.pena/api/packages/skeris/npm/"
},
"dependencies": {
"immer": "^10.1.1",
"reconnecting-eventsource": "^1.6.4"
},
"devDependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.7",
"@mui/material": "^6.4.6",
"@types/node": "^22.13.10",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.4",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^8.26.0",
"@typescript-eslint/parser": "^8.26.0",
"@vitejs/plugin-react": "^4.3.4",
"axios": "^1.8.2",
"eslint": "^9.22.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0",
"npm": "^11.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.3.0",
"react-syntax-highlighter": "^15.6.1",
"typescript": "^5.8.2",
"vite": "^6.2.1",
"vite-plugin-dts": "^4.5.3",
"vitest": "^3.0.8",
"zustand": "^5.0.3"
},
"peerDependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.7",
"@mui/material": "^6.4.6",
"axios": "^1.8.2",
"react": "^19.0.0",
"react-router-dom": "^7.3.0",
"zustand": "^5.0.3"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

274
src/App.tsx Normal file

@ -0,0 +1,274 @@
import { Box, Button, Container, Pagination, SxProps, Theme, Typography, useTheme } from "@mui/material";
import { PenaLink } from "../lib/components/PenaLink";
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import jsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx";
import { ReactNode } from "react";
import { BurgerButton, CloseButton, CloseButtonSmall, PenaTextField, WalletButton } from "../lib";
import { AvatarButton } from "../lib/components/AvatarButton";
import { LogoutButton } from "../lib/components/LogoutButton";
SyntaxHighlighter.registerLanguage("jsx", jsx);
export function App() {
const theme = useTheme();
return (
<Box sx={{
backgroundColor: theme.palette.bg.main,
minHeight: "100dvh",
width: "100%",
}}>
<Container sx={{
py: 4,
display: "flex",
flexDirection: "column",
gap: 3,
alignItems: "start",
color: "white",
}}>
<Typography variant="h4">Components</Typography>
<ComponentWithCode
code={`<Button variant="pena-contained-dark">Подробнее</Button>`}
element={<Button variant="pena-contained-dark">Подробнее</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-contained-light">Подробнее</Button>`}
element={<Button variant="pena-contained-light">Подробнее</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-outlined-dark">Подробнее</Button>`}
element={<Button variant="pena-outlined-dark">Подробнее</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-outlined-light">Подробнее</Button>`}
element={<Button variant="pena-outlined-light">Подробнее</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-outlined-purple">Перейти</Button>`}
element={<Button variant="pena-outlined-purple">Перейти</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-contained-white1">Купить</Button>`}
element={<Button variant="pena-contained-white1">Купить</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-contained-white2">Купить</Button>`}
element={<Button variant="pena-contained-white2">Выбрать</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-navitem-dark">Подробнее</Button>`}
element={<Button variant="pena-navitem-dark">Подробнее</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-navitem-light">Подробнее</Button>`}
element={<Button variant="pena-navitem-light">Подробнее</Button>}
/>
<ComponentWithCode
code={`<Button variant="pena-text">Подробнее</Button>`}
element={<Button variant="pena-text">Подробнее</Button>}
/>
<ComponentWithCode
code={`<PenaLink>Подробнее</PenaLink>`}
element={<PenaLink href="/">Подробнее</PenaLink>}
/>
<ComponentWithCode
code={`<PenaTextField />`}
element={<PenaTextField />}
/>
<ComponentWithCode
code={`<AvatarButton>AB</AvatarButton>`}
element={<AvatarButton>AB</AvatarButton>}
/>
<ComponentWithCode
code={`<LogoutButton />`}
element={<LogoutButton />}
/>
<ComponentWithCode
code={`<WalletButton />`}
element={<WalletButton />}
/>
<ComponentWithCode
code={`<CloseButton />`}
element={<CloseButton />}
/>
<ComponentWithCode
code={`<CloseButtonSmall />`}
element={<CloseButtonSmall />}
/>
<ComponentWithCode
code={`<BurgerButton />`}
element={<BurgerButton />}
/>
<ComponentWithCode
sx={{
p: 1,
backgroundColor: theme.palette.background.default,
}}
code={`<Pagination variant="pena-pagination" />`}
element={<Pagination
variant="pena-pagination"
count={10}
page={1}
/>}
/>
<ComponentWithCode
code={`<Typography variant="infographic">Some text</Typography>`}
element={<Typography variant="infographic">Lorem ipsum dolor sit amet</Typography>}
/>
<ComponentWithCode
code={`<Typography variant="p1">Some text</Typography>`}
element={<Typography variant="p1">Lorem ipsum dolor sit amet</Typography>}
/>
<ComponentWithCode
code={`<Typography variant="price">Some text</Typography>`}
element={<Typography variant="price">Lorem ipsum dolor sit amet</Typography>}
/>
<ComponentWithCode
code={`<Typography variant="oldPrice">Some text</Typography>`}
element={<Typography variant="oldPrice">Lorem ipsum dolor sit amet</Typography>}
/>
<ComponentWithCode
code={`<Typography variant="t1">Some text</Typography>`}
element={<Typography variant="t1">Lorem ipsum dolor sit amet</Typography>}
/>
<ComponentWithCode
code={`<Typography variant="pena-card-header1">Some text</Typography>`}
element={<Typography variant="pena-card-header1">Lorem ipsum dolor sit amet</Typography>}
/>
<ComponentWithCode
code={`<Typography variant="pena-h1">Some text</Typography>`}
element={<Typography variant="pena-h1">Lorem ipsum dolor sit amet</Typography>}
/>
<ComponentWithCode
code={`<Typography variant="pena-h3">Some text</Typography>`}
element={<Typography variant="pena-h3">Lorem ipsum dolor sit amet</Typography>}
/>
<Typography variant="h4">Colors</Typography>
<Typography variant="body2">Click text to copy</Typography>
<Box sx={{
width: "100%",
display: "flex",
flexWrap: "wrap",
gap: 1,
}}>
<ColorShowcase
color={theme.palette.purple.light}
text1="theme.palette.purple.light"
text2="#944FEE"
/>
<ColorShowcase
color={theme.palette.purple.main}
text1="theme.palette.purple.main"
text2="#7E2AEA"
/>
<ColorShowcase
color={theme.palette.purple.dark}
text1="theme.palette.purple.dark"
text2="#581CA7"
/>
<ColorShowcase
color={theme.palette.background.default}
text1="theme.palette.background.default"
text2="#F2F3F7"
/>
<ColorShowcase
color={theme.palette.bg.main}
text1="theme.palette.bg.main"
text2="#333647"
/>
<ColorShowcase
color={theme.palette.bg.dark}
text1="theme.palette.bg.dark"
text2="#252734"
/>
<ColorShowcase
color={theme.palette.gray.main}
text1="theme.palette.gray.main"
text2="#9A9AAF"
/>
<ColorShowcase
color={theme.palette.gray.dark}
text1="theme.palette.gray.dark"
text2="#4D4D4D"
/>
<ColorShowcase
color={theme.palette.orange.main}
text1="theme.palette.orange.main"
text2="#FB5607"
/>
<ColorShowcase
color={theme.palette.orange.light}
text1="theme.palette.orange.light"
text2="#FC712F"
/>
</Box>
</Container>
</Box>
);
}
function ComponentWithCode({ sx, code, element }: {
sx?: SxProps<Theme>;
code: string;
element: ReactNode;
}) {
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
alignItems: "start",
...sx,
}}>
<SyntaxHighlighter
language="jsx"
style={oneDark}
customStyle={{
padding: "4px",
fontSize: "16px",
margin: 0,
}}
>
{code}
</SyntaxHighlighter>
{element}
</Box>
);
}
function ColorShowcase({ color, text1, text2 }: {
text1: string;
text2: string;
color: string;
}) {
return (
<Box sx={{
backgroundColor: color,
width: "100%",
display: "flex",
flexDirection: "column",
maxWidth: "350px",
p: 2,
gap: 1,
border: "1px solid white",
overflow: "hidden",
}}>
<Typography
onClick={() => navigator.clipboard.writeText(text1)}
sx={{
textShadow: "0 0 6px black",
}}
>{text1}</Typography>
<Typography
onClick={() => navigator.clipboard.writeText(text2)}
sx={{
textShadow: "0 0 6px black",
}}
>{text2}</Typography>
</Box>
);
}

15
src/main.tsx Normal file

@ -0,0 +1,15 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App.tsx";
import { CssBaseline, ThemeProvider } from "@mui/material";
import { penaMuiTheme } from "../lib/index.ts";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider theme={penaMuiTheme}>
<CssBaseline />
<App />
</ThemeProvider>
</React.StrictMode>,
);

1
src/vite-env.d.ts vendored Normal file

@ -0,0 +1 @@
/// <reference types="vite/client" />

34
tsconfig.json Normal file

@ -0,0 +1,34 @@
{
"compilerOptions": {
"types": ["vite/client"],
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src",
"lib",
"env.d.ts"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

10
tsconfig.node.json Normal file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

29
vite.config.ts Normal file

@ -0,0 +1,29 @@
import react from '@vitejs/plugin-react';
import { resolve } from "path";
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), dts({ include: ["lib"] })],
build: {
lib: {
entry: resolve(__dirname, "lib/index.ts"),
formats: ["es"],
fileName: "index"
},
copyPublicDir: false,
rollupOptions: {
external: [
"@emotion/react",
"@emotion/styled",
"@mui/icons-material",
"@mui/material",
"axios",
"react-router-dom",
"react",
"zustand",
],
},
}
});