# КОММЕРЧЕСКИЕ ФУНКЦИИ И ЭЛЕКТРОННАЯ ТОРГОВЛЯ ## 🎯 ОБЗОР СИСТЕМЫ Коммерческая подсистема 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.name}

{/* Кнопка избранного */}

Арт: {product.article}

{/* Поставщик */}
{product.organization.name || product.organization.fullName || `ИНН ${product.organization.inn}`}
{/* Цена и наличие */}
{formatPrice(product.price)}
{product.quantity} шт.
{/* Кнопка добавления в корзину */}
{/* Модальное окно выбора количества */} Добавить в корзину
{/* Превью товара */}
{product.mainImage ? ( {product.name} ) : (
)}
{/* Информация */}

{product.name}

{formatPrice(product.price)} за шт.

Доступно: {product.quantity} шт.

{/* Выбор количества */}
{ const value = parseInt(e.target.value) if (value >= 1 && value <= product.quantity) { setQuantity(value) } }} min={1} max={product.quantity} className="w-20 text-center" />
{/* Итого */}
Итого: {formatPrice(product.price * 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} ) : (
)}

{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_