Тема
Модель e-commerce: ТП, склады, цены, налоги, скидки
Статус: проектирование объёма (доменная модель модулей магазина; реализация — фаза 3 по заказу, но модель фиксируется сейчас — ретрофит складов/типов цен в живой магазин болезнен). Детализирует раздел E каталога модулей до уровня сущностей. Референсы: Битрикс (типы цен, склады, торговые предложения), Lunar (модель variants/pricing — путь C), донор
masha(склад/остатки),notal(платежи/эскроу).
Принцип: одна модель, включаемая слоями
Магазинные модули наращивают одну доменную модель, а не свои параллельные: ТП → склады → цены → скидки → налоги/чеки. Каждый слой — отдельный модуль (включается по потребности), но схемы спроектированы совместно — здесь.
Торговые предложения (ТП / варианты / SKU)
Продаём «футболку» — а на складе лежат «футболка красная M» и «футболка синяя L».
Product(товар-«зонт»: описание, категория, SEO) →Variant(ТП): комбинация значений опций (цвет × размер);- опции (
option: цвет, размер, объём) и их значения — декларируются через движок полей на типе товара; не хардкод; - у ТП свои: SKU, штрихкод, вес/габариты, цены (см. ниже), остатки по складам (см. ниже), опционально свои медиа (фото красной футболки);
- товар без вариаций = один implicit-вариант. Корзина/заказ/склад всегда работают с
variant_id— единая модель безif ($hasVariants)по всему коду; - листинг показывает товар; карточка — выбор ТП (переключатели опций, живой остаток/цена);
- фасетный фильтр агрегирует свойства ТП («есть размер L в наличии»);
- SEO: канонический URL — у товара; ТП не плодят индексируемых страниц (выбор на карточке). Осознанные посадочные под опцию («красные футболки») — это умный SEO-фильтр, не URL ТП.
Битрикс-аналогия: инфоблок торговых предложений. Lunar: product variants — модель заимствуем оттуда (уже Filament-native).
Склады и остатки (multi-warehouse)
commerce_warehouses id, name, type ∈ {own, store, supplier, virtual},
priority, city_id (nullable — стык мультигорода), is_active
commerce_stock variant_id × warehouse_id → qty, reserved
commerce_stock_moves журнал движений: variant, warehouse, delta, reason (order/supply/correction), ref- доступность к продаже = Σ
qty - reservedпо складам, видимым текущему контексту (город/канал продаж). «На складе А есть, на Б нет» — витрина показывает наличие выбранного города/склада + статус «под заказ, N дней» с других складов (правило — конфиг модуля, не хардкод); - резервирование при оформлении заказа (
reserved+), списание при отгрузке, снятие резерва при отмене/таймауте — только через журнал движений (баланс всегда восстановим); - стратегия отгрузки: приоритет складов; сплит заказа по складам — отложенная возможность (модель позволяет: позиции заказа несут
warehouse_id); - виртуальный склад поставщика — остатки из фида/1С, «под заказ»;
- у склада может быть владелец (
vendor_id nullable) — заготовка мультивендора (см. ниже).
Цены (типы цен, группы, лояльность)
Цена — не колонка товара, а запись «ТП × тип цены»:
commerce_price_types id, code (retail, wholesale, old, promo, price1, price2, …),
title, currency, visibility (jsonb) # кому виден тип
commerce_prices variant_id × price_type_id (× city/region nullable) → value,
qty_from (nullable — оптовые пороги)- видимость типа цены — правило: guest / роль или группа пользователя (spatie/permission) / уровень лояльности / B2B-договор. Розница видит
retail, опт —wholesale, партнёр —price1; - резолвер цены: контекст (пользователь → группы/уровень, город, канал, количество) → упорядоченная цепочка типов → итоговая цена + перечёркнутая
old(маркетинговая «старая цена» — отдельный тип, а не история); - опт от количества:
qty_from— тир-прайсинг внутри типа; - скидка ≠ тип цены: скидки применяются поверх резолвера (ниже);
- кеш: цена в page-cache допустима только гостевая (retail); страница для авторизованных групп — фрагмент цены островом/сегментированный ключ по группе — иначе оптовая цена утечёт в общий кеш (производительность);
- региональные цены —
city_idна записи цены (стык мультигорода).
НДС и налоги
- справочник
TaxRate(0 / 10 / 20 / без НДС) — ставка на товаре/услуге/ТП (переопределение ТП допустимо); - розничные цены хранятся с НДС (РФ-практика); B2B-документы выделяют НДС из цены;
- режим юрлица продавца (ОСН/УСН) — конфиг; УСН → «без НДС» в чеках;
- позиция заказа снапшотит цену, ставку НДС и признак предмета расчёта на момент оформления — последующие правки каталога не меняют историю.
Чеки (54-ФЗ) — уточнение
- чек привязан к платежу и позициям заказа (снапшот); типы: предоплата / полный расчёт / возврат (чек возврата формируется при рефанде);
- каждая позиция несёт: ставку НДС, признак предмета расчёта (товар / услуга / цифровой товар), признак способа расчёта;
- оплата бонусами уменьшает цену позиций (пересчёт по позициям), а не проводится «иным способом» — иначе фискализация некорректна;
- шлюзы с собственной фискализацией (ЮKassa-чеки) — capability коннектора; без неё — модуль ОФД (АТОЛ) поверх PaymentGateway-контракта;
- мультивендор: раздельные чеки по продавцам (сплит) — кабинеты.
Цифровые товары
- ТП с
fulfilment: digital— нет склада и доставки; «остаток» = ∞ либо пул лицензий; - выдача после оплаты (событие
PaymentSucceeded→ job, идемпотентно): подписанная ссылка на файл (TTL + лимит скачиваний, приватный диск), лицензионный ключ из пула, или доступ в кабинете; - чек: признак «цифровой товар/услуга», корректная ставка НДС;
- политика возврата цифровых — конфиг (по умолчанию невозвратны после выдачи).
Продажа услуг
- услуга = ТП с
fulfilment: service— без склада/доставки; чек с признаком «услуга»; - в одном заказе смешиваются товары и услуги (монтаж + материалы);
- бронирование слота (календарь мастера/зала) — отдельный модуль поверх;
- мост с модулем «Услуги (иерархия)»: карточка услуги artel-модели может и собирать заявку (лид — по умолчанию), и продаваться онлайн (кнопка «оплатить» → заказ) — включается модулем магазина, не переделкой услуг.
Бонусы и скидки (rule engine)
Скидки — декларативные правила «условие → действие», данные, а не код (Битрикс-аналогия: «правила работы с корзиной»):
condition: { cart_total ≥ N | group = X | has_coupon | category in [...] | qty ≥ N | first_order }
action: { percent | fixed | cheapest_free | free_shipping }
priority, stop_further (bool), active_from/to- правила комбинируются по приоритету;
stop_furtherобрывает цепочку; - промокод = правило с условием «введён код» + лимиты (использований, на пользователя);
- аудит применения: заказ хранит, какие правила сработали и на сколько изменили цену.
Бонусы (лояльность) — транзакционный счёт:
bonus_transactions: начисление/списание/сгорание, баланс = Σ (никогда не колонка);- правила начисления — тот же rule-engine: % от заказа, фикс за действие (регистрация, отзыв), множители акций; начисление по событию
OrderCompleted(после выкупа, не оплаты) — идемпотентный job; - списание — лимит % от чека (конфиг), пересчёт по позициям (см. чеки);
- возврат/отмена → сторно транзакций (не удаление);
- уровни лояльности ← накопленная сумма покупок; уровень открывает тип цены и/или повышенный % бонусов (стык с видимостью типов цен).
Заготовка мультивендора (UGC-маркетплейс)
Будущий модуль «пользователи размещают товары/объявления и получают заявки» (кабинеты/UGC) использует эти же модели — поэтому уже сейчас:
vendor_id (nullable)на товаре, складе, типе цены — владелец-продавец;null= магазин;- заказ с товарами разных продавцов → сплит-подзаказы; комиссия — поверх, не внутри цены;
- заявка «по объявлению» — это лид ядра с привязкой к товару/публикации (модель
cms_leadsуже полиморфна) — UGC-«получать заявки» не требует новой подсистемы.
Ретрофит vendor_id в живую схему дороже, чем nullable-колонка с первого дня.
Highload-требования каталога (миллионы товаров)
Каталог обязан держать миллионы товаров/ТП без деградации и полностью утилизировать многоядерный сервер с большим ОЗУ. Требования — контрактные (нарушение = падение теста), не «пожелания»:
Чтение (витрина):
- листинги/карточки — только keyset-пагинация (
cursorPaginate);OFFSETна глубине — запрещён (на миллионах строк это seq-scan хвоста); - фасетные агрегации и полнотекст — из поискового индекса (Meilisearch/ES через
cms/search), неCOUNT(*)/GROUP BYпо живым таблицам; PG-fallback (tsvector) допустим только до ~100k товаров; - горячие пути (листинг, карточка, фасеты) — query builder с явным
selectнужных колонок, без гидрации лишних Eloquent-моделей и без N+1 (contract-тест); - деривативы для листинга (минимальная цена ТП, суммарный остаток, флаг «в наличии») — денормализованные проекции, пересчитываемые событиями через очередь, а не подзапросы на лету;
- кеш: карточка/листинг в Redis + page-cache; stampede-защита обязательна (performance).
Схема PG (16):
- составные/покрывающие индексы под реальные WHERE/ORDER BY листингов; GIN — по JSONB атрибутов; BRIN — по
created_atжурнальных таблиц (stock_moves); - крупные таблицы (
prices= variant×price_type×city,stock,stock_moves) — проектировать сразу под декларативное партиционирование (по складу/диапазону дат); ANALYZE/autovacuum-тюнинг для каталожных таблиц — часть провижининга; большое ОЗУ →shared_buffers/effective_cache_sizeпо правилам инфры студии (не более 25% WSL-лимита).
Запись (импорт/ETL):
- массовый импорт —
COPY/батчевый upsert чанками черезBus::batch(параллельно по ядрам), не по-строчный Eloquentsave(); - на время ETL —
max_parallel_workers_per_gather=1(правило инфры против OOM), после — вернуть; индексы при первичной загрузке — после данных; - инвалидация кеша при импорте — батчевая по тегам разделов, не по-товарная лавина.
Многоядерность/ОЗУ: пулы Horizon per-queue (импорт/индексация/медиа — отдельные очереди со своими воркерами), FPM static-pool по ядрам, поисковая индексация — фоном чанками; read-replica поддерживается конфигурацией Laravel (read/write connections) без изменений кода модулей.
Разбивка на модули (уточнение каталога)
| Модуль | Содержимое | Зависит от |
|---|---|---|
commerce-catalog | Product + Variant (ТП) + опции | ядро (движок полей), Медиа |
commerce-stock | склады, остатки, резервы, движения | commerce-catalog |
commerce-pricing | типы цен, видимость по группам/лояльности, тир-прайсинг | commerce-catalog |
commerce-promo | rule-engine скидок, промокоды | Корзина |
loyalty (раздел D каталога) | бонусный счёт, правила начисления, уровни | Заказы |
commerce-digital | цифровые ТП, выдача файлов/лицензий | commerce-catalog, Платежи |
commerce-services | продажа услуг, мост с модулем «Услуги» | commerce-catalog |
commerce-tax | ставки НДС, режимы юрлица | Заказы |
Витрина без покупки (universal-модель «каталог + лид») использует только commerce-catalog (+ опц. pricing) — граница «лид ≠ заказ» сохраняется.
Связи
- Каталог модулей, раздел E — слои и зрелость.
- Интеграции — платёжные шлюзы, 1С (склады/цены приезжают обменом).
- Кабинеты/UGC — мультивендор, эскроу, сплит.
- Производительность — сегментация кеша цен.
- Мультиязычность, SEO/мультигород — региональные цены/наличие.