BR 2.5 — Order Service

Источники

Задачи

Миграция БД

  • Liquibase changeset XXX-br-2-5-status-model:

    • Расширить CHECK-constraint (миграция 005 уже включает accepted/handed_over, но нужно добавить in_delivery, delivered):
      ALTER TABLE orders DROP CONSTRAINT IF EXISTS chk_status;
      ALTER TABLE orders ADD CONSTRAINT chk_status
        CHECK (status IN ('new', 'accepted', 'ready', 'handed_over',
                          'in_delivery', 'delivered', 'closed', 'cancelled'));
    • ALTER TABLE orders ADD COLUMN requires_kitchen BOOLEAN NOT NULL DEFAULT false
    • ALTER TABLE orders ADD COLUMN accepted_by UUID NULL
    • ALTER TABLE orders ADD COLUMN courier_id UUID NULL
    • ALTER TABLE orders ADD COLUMN accepted_at TIMESTAMP NULL
    • ALTER TABLE orders ADD COLUMN ready_at TIMESTAMP NULL
    • ALTER TABLE orders ADD COLUMN handed_over_at TIMESTAMP NULL
    • ALTER TABLE orders ADD COLUMN in_delivery_at TIMESTAMP NULL
    • ALTER TABLE orders ADD COLUMN delivered_at TIMESTAMP NULL
  • Data-migration для legacy: UPDATE orders SET paid_at = completed_at WHERE status = 'closed' AND paid_at IS NULL (опционально — обсудить)

Entity

  • Order.java — добавить поля requiresKitchen, acceptedBy, courierId, acceptedAt, readyAt, handedOverAt, inDeliveryAt, deliveredAt

State Machine

  • OrderStateMachine — явная Map allowed transitions:
    Map<String, Set<String>> = Map.of(
      "new",         Set.of("accepted", "cancelled", "closed"),
      "accepted",    Set.of("ready", "cancelled"),
      "ready",       Set.of("handed_over", "cancelled", "closed"),
      "handed_over", Set.of("in_delivery"),
      "in_delivery", Set.of("delivered"),
      "delivered",   Set.of("closed"),
      "closed",      Set.of(),
      "cancelled",   Set.of()
    );

Service

  • OrderService.startCooking(id, user) — new → accepted, проставить accepted_at/by, emit order.cooking_started
  • OrderService.markReady(id, user) — accepted → ready, emit order.ready
  • OrderService.pay(id, req, user)не меняет статус, только paid_at, emit order.paid
  • OrderService.close(id, user) — → closed, инвариант paid_at IS NOT NULL (для новых переходов), emit order.closed + order.completed (alias)
  • OrderService.cancel(id, reason, user) — только если paid_at IS NULL, иначе ORDER_ALREADY_PAID
  • OrderService.checkout(id, req, user) — shortcut pay+close для takeaway без requires_kitchen. Иначе 409 ORDER_FLOW_MISMATCH. Для обратной совместимости: если зовут на dine_in/delivery — внутренне делать pay+close и логировать deprecation warning (вместо break).
  • OrderService.handOverToCourier(id, courierId, user) — ready → handed_over, проверить что courier_id имеет permission orders.delivery
  • OrderService.startDelivery(id, user) — handed_over → in_delivery, проверить permission orders.delivery + совпадение courier_id с user
  • OrderService.confirmDelivery(id, user) — in_delivery → delivered, те же permission-проверки
  • OrderService.createOrder(...) — при сохранении рассчитать requires_kitchen по составу cart (запрос в Catalog Service или join через products таблицу Catalog — API call)

Controllers

  • OrderController — добавить endpoints:
    • POST /orders/{id}/start-cooking
    • POST /orders/{id}/mark-ready
    • POST /orders/{id}/hand-over-to-courier (body: { courier_id })
    • POST /orders/{id}/start-delivery
    • POST /orders/{id}/confirm-delivery
  • Обновить POST /orders/{id}/pay — убрать order.status = ready
  • Обновить POST /orders/{id}/complete — добавить инвариант paid_at IS NOT NULL
  • Обновить POST /orders/{id}/checkout (если был) — добавить проверку типа

Events

  • OrderEventPublisher — новые методы:
    • publishCookingStarted(Order)
    • publishReady(Order)
    • publishClosed(Order) (+ продолжать publishCompleted как alias)
    • publishHandedOver(Order)
    • publishInDelivery(Order)
    • publishDelivered(Order)
  • OrderEventPayloads — новые DTO-классы CookingStarted, Ready, Closed, HandedOver, InDelivery, Delivered

Permissions

  • orders.delivery — новая permission (добавить в PermissionCatalog Auth Service — отдельная sub-миграция в Auth Service)
  • Endpoints start-delivery / confirm-delivery требуют orders.delivery (или orders.edit как надпрокси)

Calculation of orders.requires_kitchen

  • CatalogServiceClient.areAnyItemsRequireKitchen(productIds) — HTTP-запрос в Catalog Service GET /internal/products/require-kitchen?ids=... (новый internal endpoint в Catalog Service — выделить подзадачу там)
  • Кэшировать результат на checkout’е в orders.requires_kitchen

Verification

  1. mvn compile зелёный
  2. Миграции применяются без потери данных
  3. Smoke-сценарии:
    • Takeaway без кухни: POST /orders + POST /orders/{id}/checkout → закрывается одним вызовом, status=closed
    • Dine-in с кухней: full flow через start-cooking → mark-ready → pay → close
    • Delivery: full flow через hand-over-to-courier → start-delivery → confirm-delivery → close
    • POST /orders/{id}/complete без paid_at → 409 ORDER_NOT_PAID
    • POST /orders/{id}/cancel на оплаченном заказе → 422 ORDER_ALREADY_PAID
    • Все новые события приходят в Kafka с правильным payload