# КОММЕРЧЕСКИЕ ФУНКЦИИ И ЭЛЕКТРОННАЯ ТОРГОВЛЯ
## 🎯 ОБЗОР СИСТЕМЫ
Коммерческая подсистема SFERA обеспечивает полный цикл электронной торговли B2B между организациями различных типов. Включает каталог товаров, корзину заказов, избранное и систему оформления заказов с интеграцией в workflow поставок.
## 📊 МОДЕЛИ ДАННЫХ
### Модель Cart (Корзина заказов)
```typescript
// Prisma модель Cart - персональная корзина организации
model Cart {
id String @id @default(cuid())
organizationId String @unique // Один Cart на организацию
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
items CartItem[] // Товары в корзине
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
}
```
### Модель CartItem (Товар в корзине)
```typescript
// Prisma модель CartItem - конкретная позиция в корзине
model CartItem {
id String @id @default(cuid())
cartId String // Ссылка на корзину
productId String // Ссылка на товар
quantity Int @default(1) // Количество товара
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
// Уникальная связь: один товар = одна позиция в корзине
@@unique([cartId, productId])
}
```
### Модель Favorites (Избранное)
```typescript
// Prisma модель Favorites - избранные товары организации
model Favorites {
id String @id @default(cuid())
organizationId String // ID организации-покупателя
productId String // ID избранного товара
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
// Уникальная связь: один товар может быть избранным только один раз
@@unique([organizationId, productId])
}
```
### Модель Product (Товар)
```typescript
// Prisma модель Product - товары в каталоге поставщиков
model Product {
id String @id @default(cuid())
name String // Название товара
article String // Артикул товара
description String? // Описание
price Decimal @db.Decimal(12, 2) // Цена за единицу
pricePerSet Decimal? @db.Decimal(12, 2) // Цена за комплект
quantity Int @default(0) // Остаток на складе
setQuantity Int? // Количество в комплекте
ordered Int? // Заказано (резерв)
inTransit Int? // В пути
stock Int? // На складе
sold Int? // Продано
type ProductType @default(PRODUCT) // PRODUCT | CONSUMABLE
// Характеристики товара
categoryId String? // Категория
brand String? // Бренд
color String? // Цвет
size String? // Размер
weight Decimal? @db.Decimal(8, 3) // Вес в кг
dimensions String? // Габариты
material String? // Материал
// Медиафайлы
images Json @default("[]") // Массив URL изображений
mainImage String? // Основное изображение
isActive Boolean @default(true) // Активность товара
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organizationId String // ID организации-поставщика
// Relations
cartItems CartItem[] // Товар в корзинах
favorites Favorites[] // Товар в избранном
category Category? @relation(fields: [categoryId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
supplyOrderItems SupplyOrderItem[] // Позиции в заказах поставок
// Уникальность артикула в рамках организации
@@unique([organizationId, article])
}
```
## 🏗️ АРХИТЕКТУРА КОМПОНЕНТОВ
### Структура коммерческих компонентов
```
src/components/cart/
├── cart-dashboard.tsx # 🛒 Главная панель корзины
├── cart-items.tsx # 📋 Список товаров в корзине
└── cart-summary.tsx # 💰 Сводка по заказу
src/components/favorites/
├── favorites-dashboard.tsx # ❤️ Панель избранного
└── favorites-items.tsx # 📋 Список избранных товаров
src/components/market/
├── market-dashboard.tsx # 🏪 Главная панель маркета
├── market-categories.tsx # 📂 Категории товаров
├── market-sellers.tsx # 🏢 Список поставщиков
├── market-requests.tsx # 📦 Заявки (корзина в маркете)
├── product-card.tsx # 🏷️ Карточка товара
└── organization-avatar.tsx # 🏢 Аватар организации
src/components/supplies/
├── floating-cart.tsx # 🛒 Плавающая корзина
├── product-card.tsx # 🏷️ Карточка товара (другой стиль)
├── supplier-products.tsx # 📦 Товары поставщика
└── supplier-products-page.tsx # 📄 Страница товаров поставщика
```
### Главная панель корзины
```typescript
// CartDashboard - управление заказами организации
export function CartDashboard() {
const { data, loading, error } = useQuery(GET_MY_CART)
const cart = data?.myCart
const hasItems = cart?.items && cart.items.length > 0
// Состояния загрузки и ошибок
if (loading) return
if (error) return
return (
{/* Заголовок с метриками */}
Корзина
{hasItems
? `${cart.totalItems} товаров на сумму ${formatPrice(cart.totalPrice)}`
: 'Ваша корзина пуста'}
{/* Основной контент */}
{hasItems ? (
{/* Товары в корзине (2/3 экрана) */}
{/* Сводка заказа (1/3 экрана) */}
) : (
)}
)
}
```
### Товары в корзине с группировкой
```typescript
// CartItems - список товаров с группировкой по поставщикам
export function CartItems({ cart }: CartItemsProps) {
const [loadingItems, setLoadingItems] = useState>(new Set())
const [quantities, setQuantities] = useState>({})
// Мутации для управления корзиной
const [updateCartItem] = useMutation(UPDATE_CART_ITEM, {
refetchQueries: [{ query: GET_MY_CART }],
onCompleted: (data) => {
if (data.updateCartItem.success) {
toast.success(data.updateCartItem.message)
}
}
})
const [removeFromCart] = useMutation(REMOVE_FROM_CART, {
refetchQueries: [{ query: GET_MY_CART }]
})
const [clearCart] = useMutation(CLEAR_CART, {
refetchQueries: [{ query: GET_MY_CART }]
})
// Группировка товаров по поставщикам
const groupedItems = cart.items.reduce((groups, item) => {
const orgId = item.product.organization.id
if (!groups[orgId]) {
groups[orgId] = {
organization: item.product.organization,
items: [],
totalPrice: 0,
totalItems: 0
}
}
groups[orgId].items.push(item)
groups[orgId].totalPrice += item.totalPrice
groups[orgId].totalItems += item.quantity
return groups
}, {})
const supplierGroups = Object.values(groupedItems)
// Управление количеством товара
const updateQuantity = async (productId: string, newQuantity: number) => {
if (newQuantity <= 0) return
setLoadingItems(prev => new Set(prev).add(productId))
try {
await updateCartItem({
variables: { productId, quantity: newQuantity }
})
} finally {
setLoadingItems(prev => {
const newSet = new Set(prev)
newSet.delete(productId)
return newSet
})
}
}
return (
{/* Заголовок с кнопкой очистки */}
Заявки на товары
{cart.items.length > 0 && (
)}
{/* Группы поставщиков */}
{supplierGroups.map(group => (
))}
)
}
```
## 🛒 ФУНКЦИИ КОРЗИНЫ
### Добавление товара в корзину
```typescript
// Логика добавления товара в корзину
const addToCartLogic = async (productId: string, quantity: number = 1) => {
// 1. Проверка наличия товара
const product = await validateProduct(productId)
if (!product.isActive) {
throw new Error('Товар недоступен для заказа')
}
// 2. Проверка количества
if (quantity > product.quantity) {
throw new Error(`Доступно только ${product.quantity} единиц`)
}
// 3. Получение или создание корзины
let cart = await prisma.cart.findUnique({
where: { organizationId: user.organizationId },
})
if (!cart) {
cart = await prisma.cart.create({
data: { organizationId: user.organizationId },
})
}
// 4. Проверка существующей позиции
const existingItem = await prisma.cartItem.findUnique({
where: {
cartId_productId: {
cartId: cart.id,
productId,
},
},
})
if (existingItem) {
// Обновляем количество
const newQuantity = existingItem.quantity + quantity
if (newQuantity > product.quantity) {
throw new Error(`Максимальное количество: ${product.quantity}`)
}
await prisma.cartItem.update({
where: { id: existingItem.id },
data: { quantity: newQuantity },
})
} else {
// Создаем новую позицию
await prisma.cartItem.create({
data: {
cartId: cart.id,
productId,
quantity,
},
})
}
return { success: true, message: 'Товар добавлен в корзину' }
}
```
### Обновление количества товара
```typescript
// Логика изменения количества в корзине
const updateCartItemLogic = async (productId: string, quantity: number) => {
// 1. Валидация входных данных
if (quantity < 1) {
throw new Error('Количество должно быть больше 0')
}
// 2. Поиск позиции в корзине
const cartItem = await prisma.cartItem.findFirst({
where: {
cart: { organizationId: user.organizationId },
productId,
},
include: { product: true },
})
if (!cartItem) {
throw new Error('Товар не найден в корзине')
}
// 3. Проверка доступности количества
if (quantity > cartItem.product.quantity) {
throw new Error(`Доступно только ${cartItem.product.quantity} единиц`)
}
// 4. Обновление количества
await prisma.cartItem.update({
where: { id: cartItem.id },
data: { quantity },
})
return { success: true, message: 'Количество обновлено' }
}
```
## ❤️ СИСТЕМА ИЗБРАННОГО
### Главная панель избранного
```typescript
// FavoritesDashboard - управление избранными товарами
export function FavoritesDashboard({ onBackToCategories }: FavoritesDashboardProps) {
const { data, loading, error } = useQuery(GET_MY_FAVORITES)
const favorites = data?.myFavorites || []
if (loading) return
if (error) return
return (
)
}
```
### Управление избранными товарами
```typescript
// Логика добавления/удаления из избранного
const toggleFavoriteLogic = async (productId: string, action: 'add' | 'remove') => {
const organizationId = user.organizationId
if (action === 'add') {
// Проверка дублирования
const existing = await prisma.favorites.findUnique({
where: {
organizationId_productId: {
organizationId,
productId,
},
},
})
if (existing) {
return { success: false, message: 'Товар уже в избранном' }
}
// Добавление в избранное
await prisma.favorites.create({
data: { organizationId, productId },
})
return { success: true, message: 'Добавлено в избранное' }
} else {
// Удаление из избранного
await prisma.favorites.deleteMany({
where: { organizationId, productId },
})
return { success: true, message: 'Удалено из избранного' }
}
}
```
## 🏪 МАРКЕТПЛЕЙС
### Главная панель маркета
```typescript
// MarketDashboard - B2B маркетплейс для поиска поставщиков
export function MarketDashboard() {
const [currentView, setCurrentView] = useState<'categories' | 'products' | 'cart' | 'favorites'>('categories')
const [selectedCategory, setSelectedCategory] = useState(null)
return (
{/* Навигация по разделам */}
{/* Основной контент */}
{currentView === 'categories' && (
{
setSelectedCategory(categoryId)
setCurrentView('products')
}}
onShowCart={() => setCurrentView('cart')}
onShowFavorites={() => setCurrentView('favorites')}
/>
)}
{currentView === 'products' && selectedCategory && (
setCurrentView('categories')}
/>
)}
{currentView === 'cart' && (
setCurrentView('categories')}
/>
)}
{currentView === 'favorites' && (
setCurrentView('categories')}
/>
)}
)
}
```
### Карточка товара в маркете
```typescript
// ProductCard - интерактивная карточка товара с управлением
export function ProductCard({ product, onAddToCart, compact = false }: ProductCardProps) {
const [isModalOpen, setIsModalOpen] = useState(false)
const [quantity, setQuantity] = useState(1)
// Мутации для корзины и избранного
const [addToCart, { loading: addingToCart }] = useMutation(ADD_TO_CART, {
refetchQueries: [{ query: GET_MY_CART }]
})
const [addToFavorites] = useMutation(ADD_TO_FAVORITES, {
refetchQueries: [{ query: GET_MY_FAVORITES }]
})
const [removeFromFavorites] = useMutation(REMOVE_FROM_FAVORITES, {
refetchQueries: [{ query: GET_MY_FAVORITES }]
})
// Проверка статуса избранного
const { data: favoritesData } = useQuery(GET_MY_FAVORITES)
const favorites = favoritesData?.myFavorites || []
const isFavorite = favorites.some(fav => fav.id === product.id)
const handleAddToCart = async () => {
try {
await addToCart({
variables: {
productId: product.id,
quantity
}
})
setQuantity(1) // Сброс количества
setIsModalOpen(false) // Закрытие модального окна
onAddToCart?.()
} catch (error) {
console.error('Error adding to cart:', error)
}
}
const toggleFavorite = async () => {
try {
if (isFavorite) {
await removeFromFavorites({
variables: { productId: product.id }
})
} else {
await addToFavorites({
variables: { productId: product.id }
})
}
} catch (error) {
console.error('Error toggling favorite:', error)
}
}
return (
<>
{/* Компактная карточка */}
{/* Изображение товара */}
{product.mainImage || product.images?.[0] ? (
) : (
)}
{/* Информация о товаре */}
{product.name}
{/* Кнопка избранного */}
Арт: {product.article}
{/* Поставщик */}
{product.organization.name ||
product.organization.fullName ||
`ИНН ${product.organization.inn}`}
{/* Цена и наличие */}
{formatPrice(product.price)}
{product.quantity} шт.
{/* Кнопка добавления в корзину */}
{/* Модальное окно выбора количества */}
>
)
}
```
## 🔧 GraphQL API
### Запросы (Queries)
```graphql
# Получение корзины организации
query GetMyCart {
myCart {
id
totalPrice
totalItems
items {
id
quantity
totalPrice
isAvailable
availableQuantity
product {
id
name
article
price
quantity
images
mainImage
organization {
id
name
fullName
inn
}
}
}
}
}
# Получение избранных товаров
query GetMyFavorites {
myFavorites {
id
name
article
price
quantity
images
mainImage
isActive
organization {
id
name
fullName
inn
type
}
category {
id
name
}
}
}
# Получение каталога товаров по категории
query GetProducts(
$categoryId: ID
$organizationType: OrganizationType
$search: String
$limit: Int = 20
$offset: Int = 0
) {
products(
categoryId: $categoryId
organizationType: $organizationType
search: $search
limit: $limit
offset: $offset
) {
products {
id
name
article
description
price
quantity
images
mainImage
brand
color
size
weight
dimensions
isActive
organization {
id
name
fullName
inn
type
phones
emails
}
category {
id
name
}
}
totalCount
hasMore
}
}
# Получение категорий товаров
query GetCategories {
categories {
id
name
createdAt
updatedAt
}
}
```
### Мутации (Mutations)
```graphql
# Добавление товара в корзину
mutation AddToCart($productId: ID!, $quantity: Int = 1) {
addToCart(productId: $productId, quantity: $quantity) {
success
message
cartItem {
id
quantity
totalPrice
product {
id
name
price
}
}
}
}
# Обновление количества товара в корзине
mutation UpdateCartItem($productId: ID!, $quantity: Int!) {
updateCartItem(productId: $productId, quantity: $quantity) {
success
message
cartItem {
id
quantity
totalPrice
}
}
}
# Удаление товара из корзины
mutation RemoveFromCart($productId: ID!) {
removeFromCart(productId: $productId) {
success
message
}
}
# Очистка корзины
mutation ClearCart {
clearCart {
success
message
}
}
# Добавление в избранное
mutation AddToFavorites($productId: ID!) {
addToFavorites(productId: $productId) {
success
message
favorite {
id
productId
organizationId
createdAt
}
}
}
# Удаление из избранного
mutation RemoveFromFavorites($productId: ID!) {
removeFromFavorites(productId: $productId) {
success
message
}
}
# Оформление заказа из корзины
mutation CreateSupplyOrder($items: [SupplyOrderItemInput!]!, $deliveryDate: DateTime!, $notes: String) {
createSupplyOrder(items: $items, deliveryDate: $deliveryDate, notes: $notes) {
success
message
supplyOrder {
id
status
totalAmount
totalItems
deliveryDate
partner {
id
name
fullName
}
items {
id
quantity
price
totalPrice
product {
id
name
article
}
}
}
}
}
```
## 📊 БИЗНЕС-ЛОГИКА И ПРАВИЛА
### Правила корзины
1. **Уникальность товара**: Один товар = одна позиция в корзине
2. **Проверка наличия**: Количество не может превышать остаток на складе
3. **Автоматическая корзина**: Корзина создается автоматически при первом добавлении
4. **Группировка по поставщикам**: Товары группируются по организациям-поставщикам
5. **Валидация статуса**: Только активные товары можно добавлять в корзину
### Правила избранного
1. **Уникальность**: Один товар может быть добавлен в избранное только один раз
2. **Доступность**: Неактивные товары остаются в избранном, но помечаются как недоступные
3. **Быстрое добавление**: Из избранного можно быстро добавить товар в корзину
### Интеграция с поставками
```typescript
// Преобразование корзины в заказ поставки
const convertCartToSupplyOrder = async (organizationId: string, deliveryDate: Date) => {
// 1. Получение корзины с группировкой по поставщикам
const cart = await prisma.cart.findUnique({
where: { organizationId },
include: {
items: {
include: {
product: {
include: { organization: true },
},
},
},
},
})
if (!cart || cart.items.length === 0) {
throw new Error('Корзина пуста')
}
// 2. Группировка по поставщикам
const supplierGroups = groupItemsBySupplier(cart.items)
// 3. Создание отдельных заказов для каждого поставщика
const supplyOrders = []
for (const [supplierId, items] of Object.entries(supplierGroups)) {
const totalAmount = items.reduce((sum, item) => sum + item.product.price * item.quantity, 0)
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0)
// Создание заказа поставки
const supplyOrder = await prisma.supplyOrder.create({
data: {
organizationId,
partnerId: supplierId,
deliveryDate,
status: 'PENDING',
totalAmount,
totalItems,
items: {
create: items.map((item) => ({
productId: item.productId,
quantity: item.quantity,
price: item.product.price,
totalPrice: item.product.price * item.quantity,
})),
},
},
})
supplyOrders.push(supplyOrder)
}
// 4. Очистка корзины после создания заказов
await prisma.cartItem.deleteMany({
where: { cartId: cart.id },
})
return supplyOrders
}
```
## 🔍 ПОИСК И ФИЛЬТРАЦИЯ
### Фильтрация товаров
```typescript
// Система поиска и фильтрации в маркете
const searchProducts = async (filters: ProductFilters) => {
const {
search, // Текстовый поиск
categoryId, // Категория
organizationType, // Тип поставщика (WHOLESALE, FULFILLMENT)
priceFrom, // Цена от
priceTo, // Цена до
inStockOnly, // Только товары в наличии
brandIds, // Фильтр по брендам
limit = 20,
offset = 0,
} = filters
const where: Prisma.ProductWhereInput = {
isActive: true,
// Текстовый поиск по названию и артикулу
...(search && {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ article: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
],
}),
// Фильтр по категории
...(categoryId && { categoryId }),
// Фильтр по типу организации-поставщика
...(organizationType && {
organization: { type: organizationType },
}),
// Ценовой фильтр
...(priceFrom && { price: { gte: priceFrom } }),
...(priceTo && { price: { lte: priceTo } }),
// Только товары в наличии
...(inStockOnly && { quantity: { gt: 0 } }),
// Фильтр по брендам
...(brandIds?.length && {
brand: { in: brandIds },
}),
}
const [products, totalCount] = await Promise.all([
prisma.product.findMany({
where,
include: {
organization: {
select: {
id: true,
name: true,
fullName: true,
inn: true,
type: true,
},
},
category: {
select: {
id: true,
name: true,
},
},
},
orderBy: [
{ quantity: 'desc' }, // Сначала товары в наличии
{ createdAt: 'desc' }, // Потом новые
],
take: limit,
skip: offset,
}),
prisma.product.count({ where }),
])
return {
products,
totalCount,
hasMore: offset + limit < totalCount,
}
}
```
## 📱 МОБИЛЬНАЯ АДАПТАЦИЯ
### Адаптивные компоненты
```typescript
// Мобильная версия карточки товара
const ProductCardMobile = ({ product }: ProductCardProps) => {
const [isExpanded, setIsExpanded] = useState(false)
return (
{/* Компактное отображение */}
{product.mainImage ? (
) : (
)}
{product.name}
{formatPrice(product.price)} • {product.quantity} шт.
{/* Расширенная информация */}
{isExpanded && (
Арт: {product.article}
{product.description && (
{product.description}
)}
{/* Кнопки действий */}
)}
)
}
```
## 🔒 БЕЗОПАСНОСТЬ
### Валидация и права доступа
```typescript
// Проверка прав на добавление товара в корзину
const validateCartAccess = async (userId: string, productId: string) => {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { organization: true },
})
const product = await prisma.product.findUnique({
where: { id: productId },
include: { organization: true },
})
if (!user?.organization || !product) {
throw new GraphQLError('Недостаточно данных')
}
// Нельзя добавлять свои товары в корзину
if (user.organizationId === product.organizationId) {
throw new GraphQLError('Нельзя заказывать собственные товары')
}
// Проверка партнерских отношений
const partnership = await prisma.counterparty.findFirst({
where: {
organizationId: user.organizationId,
counterpartyId: product.organizationId,
},
})
if (!partnership) {
throw new GraphQLError('Заказы доступны только от партнерских организаций')
}
return true
}
```
### Защита от дублирования
```typescript
// Предотвращение дублирования в корзине
const preventDuplicateCartItems = async (cartId: string, productId: string) => {
const existing = await prisma.cartItem.findUnique({
where: {
cartId_productId: {
cartId,
productId,
},
},
})
return !existing
}
```
## 📈 АНАЛИТИКА И МЕТРИКИ
### Статистика использования корзины
```typescript
// Сбор метрик коммерческих функций
const collectCommerceMetrics = async (organizationId: string, period: string) => {
const dateFrom = getDateFromPeriod(period)
const dateTo = new Date()
const [cartMetrics, favoriteMetrics, orderMetrics] = await Promise.all([
// Метрики корзины
prisma.$queryRaw`
SELECT
COUNT(DISTINCT ci.product_id) as unique_products,
SUM(ci.quantity) as total_quantity,
AVG(ci.quantity) as avg_quantity_per_item,
COUNT(*) as total_additions
FROM cart_items ci
JOIN carts c ON ci.cart_id = c.id
WHERE c.organization_id = ${organizationId}
AND ci.created_at BETWEEN ${dateFrom} AND ${dateTo}
`,
// Метрики избранного
prisma.$queryRaw`
SELECT
COUNT(*) as total_favorites,
COUNT(DISTINCT product_id) as unique_products
FROM favorites
WHERE organization_id = ${organizationId}
AND created_at BETWEEN ${dateFrom} AND ${dateTo}
`,
// Метрики заказов
prisma.$queryRaw`
SELECT
COUNT(*) as total_orders,
SUM(total_amount) as total_amount,
SUM(total_items) as total_items,
AVG(total_amount) as avg_order_amount
FROM supply_orders
WHERE organization_id = ${organizationId}
AND created_at BETWEEN ${dateFrom} AND ${dateTo}
`,
])
return {
cart: cartMetrics[0],
favorites: favoriteMetrics[0],
orders: orderMetrics[0],
period,
dateFrom,
dateTo,
}
}
```
---
_Извлечено из анализа: Cart/CartItem/Favorites модели + 15 компонентов коммерции_
_Источники: src/components/cart/, src/components/favorites/, src/components/market/, prisma/schema.prisma_
_Создано: 2025-08-21_