Модуль Commerce (Продукты, Корзина, Заказы)
Обзор
Модуль Commerce обеспечивает полную функциональность электронной коммерции платформы IWM, включая управление каталогом продуктов, операции с корзиной, процесс оформления заказа, выполнение заказов, обработку платежей и интеграцию с доставкой. Он интегрируется с модулем MLM для реферальной атрибуции и начисления комиссий.
Зоны ответственности
- Каталог продуктов и управление категориями
- Отслеживание инвентаря и управление запасами
- Корзина покупок (гостевая и авторизованная)
- Многошаговый процесс оформления заказа
- Управление жизненным циклом заказа
- Обработка платежей (Stripe, YooKassa)
- Интеграция с доставкой (CDEK, DHL)
- Обработка возвратов и рефандов
- Реферальная атрибуция для заказов
Доменные сущности
Product
Основная сущность продукта для каталога.
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор продукта |
| category_id | UUID | Ссылка на категорию |
| sku | VARCHAR(50) | Артикул (уникальный) |
| name | VARCHAR(200) | Название продукта |
| slug | VARCHAR(200) | URL-дружественный идентификатор |
| short_description | VARCHAR(500) | Краткое описание |
| description | TEXT | Полное описание |
| base_price | DECIMAL | Обычная цена |
| sale_price | DECIMAL | Цена со скидкой |
| cost_price | DECIMAL | Себестоимость |
| currency | VARCHAR(3) | Валюта цены |
| status | ENUM | Статус продукта |
| stock_quantity | INT | Доступный запас |
| low_stock_threshold | INT | Порог предупреждения |
| weight_grams | INT | Вес продукта |
| dimensions | JSONB | Длина, ширина, высота |
| career_points_value | DECIMAL | MLM очки за покупку |
| is_featured | BOOLEAN | Флаг рекомендуемого |
Category
Категория продуктов с иерархической структурой.
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор категории |
| name | VARCHAR(100) | Название категории |
| slug | VARCHAR(100) | URL-дружественный идентификатор |
| description | TEXT | Описание категории |
| parent_id | UUID | Родительская категория |
| path | LTREE | Материализованный путь для иерархии |
| image_url | VARCHAR(500) | Изображение категории |
| sort_order | INT | Порядок отображения |
| is_active | BOOLEAN | Статус активности |
Cart
Корзина покупок для пользователей и гостей.
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор корзины |
| user_id | UUID | Ссылка на пользователя (опционально) |
| session_id | VARCHAR(100) | ID гостевой сессии |
| referring_partner_id | UUID | MLM атрибуция |
| status | ENUM | Статус корзины |
| subtotal | DECIMAL | Сумма товаров |
| discount_total | DECIMAL | Применненые скидки |
| total | DECIMAL | Итоговая сумма |
| currency | VARCHAR(3) | Валюта корзины |
| expires_at | TIMESTAMP | Срок истечения корзины |
CartItem
Отдельные товары в корзине.
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор товара |
| cart_id | UUID | Ссылка на корзину |
| product_id | UUID | Ссылка на продукт |
| quantity | INT | Количество |
| unit_price | DECIMAL | Цена за единицу |
| total_price | DECIMAL | Сумма позиции |
| added_at | TIMESTAMP | Время добавления |
Order
Запись о заказе клиента.
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор заказа |
| order_number | VARCHAR(20) | Читаемый номер |
| user_id | UUID | Ссылка на клиента |
| status | ENUM | Статус заказа |
| subtotal | DECIMAL | Сумма товаров |
| discount_total | DECIMAL | Примененные скидки |
| shipping_cost | DECIMAL | Стоимость доставки |
| tax_amount | DECIMAL | Применимые налоги |
| total | DECIMAL | Итоговая сумма |
| currency | VARCHAR(3) | Валюта заказа |
| shipping_address_id | UUID | Адрес доставки |
| billing_address_id | UUID | Адрес для счета |
| shipping_method_id | UUID | Способ доставки |
| referring_partner_id | UUID | MLM атрибуция |
| notes | TEXT | Примечания клиента |
OrderItem
Позиции заказа (снимок продукта на момент покупки).
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор позиции |
| order_id | UUID | Ссылка на заказ |
| product_id | UUID | Ссылка на продукт |
| sku | VARCHAR(50) | Снимок артикула |
| name | VARCHAR(200) | Снимок названия |
| quantity | INT | Количество |
| unit_price | DECIMAL | Цена за единицу |
| total_price | DECIMAL | Сумма позиции |
| career_points_value | DECIMAL | Заработанные MLM очки |
Payment
Платежная транзакция для заказа.
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор платежа |
| order_id | UUID | Ссылка на заказ |
| amount | DECIMAL | Сумма платежа |
| currency | VARCHAR(3) | Валюта платежа |
| method | ENUM | CARD, EWALLET, BANK_TRANSFER |
| provider | ENUM | STRIPE, YOOKASSA |
| status | ENUM | Статус платежа |
| provider_reference | VARCHAR(255) | Внешний ID транзакции |
| provider_response | JSONB | Полный ответ провайдера |
| paid_at | TIMESTAMP | Время успешного платежа |
| failed_at | TIMESTAMP | Время неудачи |
| refunded_at | TIMESTAMP | Время рефанда |
Shipment
Отслеживание доставки заказа.
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор отправления |
| order_id | UUID | Ссылка на заказ |
| carrier | VARCHAR(50) | CDEK, DHL и т.д. |
| tracking_number | VARCHAR(100) | Номер отслеживания |
| status | ENUM | Статус отправления |
| shipped_at | TIMESTAMP | Дата отправки |
| estimated_delivery | DATE | Ожидаемая доставка |
| delivered_at | TIMESTAMP | Фактическая доставка |
| tracking_events | JSONB | История отслеживания |
Address
Адреса клиентов для доставки/счетов.
| Поле | Тип | Описание |
|---|---|---|
| id | UUID | Идентификатор адреса |
| user_id | UUID | Ссылка на пользователя |
| type | ENUM | SHIPPING, BILLING |
| first_name | VARCHAR(100) | Имя получателя |
| last_name | VARCHAR(100) | Фамилия получателя |
| company | VARCHAR(200) | Название компании |
| address_line1 | VARCHAR(255) | Адрес улицы |
| address_line2 | VARCHAR(255) | Квартира, офис и т.д. |
| city | VARCHAR(100) | Город |
| state | VARCHAR(100) | Область/регион |
| postal_code | VARCHAR(20) | Почтовый индекс |
| country | VARCHAR(3) | ISO 3166-1 alpha-3 |
| phone | VARCHAR(20) | Контактный телефон |
| is_default | BOOLEAN | Флаг адреса по умолчанию |
Каталог продуктов
Иерархия категорий (LTREE)
Категории используют PostgreSQL LTREE для эффективных иерархических запросов:
Ключевые запросы LTREE:
-- Get all products in "Health" and subcategories
SELECT p.* FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.path <@ 'health';
-- Get direct children of a category
SELECT * FROM categories
WHERE path ~ 'health.*{1}';
-- Get all ancestors of a category
SELECT * FROM categories
WHERE 'health.vitamins.multivitamins' <@ path;Атрибуты продуктов
Продукты поддерживают динамические атрибуты через JSONB:
{
"color": "Blue",
"size": "Large",
"material": "Organic Cotton",
"weight_net": "500g",
"ingredients": ["Vitamin C", "Zinc", "Elderberry"]
}Изображения продуктов
Несколько изображений на продукт с сортировкой:
| Поле | Описание |
|---|---|
| url | CDN URL изображения |
| alt_text | Текст для доступности |
| sort_order | Порядок отображения |
| is_primary | Главное изображение продукта |
Управление инвентарем
Отслеживание запасов
Операции с запасами
| Операция | Триггер | Эффект |
|---|---|---|
| Reserve | Добавление в корзину | Временно резервирует запас |
| Release | Истечение корзины, удаление товара | Возвращает зарезервированный запас |
| Commit | Подтверждение заказа | Постоянно выделяет запас |
| Deduct | Отправка заказа | Списывает из инвентаря |
| Restore | Отмена заказа/рефанд | Возвращает выделенный запас |
Предупреждения о низком запасе
Когда stock_quantity <= low_stock_threshold:
- Отправляется уведомление администратору
- Продукт помечается для дозаказа
- Опционально: Автоскрытие при запасе = 0
Правила резервирования запасов
- Резервирование корзины: 30 минут
- Резервирование оформления: 15 минут
- Подтверждение заказа: Постоянное до отправки/отмены
- Неудачный платеж: Запас освобождается через 1 час
Система корзины
Гостевые vs Пользовательские корзины
Гостевые корзины
- Идентифицируются по
session_id(UUID в cookie) - Истекают через 7 дней неактивности
- Реферальная атрибуция сохраняется в корзине
Пользовательские корзины
- Привязаны к
user_id - Сохраняются бессрочно
- Автовосстановление при входе
Слияние корзин при входе
Когда гость с товарами входит в систему:
- Найти существующую корзину пользователя (если есть)
- Для каждого товара гостевой корзины:
- Если продукт есть в корзине пользователя: Обновить количество
- Если продукта нет в корзине пользователя: Добавить товар
- Пересчитать итоги
- Удалить гостевую корзину
- Сохранить реферальную атрибуцию (побеждает последняя)
Истечение корзины
Процесс оформления заказа
Многошаговое оформление
Шаг 1: Валидация корзины
- Проверить наличие всех товаров на складе
- Проверить доступность продуктов (не сняты с продажи)
- Пересчитать цены (могли измениться)
- Применить автоматические скидки
Шаг 2: Выбор доставки
- Пользователь выбирает/добавляет адрес доставки
- Рассчитываются доступные способы доставки
- Отображается стоимость доставки
- Показываются ожидаемые сроки доставки
Шаг 3: Оплата
- Выбор способа оплаты
- Ввод платежных данных
- Применение промокодов
- Расчет итоговой суммы с налогами
Шаг 4: Подтверждение
- Проверка сводки заказа
- Принятие условий
- Окончательное оформление заказа
- Обработка платежа
Машина состояний заказа
Диаграмма состояний
Допустимые переходы
| Из | В | Триггер | Условия |
|---|---|---|---|
| PENDING | CONFIRMED | payment_success | Платеж выполнен |
| PENDING | CANCELLED | cancel | Платеж не прошел или пользователь отменил |
| CONFIRMED | PROCESSING | start_processing | Начало комплектации |
| CONFIRMED | CANCELLED | cancel | Только до отправки |
| PROCESSING | SHIPPED | ship | Присвоен номер отслеживания |
| PROCESSING | CANCELLED | cancel | Товар недоступен |
| SHIPPED | DELIVERED | deliver | Подтверждение от курьера |
| DELIVERED | REFUNDED | refund | В пределах 14-дневного окна |
Временные метки заказа
| Статус | Поле временной метки |
|---|---|
| CONFIRMED | confirmed_at |
| SHIPPED | shipped_at |
| DELIVERED | delivered_at |
| CANCELLED | cancelled_at |
| REFUNDED | refunded_at |
Интеграция платежей
Поддерживаемые провайдеры
| Провайдер | Рынки | Методы |
|---|---|---|
| Stripe | Международный | Card, Apple Pay, Google Pay |
| YooKassa | Россия/СНГ | Card, SberPay, YooMoney |
Процесс оплаты
Обработка вебхуков
Идемпотентность
- Каждый платеж имеет уникальный
idempotency_key - Предотвращает повторные списания при повторных попытках
- Формат ключа:
order_{orderId}_{timestamp}
Статусы платежей
| Статус | Описание |
|---|---|
| PENDING | Платеж инициирован |
| PROCESSING | Обрабатывается провайдером |
| COMPLETED | Успешно списано |
| FAILED | Платеж не прошел |
| REFUNDED | Полностью возвращен |
Интеграция доставки
Поддерживаемые курьерские службы
| Служба | Покрытие | Функции |
|---|---|---|
| CDEK | Россия, СНГ | Курьером до двери, пункты выдачи |
| DHL | Международный | Экспресс, стандарт |
Процесс доставки
Статусы отправления
| Статус | Описание |
|---|---|
| PREPARING | Заказ упаковывается |
| SHIPPED | Передан курьеру |
| IN_TRANSIT | У курьера, в пути |
| OUT_FOR_DELIVERY | На последней миле |
| DELIVERED | Успешно доставлен |
| RETURNED | Возвращен отправителю |
События отслеживания
Сохраняются как JSONB массив:
[
{
"timestamp": "2024-01-15T10:30:00Z",
"status": "SHIPPED",
"location": "Moscow, Russia",
"description": "Shipment picked up"
},
{
"timestamp": "2024-01-16T14:20:00Z",
"status": "IN_TRANSIT",
"location": "Novosibirsk, Russia",
"description": "In transit to destination"
}
]Возвраты и рефанды
Окно возврата
- Стандартное: 14 дней с момента доставки
- Расширенное (VIP): 30 дней
- Без возврата: Цифровые продукты, вскрытые добавки
Процесс рефанда
Частичные рефанды
- Поддерживаются для заказов с несколькими товарами
- Можно возвратить отдельные позиции
- Возврат стоимости доставки опционален
- Пропорциональная корректировка скидок
Влияние рефанда на MLM
При возврате заказа:
- Связанные комиссии отменяются
- Если комиссии были выплачены, создается clawback
- Карьерные очки вычитаются
- Балансы партнеров корректируются
Публикуемые события
| Событие | Триггер | Payload |
|---|---|---|
| ProductCreated | Добавлен новый продукт | productId, sku, categoryId |
| ProductUpdated | Продукт изменен | productId, changedFields |
| ProductStatusChanged | Переход статуса | productId, fromStatus, toStatus |
| StockLevelChanged | Изменение инвентаря | productId, previousQty, newQty |
| LowStockAlert | Ниже порога | productId, currentQty, threshold |
| CartCreated | Новая корзина | cartId, userId/sessionId |
| CartItemAdded | Товар добавлен в корзину | cartId, productId, quantity |
| CartItemRemoved | Товар удален из корзины | cartId, productId |
| CartAbandoned | Корзина истекла | cartId, userId, items |
| OrderCreated | Заказ размещен | orderId, userId, total |
| OrderConfirmed | Платеж успешен | orderId, paymentId |
| OrderStatusChanged | Переход статуса | orderId, fromStatus, toStatus |
| OrderCancelled | Заказ отменен | orderId, reason |
| OrderShipped | Отправление создано | orderId, trackingNumber |
| OrderDelivered | Доставка подтверждена | orderId, deliveredAt |
| PaymentReceived | Платеж успешен | paymentId, orderId, amount |
| PaymentFailed | Платеж не прошел | paymentId, orderId, reason |
| RefundInitiated | Рефанд начат | orderId, amount, reason |
| RefundCompleted | Рефанд обработан | orderId, refundId, amount |
| ShipmentUpdated | Обновление отслеживания | shipmentId, status, location |
Потребляемые события
| Событие | Источник | Обработчик |
|---|---|---|
| UserRegistered | Core | Слить гостевую корзину, если есть |
| UserLoggedIn | Core | Восстановить/слить корзину пользователя |
| ReferralAttributed | MLM | Сохранить атрибуцию в корзине |
Бизнес-правила и инварианты
Правила продуктов
- SKU должен быть уникальным среди всех продуктов
- Цена со скидкой не может превышать базовую цену
- Количество на складе не может быть отрицательным
- Снятые с продажи продукты нельзя добавить в корзину
- Продукты с запасом 0 показываются как "Нет в наличии"
Правила корзины
- Максимум 50 уникальных позиций на корзину
- Максимальное количество на позицию: 99
- Сумма корзины не может превышать лимиты платформы
- Гостевые корзины истекают через 7 дней
- Реферальная атрибуция сохраняется через оформление заказа
Правила заказов
- Формат номера заказа: IWM-{YYYYMMDD}-
- Переходы статусов должны следовать машине состояний
- Нельзя отменить после отправки
- Окно возврата: 14 дней с момента доставки
- Может применяться минимальная сумма заказа
Правила платежей
- Один успешный платеж на заказ
- Ключ идемпотентности предотвращает повторные списания
- Подпись вебхука должна быть проверена
- Неудачные платежи повторяются до 3 раз
Правила инвентаря
- Запас резервируется при добавлении в корзину
- Зарезервированный запас освобождается при истечении корзины
- Запас фиксируется при подтверждении заказа
- Запас восстанавливается при отмене заказа
Точки интеграции
Предоставляет другим модулям
| Интерфейс | Потребители | Назначение |
|---|---|---|
| IOrderService | MLM | Получить детали заказа для комиссии |
| IProductService | MLM | Получить значение карьерных очков продукта |
Потребляет от других модулей
| Интерфейс | Провайдер | Назначение |
|---|---|---|
| IUserLookupService | Core | Получить данные клиента |
| IPartnerLookupService | MLM | Проверить реферальную атрибуцию |
| IAttributionService | MLM | Получить реферального партнера для заказа |