openapi: 3.1.0
info:
  title: Prolo Procurement — Quote API
  version: "1.0.0"
  description: >
    Interface of the internal procurement system (Deliverable 4). Prolo's pipeline is a client:
    submit/update are the P2 seam; search/retrieve is for consumers. Money is decimal; requests are
    idempotent on messageId.
servers:
  - url: https://quotes.internal/v1
    description: Internal (private ALB, no public ingress)
security:
  - bearerAuth: []

paths:
  /quotes:
    get:
      summary: Search & retrieve quote metadata
      operationId: searchQuotes
      parameters:
        - { name: tenantId,    in: query, required: true,  schema: { type: string } }
        - { name: vendor,      in: query, schema: { type: string } }
        - { name: quoteNumber, in: query, schema: { type: string } }
        - { name: status,      in: query, schema: { $ref: '#/components/schemas/QuoteStatus' } }
        - { name: isLatest,    in: query, schema: { type: boolean } }
        - { name: page,        in: query, schema: { type: integer, default: 1, minimum: 1 } }
        - { name: pageSize,    in: query, schema: { type: integer, default: 50, minimum: 1, maximum: 100 } }
        - { name: expand,      in: query, schema: { type: array, items: { type: string } }, style: form, explode: false }
      responses:
        "200":
          description: A page of quote metadata (no line items)
          content: { application/json: { schema: { $ref: '#/components/schemas/QuoteSearchResponse' } } }
        "401": { $ref: '#/components/responses/Problem' }
        "403": { $ref: '#/components/responses/Problem' }
    post:
      summary: Submit a newly extracted quote
      operationId: submitQuote
      parameters:
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string }, description: "= messageId" }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/SubmitQuoteRequest' } } }
      responses:
        "201":
          description: Created
          headers: { Location: { schema: { type: string } } }
          content: { application/json: { schema: { $ref: '#/components/schemas/QuoteWriteResponse' } } }
        "200":
          description: Idempotent replay (same Idempotency-Key) — existing quote, no double-write
          content: { application/json: { schema: { $ref: '#/components/schemas/QuoteWriteResponse' } } }
        "422":
          description: Schema invalid, or Σ(quantity×price)+tax+shipping ≠ net
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/ProblemDetails' } } }

  /quotes/{quoteId}:
    get:
      summary: Retrieve one quote (full)
      operationId: getQuote
      parameters:
        - { name: quoteId, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: expand,  in: query, schema: { type: array, items: { type: string } }, style: form, explode: false }
      responses:
        "200":
          description: The full quote (metadata + body + optional expands)
          content: { application/json: { schema: { $ref: '#/components/schemas/QuoteResource' } } }
        "404": { $ref: '#/components/responses/Problem' }
    patch:
      summary: Status transition (e.g. MATCHED, SUBMITTED)
      operationId: patchQuoteStatus
      parameters:
        - { name: quoteId, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/QuoteStatusPatch' } } }
      responses:
        "200":
          description: Updated
          content: { application/json: { schema: { $ref: '#/components/schemas/QuoteWriteResponse' } } }
        "404": { $ref: '#/components/responses/Problem' }

  /quotes/{quoteId}/revisions:
    get:
      summary: Full version history of a quote
      operationId: listRevisions
      parameters:
        - { name: quoteId, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: All versions, ordered by revision
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/QuoteResource' } }

  /quotes/{quoteGroupId}/revisions:
    post:
      summary: Submit a revision (new version appended)
      operationId: submitRevision
      parameters:
        - { name: quoteGroupId,    in: path,   required: true, schema: { type: string, format: uuid } }
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string }, description: "the NEW email's messageId" }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/SubmitQuoteRequest' } } }
      responses:
        "201":
          description: New version created; prior flipped to SUPERSEDED (one transaction)
          content: { application/json: { schema: { $ref: '#/components/schemas/RevisionWriteResponse' } } }
        "409":
          description: Pure duplicate (same content), not a revision
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/ProblemDetails' } } }

components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }

  responses:
    Problem:
      description: Error
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/ProblemDetails' } } }

  schemas:
    QuoteStatus:
      type: string
      enum: [EXTRACTED, NEEDS_REVIEW, MATCHED, SUBMITTED, SUPERSEDED, FAILED]

    LineItem:
      type: object
      required: [description, quantity, unitOfMeasure, price]
      additionalProperties: false
      properties:
        sku:           { type: [string, "null"] }
        description:   { type: string }
        quantity:      { type: number, exclusiveMinimum: 0 }
        unitOfMeasure: { type: string }
        price:         { type: number, minimum: 0, description: per-unit }

    Totals:
      type: object
      required: [tax, shipping, net]
      additionalProperties: false
      properties:
        tax:      { type: number, minimum: 0 }
        shipping: { type: number, minimum: 0 }
        net:      { type: number, minimum: 0 }

    SupplierQuote:               # == SupplierQuoteObject (the brief's contract)
      type: object
      required: [vendorIdentifier, totals, lineItems]
      additionalProperties: false
      properties:
        vendorIdentifier: { type: string }
        totals:    { $ref: '#/components/schemas/Totals' }
        lineItems: { type: array, minItems: 1, items: { $ref: '#/components/schemas/LineItem' } }

    Confidence:
      type: object
      properties:
        vendor:    { type: number }
        totals:    { type: number }
        lineItems: { type: array, items: { type: number } }

    Provenance:
      type: object
      properties:
        field:  { type: string }
        source: { type: string }
        page:   { type: integer }
        bbox:   { type: array, items: { type: number } }

    SubmitQuoteRequest:
      type: object
      required: [tenantId, messageId, quote]
      properties:
        tenantId:  { type: string }
        messageId: { type: string }
        quote:     { $ref: '#/components/schemas/SupplierQuote' }

    QuoteStatusPatch:
      type: object
      required: [status]
      properties:
        status: { $ref: '#/components/schemas/QuoteStatus' }

    QuoteMetadata:               # a quote WITHOUT its line items
      type: object
      properties:
        quoteId:          { type: string, format: uuid }
        quoteGroupId:     { type: string, format: uuid }
        revision:         { type: integer }
        isLatest:         { type: boolean }
        vendorIdentifier: { type: string }
        quoteNumber:      { type: [string, "null"] }
        quoteDate:        { type: [string, "null"], format: date }
        currency:         { type: [string, "null"] }
        totals:           { $ref: '#/components/schemas/Totals' }
        status:           { $ref: '#/components/schemas/QuoteStatus' }
        confidence:       { $ref: '#/components/schemas/Confidence' }
        created:          { type: string, format: date-time }
        updated:          { type: string, format: date-time }

    QuoteResource:               # full retrieve = metadata + body (+ expands)
      allOf:
        - { $ref: '#/components/schemas/QuoteMetadata' }
        - type: object
          properties:
            quote:      { $ref: '#/components/schemas/SupplierQuote' }
            provenance: { type: array, items: { $ref: '#/components/schemas/Provenance' } }

    QuoteSearchResponse:
      type: object
      properties:
        items:    { type: array, items: { $ref: '#/components/schemas/QuoteMetadata' } }
        page:     { type: integer }
        pageSize: { type: integer }

    QuoteWriteResponse:
      type: object
      properties:
        quoteId:      { type: string, format: uuid }
        quoteGroupId: { type: string, format: uuid }
        revision:     { type: integer }
        status:       { $ref: '#/components/schemas/QuoteStatus' }

    RevisionWriteResponse:
      allOf:
        - { $ref: '#/components/schemas/QuoteWriteResponse' }
        - type: object
          properties:
            supersedesId: { type: [string, "null"], format: uuid }

    ProblemDetails:              # RFC-9457
      type: object
      required: [type, title, status]
      properties:
        type:   { type: string }
        title:  { type: string }
        status: { type: integer }
        detail: { type: string }
        errors: { type: array, items: { type: object } }
