add cart stuff

This commit is contained in:
nflnkr 2023-07-10 20:41:55 +03:00
parent 4c96954764
commit b0f2e94ebf
14 changed files with 473 additions and 9 deletions

@ -15,20 +15,22 @@
"registry": "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/"
},
"peerDependencies": {
"axios": "^1.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zustand": "^4.3.8",
"axios": "^1.4.0"
"zustand": "^4.3.8"
},
"devDependencies": {
"@types/node": "^20.2.5",
"@types/react": "^18.2.7",
"react": "^18.2.0",
"axios": "^1.4.0",
"react-dom": "^18.2.0",
"react": "^18.2.0",
"typescript": "^5.0.4",
"zustand": "^4.3.8"
},
"dependencies": {
"immer": "^10.0.2",
"reconnecting-eventsource": "^1.6.2"
}
}

@ -1,2 +1,3 @@
export * from "./makeRequest";
export * from "./tariff";
export * from "./tickets";

11
src/api/tariff.ts Normal file

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

@ -1,6 +1,9 @@
export * from "./useCart";
export * from "./useDebounce";
export * from "./useEventListener";
export * from "./useSSESubscription";
export * from "./useTariffs";
export * from "./useThrottle";
export * from "./useTicketMessages";
export * from "./useTickets";
export * from "./useTickets";
export * from "./useToken";

59
src/hooks/useCart.ts Normal file

@ -0,0 +1,59 @@
import { useEffect, useRef } from "react";
import { isAxiosError } from "axios";
import { devlog } from "../utils";
import { addCartTariffs, removeMissingCartTariffs, setCartTariffStatus, useCartStore } from "../stores/cart";
import { Tariff } from "../model/tariff";
import { getTariffById } from "../api/tariff";
export function useCart({ tariffs, tariffIds, onTariffRemove }: {
tariffs: Tariff[];
tariffIds: string[] | null;
onTariffRemove: (tariffId: string) => Promise<any>;
}) {
const onTariffRemoveRef = useRef(onTariffRemove);
const cartTariffMap = useCartStore(state => state.cartTariffMap);
const cart = useCartStore(state => state.cart);
useEffect(() => {
onTariffRemoveRef.current = onTariffRemove;
}, [onTariffRemove]);
useEffect(function addTariffsToCart() {
const knownTariffs: Tariff[] = [];
tariffIds?.forEach(tariffId => {
if (typeof cartTariffMap[tariffId] === "object") return;
const tariff = tariffs.find(tariff => tariff._id === tariffId);
if (tariff) return knownTariffs.push(tariff);
if (!cartTariffMap[tariffId]) {
setCartTariffStatus(tariffId, "loading");
getTariffById(tariffId).then(tariff => {
devlog("Unknown tariff", tariff);
addCartTariffs([tariff]);
}).catch(error => {
devlog(`Error fetching unknown tariff ${tariffId}`, error);
setCartTariffStatus(tariffId, "not found");
if (isAxiosError(error) && error.response?.status === 404) {
onTariffRemoveRef.current(tariffId).then(() => {
devlog(`Unexistant tariff with id ${tariffId} deleted from cart`);
}).catch(error => {
devlog("Error deleting unexistant tariff from cart", error);
});
}
});
}
});
if (knownTariffs.length > 0) addCartTariffs(knownTariffs);
}, [tariffIds, cartTariffMap, tariffs]);
useEffect(function cleanUpCart() {
if (tariffIds) removeMissingCartTariffs(tariffIds);
}, [tariffIds]);
return cart;
}

@ -1,6 +1,7 @@
export * from "./api";
export * from "./hooks";
export * from "./decorators";
export * from "./hooks";
export * from "./model";
export * from "./stores";
export * from "./utils";
export type * from "./model";

19
src/model/cart.ts Normal file

@ -0,0 +1,19 @@
export type PrivilegeCartData = {
tariffId: string;
privilegeId: string;
name: string;
price: number;
};
export type ServiceCartData = {
serviceKey: string;
privileges: PrivilegeCartData[];
price: number;
};
export type CartData = {
services: ServiceCartData[];
priceBeforeDiscounts: number;
priceAfterDiscounts: number;
itemCount: number;
};

185
src/model/discount.ts Normal file

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

@ -1 +1,5 @@
export type * from "./cart";
export type * from "./discount";
export type * from "./privilege";
export type * from "./tariff";
export type * from "./ticket";

78
src/stores/cart.ts Normal file

@ -0,0 +1,78 @@
import { produce } from "immer";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { Tariff } from "../model/tariff";
import { CartData } from "../model/cart";
import { calcCart } from "../utils/calcCart";
interface CartStore {
cartTariffMap: Record<string, Tariff | "loading" | "not found">;
cart: CartData;
notEnoughMoneyAmount: number;
}
export const useCartStore = create<CartStore>()(
devtools(
(get, set) => ({
cartTariffMap: {},
cart: {
services: [],
priceBeforeDiscounts: 0,
priceAfterDiscounts: 0,
itemCount: 0,
},
notEnoughMoneyAmount: 0,
}),
{
name: "Cart",
enabled: process.env.NODE_ENV === "development",
trace: true,
actionsBlacklist: "rejected",
}
)
);
export const setCartTariffStatus = (tariffId: string, status: "loading" | "not found") => useCartStore.setState(
produce<CartStore>(state => {
state.cartTariffMap[tariffId] = status;
}),
false,
{
type: "setCartTariffStatus",
tariffId,
status,
}
);
export const addCartTariffs = (tariffs: Tariff[]) => useCartStore.setState(
produce<CartStore>(state => {
tariffs.forEach(tariff => {
state.cartTariffMap[tariff._id] = tariff;
});
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
state.cart = calcCart(cartTariffs, []);
}),
false,
{
type: tariffs.length > 0 ? "addCartTariffs" : "rejected",
tariffIds: tariffs.map(tariff => tariff._id),
}
);
export const removeMissingCartTariffs = (tariffIds: string[]) => useCartStore.setState(
produce<CartStore>(state => {
for (const key in state.cartTariffMap) {
if (!tariffIds.includes(key)) delete state.cartTariffMap[key];
}
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
state.cart = calcCart(cartTariffs, []);
}),
false,
{
type: "removeMissingCartTariffs",
tariffIds,
}
);
export const setNotEnoughMoneyAmount = (notEnoughMoneyAmount: number) => useCartStore.setState({ notEnoughMoneyAmount });

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

94
src/utils/calcCart.ts Normal file

@ -0,0 +1,94 @@
import { CartData, PrivilegeCartData } from "../model/cart";
import { AnyDiscount, PrivilegeDiscount, ServiceDiscount } from "../model/discount";
import { PrivilegeWithAmount } from "../model/privilege";
import { Tariff } from "../model/tariff";
export function calcCart(tariffs: Tariff[], discounts: AnyDiscount[]): CartData {
const cartData: CartData = {
services: [],
priceBeforeDiscounts: 0,
priceAfterDiscounts: 0,
itemCount: 0,
};
tariffs.forEach(tariff => {
if (tariff.price && tariff.price > 0) cartData.priceBeforeDiscounts += tariff.price;
tariff.privilegies.forEach(privilege => {
let serviceData = cartData.services.find(service => service.serviceKey === privilege.serviceKey);
if (!serviceData) {
serviceData = {
serviceKey: privilege.serviceKey,
privileges: [],
price: 0,
};
cartData.services.push(serviceData);
}
let privilegePrice = privilege.amount * privilege.price;
if (!tariff.price) cartData.priceBeforeDiscounts += privilegePrice;
const privilegeDiscount = findPrivilegeDiscount(privilege, discounts);
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts);
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
const privilegeData: PrivilegeCartData = {
tariffId: tariff._id,
privilegeId: privilege.privilegeId,
name: privilege.description,
price: privilegePrice,
};
serviceData.privileges.push(privilegeData);
serviceData.price += privilegePrice;
cartData.priceAfterDiscounts += privilegePrice;
cartData.itemCount++;
});
});
return cartData;
}
export function findPrivilegeDiscount(privilege: PrivilegeWithAmount, discounts: AnyDiscount[]): PrivilegeDiscount | null {
const applicableDiscounts = discounts.filter((discount): discount is PrivilegeDiscount => {
return (
discount.conditionType === "privilege" &&
privilege.privilegeId === discount.condition.privilege.id &&
privilege.amount >= discount.condition.privilege.value
);
});
if (!applicableDiscounts.length) return null;
const maxValueDiscount = applicableDiscounts.reduce((prev, current) =>
current.condition.privilege.value > prev.condition.privilege.value ? current : prev
);
return maxValueDiscount;
}
export function findServiceDiscount(
serviceKey: string,
currentPrice: number,
discounts: AnyDiscount[],
): ServiceDiscount | null {
const discountsForTariffService = discounts.filter((discount): discount is ServiceDiscount => {
return (
discount.conditionType === "service" &&
discount.condition.service.id === serviceKey &&
currentPrice >= discount.condition.service.value
);
});
if (!discountsForTariffService.length) return null;
const maxValueDiscount = discountsForTariffService.reduce((prev, current) => {
return current.condition.service.value > prev.condition.service.value ? current : prev;
});
return maxValueDiscount;
}

@ -1,2 +1,3 @@
export * from "./devlog";
export * from "./backendMessageHandler";
export * from "./backendMessageHandler";
export * from "./calcCart";
export * from "./devlog";

@ -71,6 +71,11 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
immer@^10.0.2:
version "10.0.2"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.2.tgz#11636c5b77acf529e059582d76faf338beb56141"
integrity sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"