# Tracium Delivery Admin API — AS-SHIPPED contract for /api/v1/admin/delivery/delivery-rules/*
#
# Standalone OpenAPI 3.1 spec for the delivery BC admin surface:
#   - DeliveryRule CRUD (POST/GET/PATCH/DELETE)
#   - Preview (dry-run rule application against virtual snapshot + delivery context)
#
# Implementation: backend/internal/core/delivery/infra/http/*.go
# fx wiring: backend/internal/core/delivery/fx.go (Phase G)
# Domain: backend/internal/core/delivery/domain/
# ADR: docs/docs/20-architecture/adr/0038-delivery-rule-aggregate.md (Phase H)
#
# Path layout aligns with stock-admin.yaml and pricing-admin.yaml: server
# includes the /v1/admin/delivery prefix, paths are relative.

openapi: 3.1.0
info:
  title: Tracium Delivery Admin API
  version: 1.0.0
  summary: Admin endpoints for DeliveryRule CRUD + preview
  description: |
    Authorization — Bearer JWT carrying `delivery_admin` role; the
    `RequireAdminRole` middleware enforces and returns 403 otherwise.

    Mutations are wrapped in transactional outbox: each Create / Update /
    Deprecate writes the rule row and the matching `delivery.events.v1`
    outbox row in a single pgx.Tx. Snapshot reload is eventual:
    DELIVERY_SNAPSHOT_POLL_INTERVAL controls the cadence (default 10s).

servers:
  - url: https://admin.tracium.ru/v1/admin/delivery
    description: Production
  - url: https://admin.staging.tracium.ru/v1/admin/delivery
    description: Staging
  - url: http://localhost:8080/v1/admin/delivery
    description: Local dev

security:
  - bearerAuth: []

tags:
  - name: delivery-rules
    description: Manual DeliveryRule CRUD
  - name: delivery-preview
    description: Dry-run rule application without persisting

paths:
  /delivery-rules:
    get:
      tags: [delivery-rules]
      operationId: listDeliveryRules
      parameters:
        - name: scope_kind
          in: query
          schema: { type: string, enum: [supplier, customer_pricing_group, customer, classification_tag] }
        - name: scope_ref
          in: query
          schema: { type: string }
        - name: transform_kind
          in: query
          schema: { type: string, enum: [carrier_filter, lead_time_override, logistics_markup, free_delivery_threshold] }
      responses:
        '200':
          description: ok
          content: { application/json: { schema: { $ref: '#/components/schemas/RuleList' } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [delivery-rules]
      operationId: createDeliveryRule
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreateRuleRequest' } } }
      responses:
        '201':
          description: created
          content: { application/json: { schema: { $ref: '#/components/schemas/RuleDTO' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: overlap
          content: { application/json: { schema: { $ref: '#/components/schemas/OverlapConflict' } } }
  /delivery-rules/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [delivery-rules]
      operationId: patchDeliveryRule
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/PatchRuleRequest' } } }
      responses:
        '200':
          description: updated
          content: { application/json: { schema: { $ref: '#/components/schemas/RuleDTO' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: overlap
          content: { application/json: { schema: { $ref: '#/components/schemas/OverlapConflict' } } }
    delete:
      tags: [delivery-rules]
      operationId: deprecateDeliveryRule
      responses:
        '204': { description: deprecated }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /delivery-rules/preview:
    post:
      tags: [delivery-preview]
      operationId: previewDeliveryRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                candidate_rule: { $ref: '#/components/schemas/CreateRuleRequest' }
                delivery_context: { $ref: '#/components/schemas/DeliveryContext' }
                scope: { $ref: '#/components/schemas/PreviewScope' }
      responses:
        '200':
          description: preview result
          content: { application/json: { schema: { $ref: '#/components/schemas/PreviewResponse' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  responses:
    BadRequest:
      description: validation error
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: forbidden
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: not found
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  schemas:
    FixedAmount:
      type: object
      required: [value, precision, currency]
      properties:
        value: { type: string, description: "Raw integer in smallest unit (e.g. kopeks for RUB/precision=2)" }
        precision: { type: integer, format: int32, minimum: 0, maximum: 255 }
        currency: { type: string, minLength: 3, maxLength: 3, description: "ISO-4217 alpha-3 uppercase" }
    Scope:
      type: object
      required: [kind, ref]
      properties:
        kind: { type: string, enum: [supplier, customer_pricing_group, customer, classification_tag] }
        ref: { type: string }
    AppliesTo:
      type: object
      properties:
        qty_min: { type: integer, format: int64 }
        customer_type: { type: string, enum: ['', retail, wholesale, gov] }
        region:
          type: object
          properties:
            id: { type: string, format: uuid }
            kind: { type: string }
    ValidityWindow:
      type: object
      properties:
        valid_from: { oneOf: [{ type: string, format: date-time }, { type: 'null' }] }
        valid_to:   { oneOf: [{ type: string, format: date-time }, { type: 'null' }] }
    CarrierFilter:
      type: object
      description: Allow or block carriers by name. Exactly one of allowlist/blocklist must be non-empty.
      properties:
        allowlist:
          type: array
          items: { type: string }
          description: Only these carriers are permitted; all others are blocked.
        blocklist:
          type: array
          items: { type: string }
          description: These carriers are explicitly blocked; all others permitted.
    LeadTimeOverride:
      type: object
      required: [mode]
      description: |
        Overrides lead time on matched deliveries. Mode controls which fields are used:
        - absolute: set absolute min/max days (absolute_min_days + absolute_max_days required)
        - add_days: shift both bounds by add_days (may be negative)
        - multiply: scale both bounds by multiplier (must be > 0)
      properties:
        mode: { type: string, enum: [absolute, add_days, multiply] }
        absolute_min_days: { type: integer, minimum: 0 }
        absolute_max_days: { type: integer, minimum: 0 }
        add_days: { type: integer }
        multiplier: { type: number, exclusiveMinimum: 0 }
    LogisticsMarkup:
      type: object
      required: [mode]
      description: |
        Adds a delivery cost. Mode controls the amount field used:
        - flat: fixed cost per order (flat_amount required)
        - per_warehouse: cost multiplied by warehouse_count from ApplyContext (per_warehouse required)
        - per_kg: deferred in P2c (no upstream weight data)
      properties:
        mode: { type: string, enum: [flat, per_warehouse, per_kg] }
        flat_amount: { $ref: '#/components/schemas/FixedAmount' }
        per_warehouse: { $ref: '#/components/schemas/FixedAmount' }
        per_kg: { $ref: '#/components/schemas/FixedAmount' }
    FreeDeliveryThreshold:
      type: object
      required: [min_order_value]
      description: Zeroes out delivery cost when base_price * qty_requested >= min_order_value.
      properties:
        min_order_value: { $ref: '#/components/schemas/FixedAmount' }
    Transformation:
      type: object
      required: [kind]
      description: |
        Discriminated union — set exactly the field matching `kind`.
      properties:
        kind: { type: string, enum: [carrier_filter, lead_time_override, logistics_markup, free_delivery_threshold] }
        carrier_filter: { $ref: '#/components/schemas/CarrierFilter' }
        lead_time_override: { $ref: '#/components/schemas/LeadTimeOverride' }
        logistics_markup: { $ref: '#/components/schemas/LogisticsMarkup' }
        free_delivery_threshold: { $ref: '#/components/schemas/FreeDeliveryThreshold' }
    CreateRuleRequest:
      type: object
      required: [scope, transformation]
      properties:
        scope: { $ref: '#/components/schemas/Scope' }
        applies_to: { $ref: '#/components/schemas/AppliesTo' }
        transformation: { $ref: '#/components/schemas/Transformation' }
        priority: { type: integer, format: int32, minimum: 0, maximum: 32767 }
        validity_window: { $ref: '#/components/schemas/ValidityWindow' }
    PatchRuleRequest:
      type: object
      properties:
        applies_to: { $ref: '#/components/schemas/AppliesTo' }
        transformation: { $ref: '#/components/schemas/Transformation' }
        priority: { type: integer, format: int32 }
        validity_window: { $ref: '#/components/schemas/ValidityWindow' }
        enabled: { type: boolean }
    RuleDTO:
      type: object
      required: [id, scope, applies_to, transformation, priority, validity_window, enabled, source, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        scope: { $ref: '#/components/schemas/Scope' }
        applies_to: { $ref: '#/components/schemas/AppliesTo' }
        transformation: { $ref: '#/components/schemas/Transformation' }
        priority: { type: integer, format: int32 }
        validity_window: { $ref: '#/components/schemas/ValidityWindow' }
        enabled: { type: boolean }
        disabled_by_operator: { type: boolean }
        source: { type: string, enum: [manual] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        created_by: { type: string, format: uuid }
    RuleList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/RuleDTO' }
    DeliveryContext:
      type: object
      description: Delivery baseline + apply signals for preview.
      properties:
        carrier: { type: string, description: "Carrier name as known to the engine" }
        lead_time_min_sec: { type: integer, format: int64, description: "Baseline lead time min in seconds" }
        lead_time_max_sec: { type: integer, format: int64, description: "Baseline lead time max in seconds" }
        base_price: { $ref: '#/components/schemas/FixedAmount' }
        qty_requested: { type: integer, format: int64 }
        warehouse_count: { type: integer, minimum: 0 }
    PreviewScope:
      type: object
      required: [supplier_ref, customer_id]
      properties:
        supplier_ref: { type: string }
        customer_id: { type: string, format: uuid }
        customer_type: { type: string, enum: ['', retail, wholesale, gov] }
    PreviewResponse:
      type: object
      properties:
        carrier: { type: string }
        lead_time_min_sec: { type: integer, format: int64 }
        lead_time_max_sec: { type: integer, format: int64 }
        cost:
          oneOf:
            - { $ref: '#/components/schemas/FixedAmount' }
            - { type: 'null' }
        rejected: { type: boolean }
        reject_reason: { type: string }
        breakdown:
          type: array
          items:
            type: object
            properties:
              rule_id: { type: string, format: uuid }
              label: { type: string }
              action: { type: string }
        overlap_warnings:
          type: array
          items: { $ref: '#/components/schemas/OverlapWarning' }
    OverlapWarning:
      type: object
      properties:
        with_rule_id: { type: string, format: uuid }
        reason: { type: string }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: object, additionalProperties: true }
    OverlapConflict:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            error:
              type: object
              properties:
                details:
                  type: object
                  properties:
                    conflicting_rule_ids:
                      type: array
                      items: { type: string, format: uuid }
