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

openapi: 3.1.0
info:
  title: Tracium Stock Admin API
  version: 1.0.0
  summary: Admin endpoints for StockRule CRUD + preview
  description: |
    Authorization — Bearer JWT carrying `stock_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 `stock.events.v1`
    outbox row in a single pgx.Tx. Snapshot reload is eventual:
    STOCK_SNAPSHOT_POLL_INTERVAL controls the cadence (default 10s).

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

security:
  - bearerAuth: []

tags:
  - name: stock-rules
    description: Manual StockRule CRUD
  - name: stock-preview
    description: Dry-run rule application without persisting

paths:
  /stock-rules:
    get:
      tags: [stock-rules]
      operationId: listStockRules
      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: [visibility_filter, threshold, display_transform, synthetic_fallback] }
      responses:
        '200':
          description: ok
          content: { application/json: { schema: { $ref: '#/components/schemas/RuleList' } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [stock-rules]
      operationId: createStockRule
      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' } } }
  /stock-rules/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [stock-rules]
      operationId: patchStockRule
      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: [stock-rules]
      operationId: deprecateStockRule
      responses:
        '204': { description: deprecated }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /stock-rules/preview:
    post:
      tags: [stock-preview]
      operationId: previewStockRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                candidate_rule: { $ref: '#/components/schemas/CreateRuleRequest' }
                warehouses:
                  type: array
                  items: { $ref: '#/components/schemas/PerWarehouse' }
                stock_context: { $ref: '#/components/schemas/StockContext' }
      responses:
        '200':
          description: preview
          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:
    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] }
    VisibilityFilter:
      type: object
      properties:
        warehouse_kinds: { type: array, items: { type: string } }
        warehouse_codes: { type: array, items: { type: string } }
        max_lead_time_days: { type: integer, minimum: 0 }
        mode: { type: string, enum: [hide, exclude_from_total] }
    Threshold:
      type: object
      required: [min_qty, action]
      properties:
        min_qty: { type: integer, format: int64 }
        action: { type: string, enum: [reject, partial] }
        max_lead_time_days: { type: integer, minimum: 0 }
    DisplayTransform:
      type: object
      required: [mode]
      properties:
        mode: { type: string, enum: [round_down, bucket, hide_exact] }
        round_down: { type: integer, format: int64 }
        buckets:
          type: array
          items:
            type: object
            properties:
              lower_inclusive: { type: integer, format: int64 }
              upper_exclusive: { type: integer, format: int64 }
              label: { type: string }
    SyntheticFallback:
      type: object
      required: [mode]
      properties:
        mode: { type: string, enum: [on_request, synthetic_qty] }
        synthetic_qty: { type: integer, format: int64 }
        synthetic_lead_days: { type: integer, minimum: 0 }
        reason: { type: string }
    Transformation:
      type: object
      required: [kind]
      properties:
        kind: { type: string, enum: [visibility_filter, threshold, display_transform, synthetic_fallback] }
        visibility_filter: { $ref: '#/components/schemas/VisibilityFilter' }
        threshold: { $ref: '#/components/schemas/Threshold' }
        display_transform: { $ref: '#/components/schemas/DisplayTransform' }
        synthetic_fallback: { $ref: '#/components/schemas/SyntheticFallback' }
    ValidityWindow:
      type: object
      properties:
        valid_from: { oneOf: [{ type: string, format: date-time }, { type: 'null' }] }
        valid_to:   { oneOf: [{ type: string, format: date-time }, { type: 'null' }] }
    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 }
    RuleList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/RuleDTO' }
    PerWarehouse:
      type: object
      required: [warehouse_kind, warehouse_code, qty]
      properties:
        warehouse_kind: { type: string }
        warehouse_code: { type: string }
        qty: { type: integer, format: int64 }
        lead_time_days: { type: integer, minimum: 0 }
    StockContext:
      type: object
      required: [customer_id, supplier_ref]
      properties:
        customer_id: { type: string, format: uuid }
        supplier_ref: { type: string }
        customer_type: { type: string, enum: ['', retail, wholesale, gov] }
        quantity: { type: integer, format: int64 }
    PreviewResponse:
      type: object
      properties:
        available_qty: { type: integer, format: int64 }
        warehouses:
          type: array
          items: { $ref: '#/components/schemas/PerWarehouse' }
        breakdown: { type: array, items: { type: object } }
        display_label: { type: string, nullable: true }
        synthetic_lead_days: { type: integer, nullable: true }
        overlap_warnings: { type: array, items: { type: object } }
    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 }
