fix: duplicate shared/constants/ into client repo
This commit is contained in:
@@ -24,7 +24,7 @@ import TableRow from '@mui/material/TableRow'
|
|||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Tooltip from '@mui/material/Tooltip'
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { TEST_CHECKLIST_ITEMS } from '@/shared/constants/test-checklist-items'
|
import { TEST_CHECKLIST_ITEMS } from '@shared/constants/test-checklist-items'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
fetchTestChecklistResults,
|
fetchTestChecklistResults,
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
export const DELIVERY_CARRIER_CODES = Object.freeze([
|
import {
|
||||||
'RUSSIAN_POST',
|
DELIVERY_CARRIERS as SHARED_DELIVERY_CARRIERS,
|
||||||
'OZON_PVZ',
|
DELIVERY_CARRIER_LABELS,
|
||||||
'YANDEX_PVZ',
|
deliveryCarrierLabelRu as sharedDeliveryCarrierLabelRu,
|
||||||
'FIVE_POST',
|
} from '@shared/constants/delivery-carrier'
|
||||||
'WB_PVZ',
|
|
||||||
] as const)
|
export const DELIVERY_CARRIER_CODES = SHARED_DELIVERY_CARRIERS as typeof SHARED_DELIVERY_CARRIERS
|
||||||
|
|
||||||
export type DeliveryCarrierCode = (typeof DELIVERY_CARRIER_CODES)[number]
|
export type DeliveryCarrierCode = (typeof DELIVERY_CARRIER_CODES)[number]
|
||||||
|
|
||||||
export const DELIVERY_CARRIER_LABELS: Record<DeliveryCarrierCode, string> = {
|
|
||||||
RUSSIAN_POST: 'Почта России',
|
|
||||||
OZON_PVZ: 'Озон доставка (пункт выдачи)',
|
|
||||||
YANDEX_PVZ: 'Яндекс доставка (пункт выдачи)',
|
|
||||||
FIVE_POST: '5Post (пункт выдачи)',
|
|
||||||
WB_PVZ: 'WB доставка (пункт выдачи)',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DELIVERY_CARRIER_OPTIONS: ReadonlyArray<{ code: DeliveryCarrierCode; label: string }> =
|
export const DELIVERY_CARRIER_OPTIONS: ReadonlyArray<{ code: DeliveryCarrierCode; label: string }> =
|
||||||
DELIVERY_CARRIER_CODES.map((code) => ({
|
DELIVERY_CARRIER_CODES.map((code) => ({
|
||||||
code,
|
code,
|
||||||
@@ -23,6 +15,5 @@ export const DELIVERY_CARRIER_OPTIONS: ReadonlyArray<{ code: DeliveryCarrierCode
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
export function deliveryCarrierLabelRu(code: string | null | undefined): string | null {
|
export function deliveryCarrierLabelRu(code: string | null | undefined): string | null {
|
||||||
if (!code) return null
|
return sharedDeliveryCarrierLabelRu(code)
|
||||||
return (DELIVERY_CARRIER_LABELS as Record<string, string>)[code] ?? code
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,17 @@
|
|||||||
export const ORDER_STATUSES = Object.freeze([
|
import {
|
||||||
'DRAFT',
|
ORDER_STATUSES as SHARED_ORDER_STATUSES,
|
||||||
'PENDING_PAYMENT',
|
getNextAdminStatuses as sharedGetNextAdminStatuses,
|
||||||
'PAID',
|
} from '@shared/constants/order-status'
|
||||||
'IN_PROGRESS',
|
|
||||||
'SHIPPED',
|
export const ORDER_STATUSES = SHARED_ORDER_STATUSES as typeof SHARED_ORDER_STATUSES
|
||||||
'READY_FOR_PICKUP',
|
|
||||||
'DONE',
|
|
||||||
'CANCELLED',
|
|
||||||
] as const)
|
|
||||||
|
|
||||||
export type OrderStatus = (typeof ORDER_STATUSES)[number]
|
export type OrderStatus = (typeof ORDER_STATUSES)[number]
|
||||||
|
|
||||||
const ADMIN_ORDER_TRANSITIONS: Record<string, readonly string[] | { readonly delivery: readonly string[]; readonly pickup: readonly string[] }> = Object.freeze({
|
|
||||||
DRAFT: ['PENDING_PAYMENT', 'CANCELLED'],
|
|
||||||
PENDING_PAYMENT: ['PAID', 'CANCELLED'],
|
|
||||||
PAID: ['IN_PROGRESS', 'CANCELLED'],
|
|
||||||
IN_PROGRESS: Object.freeze({
|
|
||||||
delivery: ['SHIPPED', 'CANCELLED'],
|
|
||||||
pickup: ['READY_FOR_PICKUP', 'CANCELLED'],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
function getNextAdminStatuses(from: string, deliveryType: string): string[] {
|
|
||||||
const transition = ADMIN_ORDER_TRANSITIONS[from]
|
|
||||||
if (!transition) return []
|
|
||||||
if (Array.isArray(transition)) return [...transition]
|
|
||||||
return transition[deliveryType] ? [...transition[deliveryType]] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAdminNextOrderStatuses(status: string, deliveryType: 'delivery' | 'pickup'): OrderStatus[] {
|
export function getAdminNextOrderStatuses(status: string, deliveryType: 'delivery' | 'pickup'): OrderStatus[] {
|
||||||
return getNextAdminStatuses(status, deliveryType) as OrderStatus[]
|
return sharedGetNextAdminStatuses(status, deliveryType) as OrderStatus[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canTransitionOrderStatus(from: string, to: string, deliveryType: 'delivery' | 'pickup'): boolean {
|
export function canTransitionOrderStatus(from: string, to: string, deliveryType: 'delivery' | 'pickup'): boolean {
|
||||||
if (from === to) return true
|
if (from === to) return true
|
||||||
return getAdminNextOrderStatuses(from, deliveryType).includes(to as OrderStatus)
|
return getAdminNextOrderStatuses(from, deliveryType).includes(to as OrderStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canTransitionAdminOrderStatus(
|
|
||||||
order: { status: string; deliveryType: string },
|
|
||||||
next: string,
|
|
||||||
): boolean {
|
|
||||||
if (order.status === next) return true
|
|
||||||
return getNextAdminStatuses(order.status, order.deliveryType).includes(next)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
export interface TestChecklistItem {
|
|
||||||
key: string
|
|
||||||
section: string
|
|
||||||
action: string
|
|
||||||
expectedResult: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TEST_CHECKLIST_ITEMS: readonly TestChecklistItem[] = Object.freeze([
|
|
||||||
// Авторизация
|
|
||||||
{ key: 'auth.register-email', section: 'Авторизация', action: 'Зарегистрироваться по email', expectedResult: 'Код приходит на почту, аккаунт создаётся' },
|
|
||||||
{ key: 'auth.login-password', section: 'Авторизация', action: 'Войти по паролю', expectedResult: 'Корректный пароль пускает, неправильный — ошибка' },
|
|
||||||
{ key: 'auth.oauth-vk', section: 'Авторизация', action: 'Войти через OAuth VK', expectedResult: 'Редирект на VK, callback, авторизация успешна' },
|
|
||||||
{ key: 'auth.oauth-yandex', section: 'Авторизация', action: 'Войти через OAuth Yandex', expectedResult: 'Редирект на Yandex, callback, авторизация успешна' },
|
|
||||||
{ key: 'auth.reset-password', section: 'Авторизация', action: 'Сбросить пароль', expectedResult: 'Письмо приходит, ссылка работает, пароль меняется' },
|
|
||||||
{ key: 'auth.logout', section: 'Авторизация', action: 'Выйти из аккаунта', expectedResult: 'Сессия очищается, редирект на страницу входа' },
|
|
||||||
|
|
||||||
// Каталог и товары
|
|
||||||
{ key: 'catalog.homepage', section: 'Каталог и товары', action: 'Открыть главную страницу', expectedResult: 'Слайдер грузится, товары отображаются' },
|
|
||||||
{ key: 'catalog.filters', section: 'Каталог и товары', action: 'Применить фильтры', expectedResult: 'Фильтры по категории, цене, материалам работают' },
|
|
||||||
{ key: 'catalog.product-page', section: 'Каталог и товары', action: 'Открыть страницу товара', expectedResult: 'Фото, описание, цена, кнопка "В корзину" отображаются' },
|
|
||||||
{ key: 'catalog.seo', section: 'Каталог и товары', action: 'Проверить SEO-метаданные', expectedResult: 'Title, meta, slug корректные' },
|
|
||||||
|
|
||||||
// Корзина
|
|
||||||
{ key: 'cart.add', section: 'Корзина', action: 'Добавить товар в корзину', expectedResult: 'Счётчик корзины обновляется' },
|
|
||||||
{ key: 'cart.change-qty', section: 'Корзина', action: 'Изменить количество товара', expectedResult: 'Сумма пересчитывается' },
|
|
||||||
{ key: 'cart.remove', section: 'Корзина', action: 'Удалить товар из корзины', expectedResult: 'Товар убирается, сумма пересчитывается' },
|
|
||||||
|
|
||||||
// Чекаут
|
|
||||||
{ key: 'checkout.address', section: 'Чекаут', action: 'Выбрать адрес доставки', expectedResult: 'Можно выбрать из сохранённых или добавить новый' },
|
|
||||||
{ key: 'checkout.delivery', section: 'Чекаут', action: 'Выбрать способ доставки', expectedResult: 'Почта, OZON, Яндекс, 5post — доступны' },
|
|
||||||
{ key: 'checkout.payment', section: 'Чекаут', action: 'Выбрать способ оплаты', expectedResult: 'Онлайн / при получении — доступны' },
|
|
||||||
{ key: 'checkout.comment', section: 'Чекаут', action: 'Добавить комментарий к заказу', expectedResult: 'Поле работает, текст сохраняется' },
|
|
||||||
{ key: 'checkout.create', section: 'Чекаут', action: 'Создать заказ', expectedResult: 'Заказ создаётся, статус DRAFT' },
|
|
||||||
|
|
||||||
// Оплата
|
|
||||||
{ key: 'payment.yookassa', section: 'Оплата', action: 'Оплатить через ЮKassa', expectedResult: 'Редирект на оплату, webhook обрабатывается' },
|
|
||||||
{ key: 'payment.status', section: 'Оплата', action: 'Проверить статус платежа', expectedResult: 'Статус обновляется после webhook' },
|
|
||||||
|
|
||||||
// Профиль пользователя
|
|
||||||
{ key: 'profile.avatar', section: 'Профиль пользователя', action: 'Управление аватаром', expectedResult: 'Загрузка, отображение, удаление работают' },
|
|
||||||
{ key: 'profile.settings', section: 'Профиль пользователя', action: 'Изменить настройки профиля', expectedResult: 'Email, имя, способы входа обновляются' },
|
|
||||||
{ key: 'profile.addresses', section: 'Профиль пользователя', action: 'Управление адресами', expectedResult: 'Добавление, редактирование, удаление, по умолчанию' },
|
|
||||||
{ key: 'profile.orders', section: 'Профиль пользователя', action: 'Просмотр заказов', expectedResult: 'Список, детали, статусы отображаются' },
|
|
||||||
{ key: 'profile.messages', section: 'Профиль пользователя', action: 'Сообщения по заказу', expectedResult: 'Отправка, получение, read state работают' },
|
|
||||||
{ key: 'profile.notifications', section: 'Профиль пользователя', action: 'Настройки уведомлений', expectedResult: 'Вкл/выкл каналов работают' },
|
|
||||||
{ key: 'profile.delete-account', section: 'Профиль пользователя', action: 'Удалить аккаунт', expectedResult: 'Данные удаляются' },
|
|
||||||
|
|
||||||
// Админ — Товары
|
|
||||||
{ key: 'admin-products.list', section: 'Админ — Товары', action: 'Открыть список товаров', expectedResult: 'Пагинация, поиск работают' },
|
|
||||||
{ key: 'admin-products.create', section: 'Админ — Товары', action: 'Создать товар', expectedResult: 'Все поля сохраняются, фото загружаются, публикация работает' },
|
|
||||||
{ key: 'admin-products.edit', section: 'Админ — Товары', action: 'Редактировать товар', expectedResult: 'Изменения сохраняются' },
|
|
||||||
{ key: 'admin-products.delete', section: 'Админ — Товары', action: 'Удалить товар', expectedResult: 'Товар удаляется' },
|
|
||||||
{ key: 'admin-products.images', section: 'Админ — Товары', action: 'Управление изображениями товара', expectedResult: 'Добавление, сортировка, удаление работают' },
|
|
||||||
|
|
||||||
// Админ — Категории
|
|
||||||
{ key: 'admin-categories.crud', section: 'Админ — Категории', action: 'CRUD категорий', expectedResult: 'Создание, редактирование, удаление, сортировка работают' },
|
|
||||||
|
|
||||||
// Админ — Заказы
|
|
||||||
{ key: 'admin-orders.list', section: 'Админ — Заказы', action: 'Открыть список заказов', expectedResult: 'Фильтрация по статусу, внимание отображается' },
|
|
||||||
{ key: 'admin-orders.details', section: 'Админ — Заказы', action: 'Открыть детали заказа', expectedResult: 'Состав, статус, смена статуса работают' },
|
|
||||||
{ key: 'admin-orders.messages', section: 'Админ — Заказы', action: 'Ответить на сообщение заказа', expectedResult: 'Сообщение отправляется пользователю' },
|
|
||||||
|
|
||||||
// Админ — Отзывы
|
|
||||||
{ key: 'admin-reviews.list', section: 'Админ — Отзывы', action: 'Открыть список отзывов', expectedResult: 'Фильтрация pending/approved/rejected работает' },
|
|
||||||
{ key: 'admin-reviews.moderate', section: 'Админ — Отзывы', action: 'Модерировать отзыв', expectedResult: 'Approve/reject работают' },
|
|
||||||
|
|
||||||
// Админ — Пользователи
|
|
||||||
{ key: 'admin-users.list', section: 'Админ — Пользователи', action: 'Открыть список пользователей', expectedResult: 'Email, дата регистрации отображаются' },
|
|
||||||
{ key: 'admin-users.orders', section: 'Админ — Пользователи', action: 'Просмотр заказов пользователя', expectedResult: 'Заказы пользователя отображаются' },
|
|
||||||
|
|
||||||
// Админ — Галерея
|
|
||||||
{ key: 'admin-gallery.upload', section: 'Админ — Галерея', action: 'Управление галереей', expectedResult: 'Загрузка, удаление, использование в слайдере работают' },
|
|
||||||
|
|
||||||
// Админ — Настройки
|
|
||||||
{ key: 'admin-settings.notifications', section: 'Админ — Настройки', action: 'Настройки уведомлений админа', expectedResult: 'Email, telegram настраиваются' },
|
|
||||||
|
|
||||||
// Инфо-страницы
|
|
||||||
{ key: 'info.pages', section: 'Инфо-страницы', action: 'Открыть инфо-страницы', expectedResult: 'Доставка, оплата, как заказать, статусы заказов отображаются' },
|
|
||||||
{ key: 'info.legal', section: 'Инфо-страницы', action: 'Открыть юридические страницы', expectedResult: 'Политика конфиденциальности, условия использования отображаются' },
|
|
||||||
|
|
||||||
// SSE / Realtime
|
|
||||||
{ key: 'sse.notifications', section: 'SSE / Realtime', action: 'Проверить SSE-уведомления', expectedResult: 'Уведомления приходят в реальном времени' },
|
|
||||||
]) as unknown as TestChecklistItem[]
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
const MB = 1024 * 1024
|
import { ADMIN_UPLOAD_IMAGE_MAX_FILE_BYTES_DEFAULT } from '@shared/constants/upload-limits'
|
||||||
|
|
||||||
export const ADMIN_UPLOAD_IMAGE_MAX_BYTES = 20 * MB
|
export const ADMIN_UPLOAD_IMAGE_MAX_BYTES = ADMIN_UPLOAD_IMAGE_MAX_FILE_BYTES_DEFAULT
|
||||||
|
|
||||||
export const OTHER_UPLOAD_MAX_FILE_BYTES = 2 * MB
|
export const OTHER_UPLOAD_MAX_FILE_BYTES = 2 * 1024 * 1024 // 2 MB
|
||||||
|
|
||||||
export function formatAdminImageMaxSizeHint(): string {
|
export function formatAdminImageMaxSizeHint(): string {
|
||||||
return `${Math.round(ADMIN_UPLOAD_IMAGE_MAX_BYTES / (1024 * 1024))} МБ`
|
return `${Math.round(ADMIN_UPLOAD_IMAGE_MAX_BYTES / (1024 * 1024))} МБ`
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"],
|
||||||
|
"@shared/*": ["../shared/*"]
|
||||||
},
|
},
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "es2023",
|
"target": "es2023",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
const rootDir = fileURLToPath(new URL('.', import.meta.url))
|
const rootDir = fileURLToPath(new URL('.', import.meta.url))
|
||||||
|
const projectRoot = path.resolve(rootDir, '..')
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -11,11 +12,12 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(rootDir, 'src'),
|
'@': path.resolve(rootDir, 'src'),
|
||||||
|
'@shared': path.resolve(projectRoot, 'shared'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
fs: {
|
fs: {
|
||||||
allow: [path.resolve(rootDir, '..')],
|
allow: [projectRoot],
|
||||||
},
|
},
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
export declare const DELIVERY_CARRIERS: readonly ['RUSSIAN_POST', 'OZON_PVZ', 'YANDEX_PVZ', 'FIVE_POST', 'WB_PVZ']
|
||||||
|
|
||||||
|
export declare const DELIVERY_CARRIER_LABELS: {
|
||||||
|
readonly RUSSIAN_POST: 'Почта России'
|
||||||
|
readonly OZON_PVZ: 'Озон доставка (пункт выдачи)'
|
||||||
|
readonly YANDEX_PVZ: 'Яндекс доставка (пункт выдачи)'
|
||||||
|
readonly FIVE_POST: '5Post (пункт выдачи)'
|
||||||
|
readonly WB_PVZ: 'WB доставка (пункт выдачи)'
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function deliveryCarrierLabelRu(code: string | null | undefined): string | null
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
export const DELIVERY_CARRIERS = Object.freeze(['RUSSIAN_POST', 'OZON_PVZ', 'YANDEX_PVZ', 'FIVE_POST', 'WB_PVZ'])
|
||||||
|
|
||||||
|
export const DELIVERY_CARRIER_LABELS = Object.freeze({
|
||||||
|
RUSSIAN_POST: 'Почта России',
|
||||||
|
OZON_PVZ: 'Озон доставка (пункт выдачи)',
|
||||||
|
YANDEX_PVZ: 'Яндекс доставка (пункт выдачи)',
|
||||||
|
FIVE_POST: '5Post (пункт выдачи)',
|
||||||
|
WB_PVZ: 'WB доставка (пункт выдачи)',
|
||||||
|
})
|
||||||
|
|
||||||
|
export function deliveryCarrierLabelRu(code) {
|
||||||
|
if (!code) return null
|
||||||
|
return DELIVERY_CARRIER_LABELS[code] ?? code
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
export type NotificationEventType =
|
||||||
|
| 'order:created'
|
||||||
|
| 'order:statusChanged'
|
||||||
|
| 'orderMessage:sent'
|
||||||
|
| 'orderMessage:adminReply'
|
||||||
|
| 'payment:statusChanged'
|
||||||
|
| 'auth:codeRequested'
|
||||||
|
| 'order:deliveryFeeAdjusted'
|
||||||
|
|
||||||
|
export type NotificationChannel = 'email' | 'telegram'
|
||||||
|
|
||||||
|
export type NotificationStatus = 'pending' | 'sent' | 'failed'
|
||||||
|
|
||||||
|
export const NOTIFICATION_EVENTS: {
|
||||||
|
ORDER_CREATED: NotificationEventType
|
||||||
|
ORDER_STATUS_CHANGED: NotificationEventType
|
||||||
|
ORDER_MESSAGE_SENT: NotificationEventType
|
||||||
|
ORDER_MESSAGE_ADMIN_REPLY: NotificationEventType
|
||||||
|
PAYMENT_STATUS_CHANGED: NotificationEventType
|
||||||
|
AUTH_CODE_REQUESTED: NotificationEventType
|
||||||
|
DELIVERY_FEE_ADJUSTED: NotificationEventType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NOTIFICATION_CHANNELS: {
|
||||||
|
EMAIL: NotificationChannel
|
||||||
|
TELEGRAM: NotificationChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NOTIFICATION_STATUSES: {
|
||||||
|
PENDING: NotificationStatus
|
||||||
|
SENT: NotificationStatus
|
||||||
|
FAILED: NotificationStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MAX_RETRY_ATTEMPTS: number
|
||||||
|
export const RETRY_DELAYS_MS: number[]
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
/** @typedef {'order:created' | 'order:statusChanged' | 'orderMessage:sent' | 'orderMessage:adminReply' | 'payment:statusChanged' | 'auth:codeRequested'} NotificationEventType */
|
||||||
|
|
||||||
|
export const NOTIFICATION_EVENTS = {
|
||||||
|
ORDER_CREATED: 'order:created',
|
||||||
|
ORDER_STATUS_CHANGED: 'order:statusChanged',
|
||||||
|
ORDER_MESSAGE_SENT: 'orderMessage:sent',
|
||||||
|
ORDER_MESSAGE_ADMIN_REPLY: 'orderMessage:adminReply',
|
||||||
|
PAYMENT_STATUS_CHANGED: 'payment:statusChanged',
|
||||||
|
AUTH_CODE_REQUESTED: 'auth:codeRequested',
|
||||||
|
DELIVERY_FEE_ADJUSTED: 'order:deliveryFeeAdjusted',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NOTIFICATION_CHANNELS = {
|
||||||
|
EMAIL: 'email',
|
||||||
|
TELEGRAM: 'telegram',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NOTIFICATION_STATUSES = {
|
||||||
|
PENDING: 'pending',
|
||||||
|
SENT: 'sent',
|
||||||
|
FAILED: 'failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MAX_RETRY_ATTEMPTS = 3
|
||||||
|
|
||||||
|
export const RETRY_DELAYS_MS = [5_000, 30_000, 120_000]
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
export declare const ORDER_STATUSES: readonly [
|
||||||
|
'DRAFT',
|
||||||
|
'PENDING_PAYMENT',
|
||||||
|
'PAID',
|
||||||
|
'IN_PROGRESS',
|
||||||
|
'SHIPPED',
|
||||||
|
'READY_FOR_PICKUP',
|
||||||
|
'DONE',
|
||||||
|
'CANCELLED',
|
||||||
|
]
|
||||||
|
|
||||||
|
export type OrderStatus = (typeof ORDER_STATUSES)[number]
|
||||||
|
|
||||||
|
export declare const ADMIN_ORDER_TRANSITIONS: Record<string, readonly string[] | { readonly delivery: readonly string[]; readonly pickup: readonly string[] }>
|
||||||
|
|
||||||
|
export declare function getNextAdminStatuses(from: string, deliveryType: string): string[]
|
||||||
|
export declare function canTransitionAdminOrderStatus(order: { status: string; deliveryType: string }, next: string): boolean
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
export const ORDER_STATUSES = Object.freeze([
|
||||||
|
"DRAFT",
|
||||||
|
"PENDING_PAYMENT",
|
||||||
|
"PAID",
|
||||||
|
"IN_PROGRESS",
|
||||||
|
"SHIPPED",
|
||||||
|
"READY_FOR_PICKUP",
|
||||||
|
"DONE",
|
||||||
|
"CANCELLED",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Допустимые переходы статусов, доступные админу.
|
||||||
|
* Значение — массив из next-статусов.
|
||||||
|
* Для IN_PROGRESS: объект с ключами по deliveryType.
|
||||||
|
*/
|
||||||
|
export const ADMIN_ORDER_TRANSITIONS = Object.freeze({
|
||||||
|
DRAFT: ["PENDING_PAYMENT", "CANCELLED"],
|
||||||
|
PENDING_PAYMENT: ["PAID", "CANCELLED"],
|
||||||
|
PAID: ["IN_PROGRESS", "CANCELLED"],
|
||||||
|
IN_PROGRESS: Object.freeze({
|
||||||
|
delivery: ["SHIPPED", "CANCELLED"],
|
||||||
|
pickup: ["READY_FOR_PICKUP", "CANCELLED"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getNextAdminStatuses(from, deliveryType) {
|
||||||
|
const transition = ADMIN_ORDER_TRANSITIONS[from];
|
||||||
|
if (!transition) return [];
|
||||||
|
if (Array.isArray(transition)) return [...transition];
|
||||||
|
return transition[deliveryType] ? [...transition[deliveryType]] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canTransitionAdminOrderStatus(order, next) {
|
||||||
|
const from = order.status;
|
||||||
|
if (from === next) return true;
|
||||||
|
return getNextAdminStatuses(from, order.deliveryType).includes(next);
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export declare const PAYMENT_METHODS: readonly ['online', 'on_pickup']
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
export const PAYMENT_METHODS = Object.freeze(['online', 'on_pickup'])
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
export interface TestChecklistItem {
|
||||||
|
key: string;
|
||||||
|
section: string;
|
||||||
|
action: string;
|
||||||
|
expectedResult: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare const TEST_CHECKLIST_ITEMS: readonly TestChecklistItem[];
|
||||||
Executable
+304
@@ -0,0 +1,304 @@
|
|||||||
|
export const TEST_CHECKLIST_ITEMS = Object.freeze([
|
||||||
|
// Авторизация
|
||||||
|
{
|
||||||
|
key: "auth.register-email",
|
||||||
|
section: "Авторизация",
|
||||||
|
action: "Зарегистрироваться по email",
|
||||||
|
expectedResult: "Код приходит на почту, аккаунт создаётся",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "auth.login-password",
|
||||||
|
section: "Авторизация",
|
||||||
|
action: "Войти по паролю",
|
||||||
|
expectedResult: "Корректный пароль пускает, неправильный — ошибка",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "auth.oauth-vk",
|
||||||
|
section: "Авторизация",
|
||||||
|
action: "Войти через OAuth VK",
|
||||||
|
expectedResult: "Редирект на VK, callback, авторизация успешна",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "auth.oauth-yandex",
|
||||||
|
section: "Авторизация",
|
||||||
|
action: "Войти через OAuth Yandex",
|
||||||
|
expectedResult: "Редирект на Yandex, callback, авторизация успешна",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "auth.reset-password",
|
||||||
|
section: "Авторизация",
|
||||||
|
action: "Сбросить пароль",
|
||||||
|
expectedResult: "Письмо приходит, ссылка работает, пароль меняется",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "auth.logout",
|
||||||
|
section: "Авторизация",
|
||||||
|
action: "Выйти из аккаунта",
|
||||||
|
expectedResult: "Сессия очищается, редирект на страницу входа",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Каталог и товары
|
||||||
|
{
|
||||||
|
key: "catalog.homepage",
|
||||||
|
section: "Каталог и товары",
|
||||||
|
action: "Открыть главную страницу",
|
||||||
|
expectedResult: "Слайдер грузится, товары отображаются",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "catalog.filters",
|
||||||
|
section: "Каталог и товары",
|
||||||
|
action: "Применить фильтры",
|
||||||
|
expectedResult: "Фильтры по категории, цене, материалам работают",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "catalog.product-page",
|
||||||
|
section: "Каталог и товары",
|
||||||
|
action: "Открыть страницу товара",
|
||||||
|
expectedResult: 'Фото, описание, цена, кнопка "В корзину" отображаются',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "catalog.seo",
|
||||||
|
section: "Каталог и товары",
|
||||||
|
action: "Проверить SEO-метаданные",
|
||||||
|
expectedResult: "Title, meta, slug корректные",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Корзина
|
||||||
|
{
|
||||||
|
key: "cart.add",
|
||||||
|
section: "Корзина",
|
||||||
|
action: "Добавить товар в корзину",
|
||||||
|
expectedResult: "Счётчик корзины обновляется",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cart.change-qty",
|
||||||
|
section: "Корзина",
|
||||||
|
action: "Изменить количество товара",
|
||||||
|
expectedResult: "Сумма пересчитывается",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cart.remove",
|
||||||
|
section: "Корзина",
|
||||||
|
action: "Удалить товар из корзины",
|
||||||
|
expectedResult: "Товар убирается, сумма пересчитывается",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Чекаут
|
||||||
|
{
|
||||||
|
key: "checkout.address",
|
||||||
|
section: "Чекаут",
|
||||||
|
action: "Выбрать адрес доставки",
|
||||||
|
expectedResult: "Можно выбрать из сохранённых или добавить новый",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "checkout.delivery",
|
||||||
|
section: "Чекаут",
|
||||||
|
action: "Выбрать способ доставки",
|
||||||
|
expectedResult: "Почта, OZON, Яндекс, 5post — доступны",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "checkout.payment",
|
||||||
|
section: "Чекаут",
|
||||||
|
action: "Выбрать способ оплаты",
|
||||||
|
expectedResult: "Онлайн / при получении — доступны",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "checkout.comment",
|
||||||
|
section: "Чекаут",
|
||||||
|
action: "Добавить комментарий к заказу",
|
||||||
|
expectedResult: "Поле работает, текст сохраняется",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "checkout.create",
|
||||||
|
section: "Чекаут",
|
||||||
|
action: "Создать заказ",
|
||||||
|
expectedResult: "Заказ создаётся, статус DRAFT",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Оплата
|
||||||
|
{
|
||||||
|
key: "payment.yookassa",
|
||||||
|
section: "Оплата",
|
||||||
|
action: "Оплатить через ЮKassa",
|
||||||
|
expectedResult: "Редирект на оплату, webhook обрабатывается",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "payment.status",
|
||||||
|
section: "Оплата",
|
||||||
|
action: "Проверить статус платежа",
|
||||||
|
expectedResult: "Статус обновляется после webhook",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Профиль пользователя
|
||||||
|
{
|
||||||
|
key: "profile.avatar",
|
||||||
|
section: "Профиль пользователя",
|
||||||
|
action: "Управление аватаром",
|
||||||
|
expectedResult: "Загрузка, отображение, удаление работают",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "profile.settings",
|
||||||
|
section: "Профиль пользователя",
|
||||||
|
action: "Изменить настройки профиля",
|
||||||
|
expectedResult: "Email, имя, способы входа обновляются",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "profile.addresses",
|
||||||
|
section: "Профиль пользователя",
|
||||||
|
action: "Управление адресами",
|
||||||
|
expectedResult: "Добавление, редактирование, удаление, по умолчанию",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "profile.orders",
|
||||||
|
section: "Профиль пользователя",
|
||||||
|
action: "Просмотр заказов",
|
||||||
|
expectedResult: "Список, детали, статусы отображаются",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "profile.messages",
|
||||||
|
section: "Профиль пользователя",
|
||||||
|
action: "Сообщения по заказу",
|
||||||
|
expectedResult: "Отправка, получение, read state работают",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "profile.notifications",
|
||||||
|
section: "Профиль пользователя",
|
||||||
|
action: "Настройки уведомлений",
|
||||||
|
expectedResult: "Вкл/выкл каналов работают",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "profile.delete-account",
|
||||||
|
section: "Профиль пользователя",
|
||||||
|
action: "Удалить аккаунт",
|
||||||
|
expectedResult: "Данные удаляются",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Админ — Товары
|
||||||
|
{
|
||||||
|
key: "admin-products.list",
|
||||||
|
section: "Админ — Товары",
|
||||||
|
action: "Открыть список товаров",
|
||||||
|
expectedResult: "Пагинация, поиск работают",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "admin-products.create",
|
||||||
|
section: "Админ — Товары",
|
||||||
|
action: "Создать товар",
|
||||||
|
expectedResult:
|
||||||
|
"Все поля сохраняются, фото загружаются, публикация работает",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "admin-products.edit",
|
||||||
|
section: "Админ — Товары",
|
||||||
|
action: "Редактировать товар",
|
||||||
|
expectedResult: "Изменения сохраняются",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "admin-products.delete",
|
||||||
|
section: "Админ — Товары",
|
||||||
|
action: "Удалить товар",
|
||||||
|
expectedResult: "Товар удаляется",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "admin-products.images",
|
||||||
|
section: "Админ — Товары",
|
||||||
|
action: "Управление изображениями товара",
|
||||||
|
expectedResult: "Добавление, сортировка, удаление работают",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Админ — Категории
|
||||||
|
{
|
||||||
|
key: "admin-categories.crud",
|
||||||
|
section: "Админ — Категории",
|
||||||
|
action: "CRUD категорий",
|
||||||
|
expectedResult: "Создание, редактирование, удаление, сортировка работают",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Админ — Заказы
|
||||||
|
{
|
||||||
|
key: "admin-orders.list",
|
||||||
|
section: "Админ — Заказы",
|
||||||
|
action: "Открыть список заказов",
|
||||||
|
expectedResult: "Фильтрация по статусу, внимание отображается",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "admin-orders.details",
|
||||||
|
section: "Админ — Заказы",
|
||||||
|
action: "Открыть детали заказа",
|
||||||
|
expectedResult: "Состав, статус, смена статуса работают",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "admin-orders.messages",
|
||||||
|
section: "Админ — Заказы",
|
||||||
|
action: "Ответить на сообщение заказа",
|
||||||
|
expectedResult: "Сообщение отправляется пользователю",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Админ — Отзывы
|
||||||
|
{
|
||||||
|
key: "admin-reviews.list",
|
||||||
|
section: "Админ — Отзывы",
|
||||||
|
action: "Открыть список отзывов",
|
||||||
|
expectedResult: "Фильтрация pending/approved/rejected работает",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "admin-reviews.moderate",
|
||||||
|
section: "Админ — Отзывы",
|
||||||
|
action: "Модерировать отзыв",
|
||||||
|
expectedResult: "Approve/reject работают",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Админ — Пользователи
|
||||||
|
{
|
||||||
|
key: "admin-users.list",
|
||||||
|
section: "Админ — Пользователи",
|
||||||
|
action: "Открыть список пользователей",
|
||||||
|
expectedResult: "Email, дата регистрации отображаются",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "admin-users.orders",
|
||||||
|
section: "Админ — Пользователи",
|
||||||
|
action: "Просмотр заказов пользователя",
|
||||||
|
expectedResult: "Заказы пользователя отображаются",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Админ — Галерея
|
||||||
|
{
|
||||||
|
key: "admin-gallery.upload",
|
||||||
|
section: "Админ — Галерея",
|
||||||
|
action: "Управление галереей",
|
||||||
|
expectedResult: "Загрузка, удаление, использование в слайдере работают",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Админ — Настройки
|
||||||
|
{
|
||||||
|
key: "admin-settings.notifications",
|
||||||
|
section: "Админ — Настройки",
|
||||||
|
action: "Настройки уведомлений админа",
|
||||||
|
expectedResult: "Email, telegram настраиваются",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Инфо-страницы
|
||||||
|
{
|
||||||
|
key: "info.pages",
|
||||||
|
section: "Инфо-страницы",
|
||||||
|
action: "Открыть инфо-страницы",
|
||||||
|
expectedResult:
|
||||||
|
"Доставка, оплата, как заказать, статусы заказов отображаются",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "info.legal",
|
||||||
|
section: "Инфо-страницы",
|
||||||
|
action: "Открыть юридические страницы",
|
||||||
|
expectedResult:
|
||||||
|
"Политика конфиденциальности, условия использования отображаются",
|
||||||
|
},
|
||||||
|
|
||||||
|
// SSE / Realtime
|
||||||
|
{
|
||||||
|
key: "sse.notifications",
|
||||||
|
section: "SSE / Realtime",
|
||||||
|
action: "Проверить SSE-уведомления",
|
||||||
|
expectedResult: "Уведомления приходят в реальном времени",
|
||||||
|
},
|
||||||
|
]);
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
export declare const ADMIN_UPLOAD_IMAGE_MAX_FILE_BYTES_DEFAULT: 20971520
|
||||||
|
|
||||||
|
export declare const ADMIN_UPLOAD_IMAGE_MAX_BYTES: 20971520
|
||||||
|
|
||||||
|
export declare function formatAdminImageMaxSizeHint(): string
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
const MB = 1024 * 1024
|
||||||
|
|
||||||
|
export const ADMIN_UPLOAD_IMAGE_MAX_FILE_BYTES_DEFAULT = 20 * MB
|
||||||
|
|
||||||
|
export const ADMIN_UPLOAD_IMAGE_MAX_BYTES = 20 * MB
|
||||||
|
|
||||||
|
export function formatAdminImageMaxSizeHint() {
|
||||||
|
return '20 МБ'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user