# Tracium Pricing Admin API — AS-SHIPPED contract for /api/v1/admin/pricing/price-rules/*
#
# Standalone OpenAPI 3.1 spec for the pricing BC admin surface:
#   - PriceRule CRUD (POST/GET/PATCH/DELETE)
#   - Preview (dry-run rule application against virtual snapshot)
#   - Drafts review (accept/reject/replace derived suggestions)
#   - Derivation runs (list + manual trigger)
#
# Implementation: backend/internal/core/pricing/infra/http/*.go
# fx wiring: backend/internal/core/pricing/fx.go
# Domain: backend/internal/core/pricing/domain/
# ADR: docs/docs/20-architecture/adr/0036-pricing-rule-aggregate-and-auto-derivation.md
#
# Path layout aligns with admin-api.openapi.yaml: server includes the
# /v1/admin/pricing prefix, paths are relative. The openapilint tool walks
# both this spec and infra/http handlers; drift produces a non-zero exit.

openapi: 3.1.0
info:
  title: Tracium Pricing Admin API
  version: 1.0.0
  summary: Admin endpoints for PriceRule CRUD + preview + drafts review + derivation runs
  description: |
    Authorization — Bearer JWT carrying `pricing_admin` role; the
    `RequireAdminRole` middleware (auth.go) enforces the role and
    returns 403 otherwise.

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

    Errors follow the platform envelope: `{ "error": { "code", "message",
    "details" } }`. See `pricing-admin-error` schema below.
  contact:
    name: Tracium Pricing Team

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

security:
  - bearerAuth: []

tags:
  - name: pricing-rules
    description: Manual + auto-derived PriceRule CRUD
  - name: pricing-preview
    description: Dry-run rule application without persisting
  - name: pricing-drafts
    description: Low-confidence derived patterns awaiting review
  - name: pricing-derivation
    description: Auto-derivation run history + manual trigger
  - name: pricing-handlers
    description: Custom pricing handler plugin registry (P3)
  - name: pricing-preferred-sellers
    description: PreferredSellerPolicy CRUD (P5b)

paths:
  /price-rules:
    get:
      tags: [pricing-rules]
      summary: List PriceRules with optional filters
      operationId: listPriceRules
      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: source
          in: query
          schema: { type: string, enum: [manual, derived_auto] }
        - name: transform_kind
          in: query
          schema: { type: string, enum: [percentage, fixed, tiers] }
      responses:
        '200':
          description: Filtered list (only enabled rules)
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/RuleDTO' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [pricing-rules]
      summary: Create a new manual PriceRule
      operationId: createPriceRule
      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 conflict — another enabled rule covers the same scope+priority+kind+applies_to+window
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OverlapConflictError' }
  /price-rules/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [pricing-rules]
      summary: Patch a PriceRule (partial update)
      operationId: patchPriceRule
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PatchRuleRequest' }
      responses:
        '200':
          description: Updated rule
          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: Patched fields create overlap with another rule
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OverlapConflictError' }
    delete:
      tags: [pricing-rules]
      summary: Soft-delete (deprecate) a PriceRule
      operationId: deprecatePriceRule
      responses:
        '204':
          description: Deprecated (enabled=false, deprecated_at set, disabled_by_operator=true)
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /price-rules/preview:
    post:
      tags: [pricing-preview]
      summary: Dry-run a candidate rule against the current snapshot
      operationId: previewPriceRule
      description: |
        Builds a virtual snapshot (current ∪ candidate) and applies the
        Engine to compute the resulting price. Does NOT persist the
        candidate. Returns the breakdown plus any overlap warnings.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pricing_context]
              properties:
                candidate_rule:
                  $ref: '#/components/schemas/CreateRuleRequest'
                existing_rule_id:
                  type: string
                  format: uuid
                  description: Preview an existing rule without adding to virtual snapshot
                pricing_context:
                  $ref: '#/components/schemas/PricingContext'
      responses:
        '200':
          description: Preview result
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreviewResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /price-rules/drafts:
    get:
      tags: [pricing-drafts]
      summary: List pending derivation drafts (low-confidence patterns)
      operationId: listDrafts
      parameters:
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Pending drafts (suggested_at ascending)
          content:
            application/json:
              schema:
                type: object
                required: [items, next_cursor]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/DraftDTO' }
                  next_cursor: { type: string }
        '403': { $ref: '#/components/responses/Forbidden' }

  /price-rules/drafts/{id}/accept:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [pricing-drafts]
      summary: Accept a draft as a manual PriceRule
      operationId: acceptDraft
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                priority:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 32767
                  default: 100
      responses:
        '201':
          description: Manual rule created from draft, draft marked accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RuleDTO' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: Draft already reviewed or new manual rule conflicts
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PricingError' }

  /price-rules/drafts/{id}/reject:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [pricing-drafts]
      summary: Reject a draft (no rule created)
      operationId: rejectDraft
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
      responses:
        '204': { description: Draft marked rejected }
        '403': { $ref: '#/components/responses/Forbidden' }

  /price-rules/drafts/{id}/replace:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [pricing-drafts]
      summary: Mark draft as replaced by an existing manual rule
      operationId: replaceDraft
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [manual_rule_id]
              properties:
                manual_rule_id: { type: string, format: uuid }
      responses:
        '204': { description: Draft marked replaced }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /price-rules/derivation-runs:
    get:
      tags: [pricing-derivation]
      summary: List derivation run history
      operationId: listDerivationRuns
      parameters:
        - name: supplier_ref
          in: query
          schema: { type: string }
        - name: canonical_id
          in: query
          schema: { type: string, format: uuid }
        - name: status
          in: query
          schema: { type: string, enum: [running, ok, skipped, failed] }
        - name: trigger
          in: query
          schema: { type: string, enum: [event, weekly_cron, manual_api] }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Run history (started_at descending)
          content:
            application/json:
              schema:
                type: object
                required: [items, next_cursor]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/DerivationRunDTO' }
                  next_cursor: { type: string }
        '403': { $ref: '#/components/responses/Forbidden' }

  /price-rules/derivation-runs/trigger:
    post:
      tags: [pricing-derivation]
      summary: Manually trigger a derivation run for a (supplier, canonical) pair
      operationId: triggerDerivationRun
      description: |
        Returns 503 when PRICING_DERIVATION_ENABLED=false; the engine
        does not have a Deriver wired in that mode.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [supplier_ref, canonical_id]
              properties:
                supplier_ref: { type: string, examples: [etm] }
                canonical_id: { type: string, format: uuid }
      responses:
        '202':
          description: Run scheduled
          content:
            application/json:
              schema:
                type: object
                required: [run_id]
                properties:
                  run_id: { type: string, format: uuid }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '503':
          description: Derivation disabled (PRICING_DERIVATION_ENABLED=false)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PricingError' }

  /handlers:
    get:
      tags: [pricing-handlers]
      summary: List built-in pricing handlers (compile-time inventory)
      operationId: listPricingHandlers
      responses:
        '200':
          description: Inventory list
          content:
            application/json:
              schema:
                type: object
                required: [handlers]
                properties:
                  handlers:
                    type: array
                    items: { $ref: '#/components/schemas/HandlerDescriptor' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /handler-registrations:
    get:
      tags: [pricing-handlers]
      operationId: listHandlerRegistrations
      parameters:
        - name: scope_kind
          in: query
          schema: { type: string }
        - name: handler_ref
          in: query
          schema: { type: string }
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/HandlerRegistrationDTO' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [pricing-handlers]
      operationId: createHandlerRegistration
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateHandlerRegistrationRequest' }
      responses:
        '201':
          description: created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HandlerRegistrationDTO' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: duplicate fingerprint
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PricingError' }

  /handler-registrations/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [pricing-handlers]
      operationId: patchHandlerRegistration
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PatchHandlerRegistrationRequest' }
      responses:
        '200':
          description: updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HandlerRegistrationDTO' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [pricing-handlers]
      operationId: deprecateHandlerRegistration
      responses:
        '204': { description: deprecated }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /preferred-seller-policies:
    get:
      tags: [pricing-preferred-sellers]
      operationId: listPreferredSellerPolicies
      summary: List PreferredSellerPolicies with optional filters
      parameters:
        - name: scope_kind
          in: query
          schema: { type: string, enum: [customer, customer_pricing_group] }
        - name: scope_ref
          in: query
          schema: { type: string, format: uuid }
        - name: status
          in: query
          schema: { type: string, enum: [active, inactive] }
        - name: active_at
          in: query
          schema: { type: string, format: date-time }
        - name: limit
          in: query
          schema: { type: integer, default: 100, minimum: 1, maximum: 500 }
        - name: offset
          in: query
          schema: { type: integer, default: 0, minimum: 0 }
      responses:
        '200':
          description: Filtered list
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/PreferredSellerPolicyDTO' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [pricing-preferred-sellers]
      operationId: createPreferredSellerPolicy
      summary: Create a PreferredSellerPolicy
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreatePreferredSellerPolicyRequest' }
      responses:
        '201':
          description: Created
          headers:
            Location:
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreferredSellerPolicyDTO' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /preferred-seller-policies/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [pricing-preferred-sellers]
      operationId: getPreferredSellerPolicy
      summary: Get a PreferredSellerPolicy by ID
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreferredSellerPolicyDTO' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [pricing-preferred-sellers]
      operationId: updatePreferredSellerPolicy
      summary: Update a PreferredSellerPolicy (CAS via If-Match header)
      parameters:
        - name: If-Match
          in: header
          required: true
          description: Current version (CAS sentinel)
          schema: { type: integer, minimum: 1 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdatePreferredSellerPolicyRequest' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreferredSellerPolicyDTO' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Version mismatch — reload the record and retry
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PricingError' }
    delete:
      tags: [pricing-preferred-sellers]
      operationId: deletePreferredSellerPolicy
      summary: Delete a PreferredSellerPolicy
      responses:
        '204': { description: Deleted }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /preferred-seller-policies/{id}/activate:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [pricing-preferred-sellers]
      operationId: activatePreferredSellerPolicy
      summary: Activate a policy (inactive → active) via CAS
      parameters:
        - name: If-Match
          in: header
          required: true
          description: Current version (CAS sentinel)
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: Activated — returns full PolicyDTO with bumped version
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreferredSellerPolicyDTO' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Version mismatch
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PricingError' }

  /preferred-seller-policies/{id}/deactivate:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [pricing-preferred-sellers]
      operationId: deactivatePreferredSellerPolicy
      summary: Deactivate a policy (active → inactive) via CAS
      parameters:
        - name: If-Match
          in: header
          required: true
          description: Current version (CAS sentinel)
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: Deactivated — returns full PolicyDTO with bumped version
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreferredSellerPolicyDTO' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Version mismatch
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PricingError' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  responses:
    BadRequest:
      description: Validation error or malformed payload
      content:
        application/json:
          schema: { $ref: '#/components/schemas/PricingError' }
    Forbidden:
      description: JWT lacks `pricing_admin` role
      content:
        application/json:
          schema: { $ref: '#/components/schemas/PricingError' }
    NotFound:
      description: Rule not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/PricingError' }

  schemas:
    PricingError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, examples: [validation_error, rule_overlap, rule_not_found, derivation_disabled] }
            message: { type: string }
            details: { type: object, additionalProperties: true }

    OverlapConflictError:
      allOf:
        - $ref: '#/components/schemas/PricingError'
        - type: object
          properties:
            error:
              type: object
              properties:
                details:
                  type: object
                  properties:
                    conflicting_rule_ids:
                      type: array
                      items: { type: string, format: uuid }

    Scope:
      type: object
      required: [kind, ref]
      properties:
        kind: { type: string, enum: [supplier, customer_pricing_group, customer, classification_tag] }
        ref: { type: string, description: "UUID for customer/group/tag; supplier-string for supplier" }

    Region:
      type: object
      required: [id, kind]
      properties:
        id: { type: string, format: uuid }
        kind: { type: string, examples: [country, region, city] }

    AppliesTo:
      type: object
      properties:
        qty_min: { type: integer, format: int64, minimum: 0 }
        customer_type: { type: string, enum: ['', retail, wholesale, gov], description: "Empty string = any" }
        region: { $ref: '#/components/schemas/Region', nullable: true }

    Percentage:
      type: object
      required: [value]
      properties:
        value:
          type: number
          format: double
          description: "Proportional, e.g. -0.08 = 8% discount; range [-1.0, 10.0]"
          minimum: -1.0
          maximum: 10.0

    FixedAmount:
      type: object
      required: [value, precision, currency, mode]
      properties:
        value: { type: string, description: "Decimal-string of minor units" }
        precision: { type: integer, format: int32, minimum: 0, maximum: 12 }
        currency: { type: string, examples: [RUB, USD, EUR] }
        mode: { type: string, enum: [add, subtract, replace] }

    Tier:
      type: object
      required: [qty_from, multiplier]
      properties:
        qty_from: { type: integer, format: int64, minimum: 0 }
        qty_to:
          oneOf:
            - { type: integer, format: int64 }
            - { type: 'null' }
          description: "null = open upper bound (only allowed on last tier)"
        multiplier: { type: number, format: double, minimum: 0 }

    Transformation:
      type: object
      required: [kind]
      properties:
        kind: { type: string, enum: [percentage, fixed, tiers] }
        percentage: { $ref: '#/components/schemas/Percentage' }
        fixed: { $ref: '#/components/schemas/FixedAmount' }
        tiers:
          type: array
          items: { $ref: '#/components/schemas/Tier' }
      description: "Exactly one of percentage / fixed / tiers must be set, matching the kind discriminator."

    ValidityWindow:
      type: object
      properties:
        valid_from:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        valid_to:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
      description: "Inclusive [valid_from, valid_to). Null = open bound."

    SourceMetadata:
      type: object
      description: "Set only for source=derived_auto rules"
      properties:
        derivation_run_id: { type: string, format: uuid }
        confidence: { type: number, format: double, minimum: 0, maximum: 1 }
        baseline_credential_ref: { type: string, format: uuid }
        supporting_observation_count: { type: integer, format: int32, minimum: 0 }
        generated_at: { type: string, format: date-time }

    RuleDTO:
      type: object
      required: [id, scope, applies_to, transformation, priority, validity_window, enabled, disabled_by_operator, 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
          minimum: 0
          maximum: 32767
          description: "Default 100 for manual rules, 50 for derived"
        validity_window: { $ref: '#/components/schemas/ValidityWindow' }
        enabled: { type: boolean }
        disabled_by_operator: { type: boolean }
        source: { type: string, enum: [manual, derived_auto] }
        source_metadata:
          oneOf: [{ $ref: '#/components/schemas/SourceMetadata' }, { type: 'null' }]
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        created_by:
          oneOf: [{ type: string, format: uuid }, { 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
          description: "Defaults to 100 (manual default)"
        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, minimum: 0, maximum: 32767 }
        validity_window: { $ref: '#/components/schemas/ValidityWindow' }
        enabled:
          type: boolean
          description: "false → triggers Deprecate (sets deprecated_at + disabled_by_operator)"

    PricingContext:
      type: object
      required: [customer_id, supplier_ref, base_price]
      properties:
        customer_id: { type: string, format: uuid }
        customer_pricing_group_id: { type: string, format: uuid }
        customer_type: { type: string, enum: ['', retail, wholesale, gov] }
        supplier_ref: { type: string }
        canonical_id: { type: string, format: uuid }
        quantity: { type: integer, format: int64, minimum: 0 }
        base_price: { $ref: '#/components/schemas/FixedAmount' }

    BreakdownItem:
      type: object
      required: [label, result, delta]
      properties:
        rule_id: { type: string, format: uuid }
        label: { type: string }
        delta: { $ref: '#/components/schemas/FixedAmount' }
        result: { $ref: '#/components/schemas/FixedAmount' }
        source: { type: string, enum: [manual, derived_auto] }

    PreviewResponse:
      type: object
      required: [would_apply, final_price, breakdown, applied_rules, overlap_warnings]
      properties:
        would_apply: { type: boolean }
        final_price: { $ref: '#/components/schemas/FixedAmount' }
        breakdown:
          type: array
          items: { $ref: '#/components/schemas/BreakdownItem' }
        applied_rules:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              label: { type: string }
              source: { type: string }
        overlap_warnings:
          type: array
          items:
            type: object
            properties:
              with_rule_id: { type: string, format: uuid }
              reason: { type: string }

    DraftDTO:
      type: object
      required: [id, scope, applies_to, transformation, priority, confidence, derivation_run_id, supporting_observation_count, suggested_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 }
        confidence: { type: number, format: double, minimum: 0, maximum: 1 }
        derivation_run_id: { type: string, format: uuid }
        supporting_observation_count: { type: integer, format: int32, minimum: 0 }
        suggested_at: { type: string, format: date-time }

    DerivationRunDTO:
      type: object
      required: [id, trigger, started_at, status, derived_count, draft_count]
      properties:
        id: { type: string, format: uuid }
        trigger: { type: string, enum: [event, weekly_cron, manual_api] }
        supplier_ref: { type: string }
        canonical_id: { type: string, format: uuid }
        started_at: { type: string, format: date-time }
        finished_at: { type: string, format: date-time }
        status: { type: string, enum: [running, ok, skipped, failed] }
        skip_reason:
          type: string
          enum: [insufficient_observations, no_baseline, no_pattern, derivation_confidence_dropped]
        derived_count: { type: integer, format: int32, minimum: 0 }
        draft_count: { type: integer, format: int32, minimum: 0 }

    HandlerDescriptor:
      type: object
      required: [id, label, version]
      properties:
        id: { type: string, examples: [volume_discount_v1, contract_markup_v1] }
        label: { type: string }
        description: { type: string }
        version: { type: integer, format: int32 }

    TriggerPredicate:
      type: object
      additionalProperties: true
      description: |
        Equality predicate evaluated against HandlerInvocationContext.
        Allowed keys: supplier, customer_type, customer_id, canonical_id,
        observation.has_volume_discount, observation.on_request.

    HandlerRegistrationDTO:
      type: object
      required: [id, handler_ref, scope, applies_to, trigger, priority, validity_window, enabled, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        handler_ref: { type: string }
        scope: { $ref: '#/components/schemas/Scope' }
        applies_to: { $ref: '#/components/schemas/AppliesTo' }
        trigger: { $ref: '#/components/schemas/TriggerPredicate' }
        priority: { type: integer, format: int32 }
        validity_window: { $ref: '#/components/schemas/ValidityWindow' }
        enabled: { type: boolean }
        disabled_by_operator: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    CreateHandlerRegistrationRequest:
      type: object
      required: [handler_ref, scope, trigger]
      properties:
        handler_ref: { type: string }
        scope: { $ref: '#/components/schemas/Scope' }
        applies_to: { $ref: '#/components/schemas/AppliesTo' }
        trigger: { $ref: '#/components/schemas/TriggerPredicate' }
        priority: { type: integer, format: int32, minimum: 0, maximum: 32767 }
        validity_window: { $ref: '#/components/schemas/ValidityWindow' }

    PatchHandlerRegistrationRequest:
      type: object
      properties:
        applies_to: { $ref: '#/components/schemas/AppliesTo' }
        trigger: { $ref: '#/components/schemas/TriggerPredicate' }
        priority: { type: integer, format: int32 }
        validity_window: { $ref: '#/components/schemas/ValidityWindow' }
        enabled: { type: boolean }

    # ----- Preferred Seller Policy (P5b) -------------------------------------
    PreferredSellerPolicyDTO:
      type: object
      required: [id, scope_kind, scope_ref, priority, preferred_sellers, blocked_sellers, status, created_at, updated_at, version]
      properties:
        id: { type: string, format: uuid }
        scope_kind:
          type: string
          enum: [customer, customer_pricing_group]
        scope_ref: { type: string, format: uuid }
        priority: { type: integer, minimum: 0 }
        valid_from:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        valid_to:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        preferred_sellers:
          type: array
          items: { type: string, format: uuid }
        blocked_sellers:
          type: array
          items: { type: string, format: uuid }
        status: { type: string, enum: [active, inactive] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        version: { type: integer, minimum: 1 }

    CreatePreferredSellerPolicyRequest:
      type: object
      required: [scope_kind, scope_ref, priority, preferred_sellers, blocked_sellers]
      properties:
        scope_kind: { type: string, enum: [customer, customer_pricing_group] }
        scope_ref: { type: string, format: uuid }
        priority: { type: integer, minimum: 0, default: 0 }
        valid_from:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        valid_to:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        preferred_sellers:
          type: array
          items: { type: string, format: uuid }
        blocked_sellers:
          type: array
          items: { type: string, format: uuid }

    UpdatePreferredSellerPolicyRequest:
      type: object
      required: [priority, preferred_sellers, blocked_sellers]
      description: "scope_kind + scope_ref are immutable; send only mutable fields"
      properties:
        priority: { type: integer, minimum: 0 }
        valid_from:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        valid_to:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        preferred_sellers:
          type: array
          items: { type: string, format: uuid }
        blocked_sellers:
          type: array
          items: { type: string, format: uuid }
