ADR-020 — Warehouse автоматически обновляет stock при completion/refund заказа
Context
Warehouse-service был полностью изолирован от событий заказа: ни одного @KafkaListener, никакой реакции на закрытие/возврат. Stock balances обновлялись только вручную через Receipt Acts (приход) и Write Off Acts (черновое списание).
Это означало:
- E2e flow «заказ → списание ингредиентов» не работал —
current_quantityне падало после продажи - Refund возвращал деньги, но не возвращал ингредиенты на склад
- Любая отчётность по себестоимости/потерям была невалидной
Симптомы поймали в e2e-тестах сквозных функциональностей 2026-05-05 (см. план e2e тесты сквозных функциональностей).
Decision
Warehouse-service подписывается на 2 события из order-service:
-
order.completed→ авто-списание (AUTO-WRITEOFF-{order_number}-{ts})- По каждому
order_itemsищемTechCard.findByProductIdAndModifierOptionIdIsNull(product_id) - Суммируем
recipe_item.net_weight × order_item.quantityпоingredient_id - Создаём
WriteOffActсоstatus="posted", в одной транзакции сStockBalance.current_quantity -= deltaиrecalculateAverageCost
- По каждому
-
order.refundedсis_full_refund=true→ restock (AUTO-RESTOCK-{order_number}-{ts})- Используем тот же calculator, инверсию знака (positive delta)
- Партиальные возвраты пока не поддерживаются (требуют маппинг
refund_cartна recipe — отдельная задача)
Order-service обогащён: Completed/Refunded payloads теперь содержат items: [{product_id, product_name, quantity}]. Это минимальный snapshot без ссылок на storage-сервис.
Implementation
| Component | Path | Changes |
|---|---|---|
| Order DTO | erp-order-service/.../event/OrderEventPayloads.java | + nested Item, поле items в Completed и Refunded |
| Order Publisher | erp-order-service/.../event/OrderEventPublisher.java | helper toEventItems(order); publishCompleted всегда заполняет, publishRefunded — только если isFullRefund |
| Warehouse Kafka error handler | erp-warehouse-service/.../config/KafkaConfig.java | DLT + safeRecoverer (копия order-service.KafkaConfig) |
| Warehouse consumer | erp-warehouse-service/.../event/OrderEventConsumer.java | 2 @KafkaListener методы (consumer-groups: warehouse-service-order-completed, warehouse-service-order-refunded); парсит JsonNode (StringDeserializer на consumer level) |
| Warehouse сервис | erp-warehouse-service/.../service/OrderInventoryService.java | deductForCompletedOrder/restockForRefundedOrder — atomically создаёт WriteOffAct со status=“posted” + lines + обновляет StockBalance |
| Warehouse YAML | erp-warehouse-service/src/main/resources/application.yml | spring.kafka.consumer + spring.kafka.listener.missing-topics-fatal=false |
Consequences
Положительные
- Stock_balances реально отражают остатки после продаж
WriteOffActзаписи дают аудит всех auto-операций (видны в админке)- Refund восстанавливает склад симметрично
- Average cost пересчитывается после каждой операции
Известные ограничения (отложено)
-
Modifier-tech-cards игнорируются.
OrderItem.modifiers(jsonb) хранит толькоgroup_name+option_name+price, безmodifier_option_id. Чтобы списывать модификаторы, нужно расширитьModifierEntryUUID-ссылкой наmodifier_options.id. Затронет: order-service entity + pos-bff body validation + frontend (POS) order submit. -
Partial refund. Pos-bff передаёт
refund_cartсо списком возвращаемых items. Сейчас warehouse возвращает stock только дляis_full_refund=true. Для partial — нужен маппингrefund_cart.product_id → order_items.product_idи пропорциональный delta (например в одной шаурме вернули половину → 1/2 ingredient_qty). -
No insufficient-stock guard. Если
current_quantity < delta, мы делаем отрицательный balance. По хорошему — лог warning + не падать (продажа уже произошла, склад просто не сошёлся). Для production — добавить алерт.
Отрицательные
- Дополнительная зависимость warehouse → order через kafka (но direction OK по слоистой архитектуре)
- Eventual consistency: между POST /complete и обновлением stock проходит несколько мс (kafka lag); если кто-то делает GET /stock-balances сразу — увидит стейл
Verification
E2e прогон 2026-05-05 на Арбате:
- order 001 (Шаурма Классическая M + Кола): после complete — Курица филе -0.150, Лепёшка -1, Лук -0.030, Помидор -0.050, Капуста -0.040, Соус чесночный -30мл (тех.карта применена точно)
- order 003 (2× Шаурма Говядина + 2× Кола): complete → -0.300 кг говядины, refund → +0.300 кг (восстановлено)
- WriteOffActs созданы со статусами
posted, reason содержит order_number → трассировка с заказами
Ссылки
- Демо-стенд
- Events
- Order-service event publish:
OrderEventPublisher.publishCompleted/publishRefunded