openapi: 3.1.0

info:
  title: Aklamio Events API V3 — Sales
  version: 0.1.0-draft
  description: |
    API contract for the V3 Events API — Sales event type.

    This specification covers create, list, update, confirm, and reject operations
    on sale resources identified by UUID v4 (`sales_entities.uid`).

    **Authentication**: API-key (`Api-Secret`) — brand-scoped.
    **Localization**: Error messages are localized via the `Accept-Language` request header; default is English (`en`).
  contact:
    name: Aklamio Engineering
    email: facundo.diazarce@aklamio.com

servers:
  - url: https://api.aklamio.com
    description: Production

tags:
  - name: Sales
    description: Operations on sale resources

security:
  - ApiSecret: []

paths:
  # ──────────────────────────────────────────────
  # GET  /v3/events/sales
  # ──────────────────────────────────────────────
  /v3/events/sales:
    post:
      operationId: createSale
      summary: Create a sale
      description: |
        Creates a sale in either direct or tracking mode.
        - `mode=direct`: creates a `sales_request` synchronously and returns `201`.
          When `supportRequestId` is provided, promotion and request identity come from the
          support ticket; `aid`, `coupon`, and email fields are ignored. The ticket must belong
          to the same promotion as `promotionUid`.
        - `mode=tracking`: ingests tracking payload asynchronously and returns `200 {}`.
      tags: [Sales]
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/XRequestIdRequest'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSaleRequest'
            examples:
              direct:
                summary: Direct creation
                value:
                  mode: direct
                  promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                  orderId: "ORD-2026-002"
                  trackedAt: "2026-03-18T14:00:00Z"
                  followerEmail: "buyer@example.com"
                  recommenderEmail: "ref@example.com"
                  followerLocale: "de"
                  recommenderLocale: "en"
                  lineItems:
                    - sku: "SKU-001"
                      name: "Premium Widget"
                      category: "electronics"
                      price: 9999
                  salesChannel: "online"
                  isTest: false
              directSupportRequest:
                summary: Direct creation via support request
                value:
                  mode: direct
                  promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                  supportRequestId: "345"
                  orderId: "ORD-2026-SR-001"
                  trackedAt: "2026-03-18T14:00:00Z"
                  lineItems:
                    - sku: "SKU-001"
                      name: "Premium Widget"
                      category: "electronics"
                      price: 9999
                  salesChannel: "online"
                  isTest: false
              tracking:
                summary: Tracking ingestion
                value:
                  mode: tracking
                  promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                  orderId: "ORD-2026-003"
                  trackedAt: "2026-03-18T14:00:00Z"
                  followerLocale: "de"
                  recommenderLocale: "en"
                  lineItems:
                    - sku: "SKU-001"
                      name: "Premium Widget"
                      category: "electronics"
                      price: 9999
                  tracking:
                    mode: coupon
                    coupon: "SPRING-2026-ABCD"
                    email: "buyer@example.com"
                  salesChannel: "online"
                  isTest: false
      responses:
        '201':
          description: Sale created (direct mode)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SaleSingleResponse'
        '200':
          description: Tracking event accepted (tracking mode)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrackingAcceptedResponse'
              example: {}
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/AuthenticationRequired'
        '422':
          description: Business rule violation
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                noRewardsCreated:
                  summary: Sale closed due to missing rewards
                  value:
                    error:
                      code: "business_rule_violation"
                      message: "Sale has been closed because rewards could not be created"
                      requestId: "550e8400-e29b-41d4-a716-446655440003"
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      operationId: listSales
      summary: List sales
      description: |
        Returns a paginated list of sales scoped to a promotion.
        By default includes both matched sales and unmatched event-only sales
        (`status = unmatched`, `hasSalesRequest = false`).
        Use `hasSalesRequest=true` to exclude unmatched entries.
      tags: [Sales]
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/XRequestIdRequest'
        - $ref: '#/components/parameters/PromotionUidRequired'
        - $ref: '#/components/parameters/StatusFilter'
        - $ref: '#/components/parameters/HasSalesRequestFilter'
        - $ref: '#/components/parameters/AccountManagerUidFilter'
        - $ref: '#/components/parameters/OrderIdFilter'
        - $ref: '#/components/parameters/SalesChannelFilter'
        - $ref: '#/components/parameters/EventSourceFilter'
        - $ref: '#/components/parameters/RequestTypeFilter'
        - $ref: '#/components/parameters/IsTestFilter'
        - $ref: '#/components/parameters/CreatedAtFrom'
        - $ref: '#/components/parameters/CreatedAtTo'
        - $ref: '#/components/parameters/Sort'
        - $ref: '#/components/parameters/PageToken'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Paginated list of sales
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SaleListResponse'
              examples:
                success:
                  summary: Paginated list with one sale
                  value:
                    data:
                      - id: "4f91cc9f-6e9f-4d72-b9f7-3f4e2a1d4f90"
                        orderId: "ORD-2026-001"
                        status: "open"
                        hasSalesRequest: true
                        promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                        accountManagerUid: null
                        requestType: "referral"
                        salesChannel: "online"
                        matchingCriterion: "aid"
                        eventSource: "trackingjs"
                        isTest: false
                        followerEmail: "joh***@***mple.com"
                        recommenderEmail: "jan***@***mple.com"
                        trackedAt: "2026-03-15T10:30:00Z"
                        createdAt: "2026-03-15T10:30:00Z"
                        updatedAt: "2026-03-15T10:30:00Z"
                        lineItems:
                          - sku: "SKU-001"
                            name: "Premium Widget"
                            category: "electronics"
                            price: 9999
                        hasFile: true
                        additionalFields: null
                        eventMetadata:
                          info: "aklamio"
                    meta:
                      page:
                        nextToken: "eyJzb3J0RmllbGQiOiJjcmVhdGVkQXQiLCJzb3J0RGlyZWN0aW9uIjoiZGVzYyIsInNvcnRWYWx1ZSI6IjIwMjYtMDMtMTVUMTA6MzA6MDBaIiwiaWQiOjEyMn0="
                        pageSize: 25
                emptyList:
                  summary: No sales match filters
                  value:
                    data: []
                    meta:
                      page:
                        nextToken: null
                        pageSize: 25
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/AuthenticationRequired'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ──────────────────────────────────────────────
  # GET / PATCH  /v3/events/sales/{saleId}
  # ──────────────────────────────────────────────
  /v3/events/sales/{saleId}:
    get:
      operationId: getSale
      summary: Get a single sale
      description: |
        Returns a single sale by UID (`saleId`).
        Requires `promotionUid` query parameter for authorization context.
      tags: [Sales]
      parameters:
        - $ref: '#/components/parameters/SaleIdPath'
        - $ref: '#/components/parameters/PromotionUidRequired'
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/XRequestIdRequest'
      responses:
        '200':
          description: Sale detail
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SaleSingleResponse'
              examples:
                saleDetail:
                  summary: Open sale with file attached
                  value:
                    data:
                      id: "4f91cc9f-6e9f-4d72-b9f7-3f4e2a1d4f90"
                      orderId: "ORD-2026-001"
                      status: "open"
                      hasSalesRequest: true
                      promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                      accountManagerUid: null
                      requestType: "referral"
                      salesChannel: "online"
                      matchingCriterion: "manual"
                      eventSource: null
                      isTest: false
                      followerEmail: "joh***@***mple.com"
                      recommenderEmail: "jan***@***mple.com"
                      trackedAt: "2026-03-15T10:30:00Z"
                      createdAt: "2026-03-15T10:30:00Z"
                      updatedAt: "2026-03-15T10:30:00Z"
                      lineItems:
                        - sku: "SKU-001"
                          name: "Premium Widget"
                          category: "electronics"
                          price: 9999
                      file:
                        downloadUrl: "https://s3.eu-central-1.amazonaws.com/aklamio-receipts/4f91cc9f.pdf?X-Amz-Expires=3600&..."
                        downloadUrlExpiresAt: "2026-03-15T11:30:00Z"
                      additionalFields:
                        AgentID: "1953"
                      eventMetadata: null
                unmatchedSale:
                  summary: Unmatched event-only sale (no sales request)
                  value:
                    data:
                      id: "8ac72e28-7a6b-49f5-8a95-c909a7c1b8ae"
                      orderId: "ORD-2026-099"
                      status: "unmatched"
                      hasSalesRequest: false
                      promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                      accountManagerUid: null
                      requestType: null
                      salesChannel: "online"
                      matchingCriterion: null
                      eventSource: "api"
                      isTest: false
                      followerEmail: "Anonymous"
                      recommenderEmail: "Anonymous"
                      trackedAt: "2026-03-20T08:00:00Z"
                      createdAt: "2026-03-20T08:00:00Z"
                      updatedAt: "2026-03-20T08:00:00Z"
                      lineItems:
                        - sku: "SKU-050"
                          name: "Standard Widget"
                          category: "electronics"
                          price: 2999
                      file: null
                      additionalFields: null
                      eventMetadata:
                        info: "aklamio"
        '401':
          $ref: '#/components/responses/AuthenticationRequired'
        '404':
          $ref: '#/components/responses/ResourceNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

    patch:
      operationId: updateSale
      summary: Update a sale
      description: |
        Replaces the line items on an existing sale.
        Internally the current sales request is rejected and a new one is created
        with the provided line items. The `sales_entities` row keeps the same UID.
        Only sales with a linked `sales_request` in `open`, `rejected`, or `closed` state can be updated.
        Requires `promotionUid` query parameter for authorization context.
      tags: [Sales]
      parameters:
        - $ref: '#/components/parameters/SaleIdPath'
        - $ref: '#/components/parameters/PromotionUidRequired'
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/XRequestIdRequest'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSaleRequest'
            examples:
              replaceLineItems:
                summary: Replace line items
                value:
                  lineItems:
                    - sku: "SKU-001"
                      name: "Premium Widget v2"
                      category: "electronics"
                      price: 8999
      responses:
        '200':
          description: Sale updated successfully
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SaleSingleResponse'
              examples:
                updated:
                  summary: Updated sale resource
                  value:
                    data:
                      id: "11f8e2f5-9f04-41a5-ac3f-49f9f939f7fb"
                      orderId: "ORD-2026-002"
                      status: "open"
                      hasSalesRequest: true
                      promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                      accountManagerUid: null
                      requestType: "referral"
                      salesChannel: "online"
                      matchingCriterion: "manual"
                      eventSource: null
                      isTest: false
                      followerEmail: "joh***@***mple.com"
                      recommenderEmail: "jan***@***mple.com"
                      trackedAt: "2026-03-19T11:00:00Z"
                      createdAt: "2026-03-19T11:00:00Z"
                      updatedAt: "2026-03-19T11:00:00Z"
                      lineItems:
                        - sku: "SKU-001"
                          name: "Premium Widget v2"
                          category: "electronics"
                          price: 8999
                      additionalFields:
                        AgentID: "1953"
                      eventMetadata: null
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/AuthenticationRequired'
        '404':
          $ref: '#/components/responses/ResourceNotFound'
        '422':
          description: Business rule violation
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                wrongState:
                  summary: Sale not in updatable state
                  value:
                    error:
                      code: "business_rule_violation"
                      message: "Only open, rejected or closed sales can be updated"
                      requestId: "550e8400-e29b-41d4-a716-446655440000"
                noSalesRequest:
                  summary: Event-only sale (unmatched)
                  value:
                    error:
                      code: "business_rule_violation"
                      message: "Sale cannot be updated because no sales request exists for this saleId"
                      requestId: "550e8400-e29b-41d4-a716-446655440001"
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ──────────────────────────────────────────────
  # POST  /v3/events/sales/confirm
  # ──────────────────────────────────────────────
  /v3/events/sales/confirm:
    post:
      operationId: confirmSales
      summary: Confirm sales
      description: |
        Confirms one or more sales by UID. Supports partial success —
        individual failures do not roll back successful confirmations.

        A balance check is performed before confirmation.
        If the advertiser's account balance is insufficient the entire batch is rejected.

        The response returns only failed items in `failures`;
        successfully processed UIDs are represented only in `summary.succeeded`.
      tags: [Sales]
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/XRequestIdRequest'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchActionRequest'
            examples:
              confirmTwo:
                summary: Confirm two sales
                value:
                  promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                  ids:
                    - "4f91cc9f-6e9f-4d72-b9f7-3f4e2a1d4f90"
                    - "56f4dd7b-3e0b-4791-b9bb-6207e064fbb1"
              confirmByOrderIds:
                summary: Confirm by order IDs
                value:
                  promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                  orderIds:
                    - "ORD-2026-001"
                    - "ORD-2026-002"
      responses:
        '200':
          description: Batch result with failures and summary
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchActionResponse'
              examples:
                partialFailure:
                  summary: One confirmed, one failed
                  value:
                    data:
                      failures:
                        - id: "56f4dd7b-3e0b-4791-b9bb-6207e064fbb1"
                          error:
                            code: "business_rule_violation"
                            message: "Sale is not in an open state"
                      summary:
                        total: 2
                        succeeded: 1
                        failed: 1
                allSucceeded:
                  summary: All confirmed successfully
                  value:
                    data:
                      failures: []
                      summary:
                        total: 2
                        succeeded: 2
                        failed: 0
                unmatchedFailure:
                  summary: Unmatched event-only sale failure
                  value:
                    data:
                      failures:
                        - id: "8ac72e28-7a6b-49f5-8a95-c909a7c1b8ae"
                          error:
                            code: "business_rule_violation"
                            message: "Sale cannot be confirmed because no sales request exists for this saleId"
                      summary:
                        total: 1
                        succeeded: 0
                        failed: 1
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/AuthenticationRequired'
        '422':
          description: Business rule violation (e.g. insufficient balance)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                insufficientBalance:
                  summary: Insufficient account balance
                  value:
                    error:
                      code: "business_rule_violation"
                      message: "Insufficient account balance to confirm these sales"
                      requestId: "550e8400-e29b-41d4-a716-446655440010"
                      details:
                        requiredAmount: 15000
                        availableBalance: 5000
                        currency: "EUR"
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ──────────────────────────────────────────────
  # POST  /v3/events/sales/reject
  # ──────────────────────────────────────────────
  /v3/events/sales/reject:
    post:
      operationId: rejectSales
      summary: Reject sales
      description: |
        Rejects one or more sales by UID. Supports partial success.

        The response returns only failed items in `failures`;
        successfully processed UIDs are represented only in `summary.succeeded`.
      tags: [Sales]
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/XRequestIdRequest'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchActionRequest'
            examples:
              rejectTwo:
                summary: Reject two sales
                value:
                  promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                  ids:
                    - "4f91cc9f-6e9f-4d72-b9f7-3f4e2a1d4f90"
                    - "56f4dd7b-3e0b-4791-b9bb-6207e064fbb1"
              rejectByOrderIds:
                summary: Reject by order IDs
                value:
                  promotionUid: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"
                  orderIds:
                    - "ORD-2026-001"
                    - "ORD-2026-002"
      responses:
        '200':
          description: Batch result with failures and summary
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Content-Language:
              $ref: '#/components/headers/ContentLanguage'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchActionResponse'
              examples:
                partialFailure:
                  summary: One rejected, one failed
                  value:
                    data:
                      failures:
                        - id: "56f4dd7b-3e0b-4791-b9bb-6207e064fbb1"
                          error:
                            code: "business_rule_violation"
                            message: "Sale is not in an open state"
                      summary:
                        total: 2
                        succeeded: 1
                        failed: 1
                allSucceeded:
                  summary: All rejected successfully
                  value:
                    data:
                      failures: []
                      summary:
                        total: 2
                        succeeded: 2
                        failed: 0
                unmatchedFailure:
                  summary: Unmatched event-only sale failure
                  value:
                    data:
                      failures:
                        - id: "8ac72e28-7a6b-49f5-8a95-c909a7c1b8ae"
                          error:
                            code: "business_rule_violation"
                            message: "Sale cannot be rejected because no sales request exists for this saleId"
                      summary:
                        total: 1
                        succeeded: 0
                        failed: 1
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/AuthenticationRequired'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

# ════════════════════════════════════════════════
# COMPONENTS
# ════════════════════════════════════════════════
components:

  # ── Security ──────────────────────────────────
  securitySchemes:
    ApiSecret:
      type: apiKey
      in: header
      name: Api-Secret
      description: Brand-scoped API key provisioned by Aklamio.

  # ── Headers ───────────────────────────────────
  headers:
    XRequestId:
      description: Correlation ID for the request. Matches `requestId` in error bodies.
      schema:
        type: string
        format: uuid
        example: "550e8400-e29b-41d4-a716-446655440000"
    ContentLanguage:
      description: Effective locale used for the response (e.g. `en`, `de`).
      schema:
        type: string
        example: "en"
    RateLimitLimit:
      description: Requests allowed in the current window.
      schema:
        type: integer
        example: 1000
    RateLimitRemaining:
      description: Remaining requests in the current window.
      schema:
        type: integer
        example: 997
    RateLimitReset:
      description: Unix timestamp when the current window resets.
      schema:
        type: integer
        example: 1711972800
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        example: 60

  # ── Parameters ────────────────────────────────
  parameters:
    AcceptLanguage:
      name: Accept-Language
      in: header
      required: false
      description: Preferred locale for human-readable error messages. Default `en`.
      schema:
        type: string
        default: en
        example: en

    XRequestIdRequest:
      name: X-Request-Id
      in: header
      required: false
      description: Client-provided correlation ID. If absent or invalid, the server generates one.
      schema:
        type: string
        example: "550e8400-e29b-41d4-a716-446655440000"

    SaleIdPath:
      name: saleId
      in: path
      required: true
      description: Sale UUID (`sales_entities.uid`).
      schema:
        type: string
        format: uuid
        example: "4f91cc9f-6e9f-4d72-b9f7-3f4e2a1d4f90"

    PromotionUidRequired:
      name: promotionUid
      in: query
      required: true
      description: Promotion UID to scope results to.
      schema:
        type: string
        example: "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6"

    StatusFilter:
      name: status
      in: query
      required: false
      description: "Comma-separated status filter: `unmatched,open,confirmed,rejected,closed` (backed by persisted `sales_entities.state`)."
      schema:
        type: string
        example: "open,confirmed"

    HasSalesRequestFilter:
      name: hasSalesRequest
      in: query
      required: false
      description: Filter by whether a `sales_request` exists.
      schema:
        type: boolean

    AccountManagerUidFilter:
      name: accountManagerUid
      in: query
      required: false
      description: Filter by assigned account-manager UID on the linked `sales_request`.
      schema:
        type: string
        format: uuid
        example: "6a8b3d6e-7f62-4e86-8f7f-bf7d5d81a201"

    OrderIdFilter:
      name: orderId
      in: query
      required: false
      description: Filter by external order ID.
      schema:
        type: string
        example: "ORD-2026-001"

    SalesChannelFilter:
      name: salesChannel
      in: query
      required: false
      description: "Filter by sales channel: `online`, `offline`, `hotline`."
      schema:
        type: string
        enum: [online, offline, hotline]

    EventSourceFilter:
      name: eventSource
      in: query
      required: false
      description: "Comma-separated event source filter: `api,trackingjs,claim_by_link`."
      schema:
        type: string
        example: "api,trackingjs"

    RequestTypeFilter:
      name: requestType
      in: query
      required: false
      description: "Comma-separated request type filter: `referral,cashback`."
      schema:
        type: string
        example: "referral"

    IsTestFilter:
      name: isTest
      in: query
      required: false
      description: Filter by test flag.
      schema:
        type: boolean

    CreatedAtFrom:
      name: createdAtFrom
      in: query
      required: false
      description: RFC 3339 UTC lower bound for `createdAt`.
      schema:
        type: string
        format: date-time
        example: "2026-03-01T00:00:00Z"

    CreatedAtTo:
      name: createdAtTo
      in: query
      required: false
      description: RFC 3339 UTC upper bound for `createdAt`.
      schema:
        type: string
        format: date-time
        example: "2026-03-31T23:59:59Z"

    Sort:
      name: sort
      in: query
      required: false
      description: "Sort field and direction. Format: `field:direction` (supported fields: `createdAt`, `updatedAt`, `trackedAt`, `orderId`, `status`; default: `createdAt:desc`)."
      schema:
        type: string
        default: "createdAt:desc"
        example: "createdAt:desc"

    PageToken:
      name: pageToken
      in: query
      required: false
      description: Opaque cursor returned by the previous list response.
      schema:
        type: string
        example: "eyJzb3J0RmllbGQiOiJjcmVhdGVkQXQiLCJzb3J0RGlyZWN0aW9uIjoiZGVzYyIsInNvcnRWYWx1ZSI6IjIwMjYtMDMtMTVUMTA6MzA6MDBaIiwiaWQiOjEyM30="

    PageSize:
      name: pageSize
      in: query
      required: false
      description: Items per page (default 20, max 100).
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
        example: 25

  # ── Schemas ───────────────────────────────────
  schemas:

    # ── Sale (list variant — uses hasFile) ──
    SaleListItem:
      type: object
      description: Sale resource as returned in list responses (uses `hasFile` instead of `file`).
      required:
        - id
        - orderId
        - status
        - hasSalesRequest
        - promotionUid
        - isTest
        - createdAt
        - updatedAt
        - lineItems
        - hasFile
      properties:
        id:
          type: string
          format: uuid
          description: Public sale UID (`sales_entities.uid`).
        orderId:
          type: string
          description: External order identifier.
        status:
          type: string
          enum: [unmatched, open, confirmed, rejected, closed]
          description: Current sale status persisted in `sales_entities.state`.
        hasSalesRequest:
          type: boolean
          description: Whether a `sales_request` exists for this sale.
        promotionUid:
          type: string
          description: Public promotion identifier (UID).
        accountManagerUid:
          type: [string, 'null']
          format: uuid
          description: UID of the assigned account manager; `null` when not assigned.
        requestType:
          type: [string, 'null']
          enum: [referral, cashback, null]
          description: "Type of request: `referral` or `cashback`."
        salesChannel:
          type: [string, 'null']
          enum: [online, offline, hotline, null]
          description: Sales channel. For unmatched event-only sales this is derived from `sales_event.sales_data.salesChannel` when present, otherwise `null`.
        matchingCriterion:
          type: [string, 'null']
          description: How the sale was matched (e.g. `aid`, `coupon`, `ip_and_fingerprint`, `manual`).
        eventSource:
          type: [string, 'null']
          description: "How the sale originated: `api`, `trackingjs`, `claim_by_link`. `null` when no linked event."
        isTest:
          type: boolean
          description: Whether this is a test sale.
        followerEmail:
          type: string
          description: Masked follower email or `"Anonymous"`.
        recommenderEmail:
          type: string
          description: Masked recommender email or `"Anonymous"`.
        trackedAt:
          type: string
          format: date-time
          description: RFC 3339 UTC timestamp when the sale was tracked.
        createdAt:
          type: string
          format: date-time
          description: RFC 3339 UTC timestamp when the sale was created.
        updatedAt:
          type: string
          format: date-time
          description: RFC 3339 UTC timestamp when the sale was last updated.
        lineItems:
          type: array
          items:
            $ref: '#/components/schemas/LineItem'
          description: Line items of the sale.
        hasFile:
          type: boolean
          description: Whether a receipt file is attached (list only).
        additionalFields:
          type: [object, 'null']
          additionalProperties:
            type: string
          description: Custom key-value fields. `null` when none were provided.
        eventMetadata:
          type: [object, 'null']
          additionalProperties: true
          description: Metadata from the sales event payload. `null` when not available.

    # ── Sale (detail variant — uses file object) ──
    SaleDetail:
      type: object
      description: Sale resource as returned by GET detail and PATCH responses (includes `file` object).
      required:
        - id
        - orderId
        - status
        - hasSalesRequest
        - promotionUid
        - isTest
        - createdAt
        - updatedAt
        - lineItems
      properties:
        id:
          type: string
          format: uuid
          description: Public sale UID (`sales_entities.uid`).
        orderId:
          type: string
          description: External order identifier.
        status:
          type: string
          enum: [unmatched, open, confirmed, rejected, closed]
          description: Current sale status persisted in `sales_entities.state`.
        hasSalesRequest:
          type: boolean
        promotionUid:
          type: string
        accountManagerUid:
          type: [string, 'null']
          format: uuid
          description: UID of the assigned account manager; `null` when not assigned.
        requestType:
          type: [string, 'null']
          enum: [referral, cashback, null]
        salesChannel:
          type: [string, 'null']
          enum: [online, offline, hotline, null]
        matchingCriterion:
          type: [string, 'null']
        eventSource:
          type: [string, 'null']
        isTest:
          type: boolean
        followerEmail:
          type: string
        recommenderEmail:
          type: string
        trackedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        lineItems:
          type: array
          items:
            $ref: '#/components/schemas/LineItem'
        file:
          $ref: '#/components/schemas/FileObject'
        additionalFields:
          type: [object, 'null']
          additionalProperties:
            type: string
        eventMetadata:
          type: [object, 'null']
          additionalProperties: true

    # ── Line Item ──
    LineItem:
      type: object
      description: A single line item. At least one of `price`, `name`, or `category` must be present.
      properties:
        sku:
          type: string
          description: SKU identifier.
        name:
          type: string
          description: Product name.
        category:
          type: [string, 'null']
          description: Product category.
        price:
          type: integer
          description: Price in smallest currency unit (cents).

    # ── File Object ──
    FileObject:
      type: [object, 'null']
      description: Receipt file with pre-signed download URL. `null` when no file exists.
      properties:
        downloadUrl:
          type: string
          format: uri
          description: Pre-signed S3 download URL (expires after 1 hour).
        downloadUrlExpiresAt:
          type: string
          format: date-time
          description: RFC 3339 UTC expiration timestamp.

    # ── Envelopes ──
    SaleListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SaleListItem'
        meta:
          $ref: '#/components/schemas/PaginationMeta'

    SaleSingleResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/SaleDetail'

    PaginationMeta:
      type: object
      required: [page]
      properties:
        page:
          type: object
          required: [pageSize]
          properties:
            nextToken:
              type: [string, 'null']
              description: Opaque cursor for the next page; `null` when no more pages.
            pageSize:
              type: integer
              description: Items per page.

    # ── Batch Action ──
    BatchActionRequest:
      type: object
      required: [promotionUid]
      oneOf:
        - required: [ids]
        - required: [orderIds]
      not:
        required: [ids, orderIds]
      properties:
        promotionUid:
          type: string
          description: Promotion UID these sales belong to.
        ids:
          type: array
          description: Sale identifiers to process (min 1, max 1000). Values are treated as strings.
          minItems: 1
          maxItems: 1000
          items:
            type: string
        orderIds:
          type: array
          description: Order IDs to process (min 1, max 1000). Provide either `ids` or `orderIds`.
          minItems: 1
          maxItems: 1000
          items:
            type: string

    BatchActionResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [failures, summary]
          properties:
            failures:
              type: array
              items:
                $ref: '#/components/schemas/BatchFailure'
              description: Only the items that failed.
            summary:
              $ref: '#/components/schemas/BatchSummary'

    BatchFailure:
      type: object
      required: [id, error]
      description: A single failed item in a batch response.
      properties:
        id:
          type: string
          description: Failed item identifier (`saleId` when using `ids`, `orderId` when using `orderIds`).
        error:
          $ref: '#/components/schemas/BatchItemError'

    BatchItemError:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Machine-readable error code (language-neutral).
        message:
          type: string
          description: Human-readable, localized error message.

    BatchSummary:
      type: object
      required: [total, succeeded, failed]
      description: Aggregate counts for a batch operation.
      properties:
        total:
          type: integer
          description: Total items submitted.
        succeeded:
          type: integer
          description: Items processed successfully.
        failed:
          type: integer
          description: Items that failed.

    # ── Update Request ──
    CreateSaleRequest:
      oneOf:
        - $ref: '#/components/schemas/CreateSaleDirectRequest'
        - $ref: '#/components/schemas/CreateSaleTrackingRequest'

    CreateSaleDirectRequest:
      type: object
      required: [mode, promotionUid, orderId, trackedAt, lineItems]
      properties:
        mode:
          type: string
          enum: [direct]
        promotionUid:
          type: string
        orderId:
          type: string
        trackedAt:
          type: string
          format: date-time
        followerEmail:
          type: string
          description: Required in manual mode, and for coupon Share/Give. Not validated when `supportRequestId` is provided.
        recommenderEmail:
          type: string
          description: Required in manual mode when neither `aid` nor `coupon` is provided. Not validated when `supportRequestId` is provided.
        followerLocale:
          type: string
          enum: [en, de, es, fr, it, nl, pl, pt]
          description: Optional locale for the follower or cashback user. Existing non-anonymous users are updated when provided.
        recommenderLocale:
          type: string
          enum: [en, de, es, fr, it, nl, pl, pt]
          description: Optional locale for the recommender user. Existing non-anonymous users are updated when provided.
        aid:
          type: string
          description: Optional tracking token. Ignored when `supportRequestId` is provided.
        coupon:
          type: string
          description: Coupon identifier. If coupon resolves to Keep, followerEmail is optional. Keep eligibility follows matcher semantics (`reward_receivers` includes `mittlr`, or fixed-mode with recommender-only rewards). Ignored when `supportRequestId` is provided.
        checkoutLocation:
          type: string
        supportRequestId:
          type: string
          description: |
            Mittlr support request ID. When set, identity fields (`aid`, `coupon`, `followerEmail`,
            `recommenderEmail`) are ignored; promotion and request are taken from the ticket.
            The ticket must belong to the same promotion as `promotionUid`.
        lineItems:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/LineItem'
        salesChannel:
          type: string
          enum: [online, offline, hotline]
        isTest:
          type: boolean
        additionalFields:
          type: object
          additionalProperties: true

    CreateSaleTrackingRequest:
      type: object
      required: [mode, promotionUid, orderId, trackedAt, lineItems, tracking]
      properties:
        mode:
          type: string
          enum: [tracking]
        promotionUid:
          type: string
        orderId:
          type: string
        trackedAt:
          type: string
          format: date-time
        followerLocale:
          type: string
          enum: [en, de, es, fr, it, nl, pl, pt]
          description: Optional locale for the follower or cashback user. Existing non-anonymous users are updated when provided.
        recommenderLocale:
          type: string
          enum: [en, de, es, fr, it, nl, pl, pt]
          description: Optional locale for the recommender user. Existing non-anonymous users are updated when provided.
        checkoutLocation:
          type: string
        lineItems:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/LineItem'
        salesChannel:
          type: string
          enum: [online, offline, hotline]
        isTest:
          type: boolean
        tracking:
          $ref: '#/components/schemas/CreateSaleTrackingPayload'

    CreateSaleTrackingPayload:
      type: object
      required: [mode]
      properties:
        mode:
          type: string
          enum: [aid, coupon, ip_and_fingerprint, native_cashback]
        aid:
          type: string
        coupon:
          type: string
          description: Coupon identifier. If coupon resolves to Keep, `email` is optional. Keep eligibility follows matcher semantics (`reward_receivers` includes `mittlr`, or fixed-mode with recommender-only rewards).
        email:
          type: string
          description: Required for coupon Share/Give flows; optional for Keep.
        ip:
          type: string
        fingerprint:
          type: string
        encryptedEmail:
          type: string

    TrackingAcceptedResponse:
      type: object
      additionalProperties: false

    UpdateSaleRequest:
      type: object
      required: [lineItems]
      properties:
        lineItems:
          type: array
          description: New line items (replaces all existing).
          minItems: 1
          items:
            $ref: '#/components/schemas/LineItem'

    # ── Error Schemas ──
    ErrorResponse:
      type: object
      required: [error]
      description: Canonical error envelope.
      properties:
        error:
          type: object
          required: [code, message, requestId]
          properties:
            code:
              type: string
              description: Machine-readable error code (language-neutral, snake_case).
            message:
              type: string
              description: Human-readable, localized error message.
            requestId:
              type: string
              description: Correlation ID matching `X-Request-Id` response header.
            details:
              type: object
              additionalProperties: true
              description: Optional structured details.

    ValidationErrorResponse:
      type: object
      required: [error]
      description: Error response with field-level validation details.
      properties:
        error:
          type: object
          required: [code, message, requestId]
          properties:
            code:
              type: string
              example: "validation_error"
            message:
              type: string
              example: "Request validation failed"
            requestId:
              type: string
            details:
              type: object
              properties:
                errors:
                  type: array
                  items:
                    $ref: '#/components/schemas/ValidationFieldError'

    ValidationFieldError:
      type: object
      required: [field, code, message]
      properties:
        field:
          type: string
          description: JSON path of the invalid field.
        code:
          type: string
          description: Machine-readable field error code.
        message:
          type: string
          description: Localized field error message.

  # ── Reusable Responses ────────────────────────
  responses:
    ValidationError:
      description: Invalid or missing request fields.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Content-Language:
          $ref: '#/components/headers/ContentLanguage'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'
          examples:
            missingPromotionUid:
              summary: Missing required field
              value:
                error:
                  code: "validation_error"
                  message: "Request validation failed"
                  requestId: "550e8400-e29b-41d4-a716-446655440002"
                  details:
                    errors:
                      - field: "promotionUid"
                        code: "required"
                        message: "promotionUid is required"

    AuthenticationRequired:
      description: Missing or invalid `Api-Secret`.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Content-Language:
          $ref: '#/components/headers/ContentLanguage'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            missingApiKey:
              summary: No Api-Secret header
              value:
                error:
                  code: "authentication_required"
                  message: "Missing or invalid Api-Secret header"
                  requestId: "550e8400-e29b-41d4-a716-446655440003"

    ResourceNotFound:
      description: Sale does not exist or is not accessible.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Content-Language:
          $ref: '#/components/headers/ContentLanguage'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            saleNotFound:
              summary: Sale not found
              value:
                error:
                  code: "resource_not_found"
                  message: "Sale with ID '4f91cc9f-6e9f-4d72-b9f7-3f4e2a1d4f90' was not found"
                  requestId: "550e8400-e29b-41d4-a716-446655440004"

    RateLimitExceeded:
      description: Too many requests.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Content-Language:
          $ref: '#/components/headers/ContentLanguage'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            rateLimited:
              summary: Rate limit exceeded
              value:
                error:
                  code: "rate_limit_exceeded"
                  message: "Rate limit exceeded. Please retry later."
                  requestId: "550e8400-e29b-41d4-a716-446655440005"
                  details:
                    retryAfter: 60

    InternalServerError:
      description: Unexpected server error.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Content-Language:
          $ref: '#/components/headers/ContentLanguage'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            internalError:
              summary: Internal server error
              value:
                error:
                  code: "internal_server_error"
                  message: "An unexpected error occurred"
                  requestId: "550e8400-e29b-41d4-a716-446655440006"
