openapi: 3.0.3
info:
  title: PriceTag Retailer Integration API
  version: '1.0'
  description: |
    Push inbound API for gestionale / POS / middleware (Italia).
    Wire JSON in English field names: name, price, department (not nome/prezzo/reparto).
    Products are keyed by EAN. Mutations translate to internal snapshot ops.
    Not a bidirectional ERP sync.
servers:
  - url: https://webapp-pricetag-production.up.railway.app/api
    description: Production
security:
  - IntegrationBearer: []
  - PriceTagKey: []
paths:
  /v1/integration/stores/{storeId}/health:
    get:
      summary: Token + store reachability
      parameters:
        - $ref: '#/components/parameters/storeId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok, storeId, enabled]
                properties:
                  ok: { type: boolean }
                  storeId: { type: string, format: uuid }
                  enabled: { type: boolean }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/integration/stores/{storeId}/products/{ean}:
    get:
      summary: Read one product by EAN
      parameters:
        - $ref: '#/components/parameters/storeId'
        - name: ean
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Product
          content:
            application/json:
              schema:
                type: object
                required: [ean, name, price]
                properties:
                  ean: { type: string }
                  name: { type: string }
                  price: { type: string, description: Italian display e.g. "3,49 €" }
                  department: { type: string }
                  brand: { type: string }
                  sku: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/integration/stores/{storeId}/catalog:
    post:
      summary: Upsert products by EAN
      parameters:
        - $ref: '#/components/parameters/storeId'
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CatalogRequest'
            example:
              items:
                - ean: '8051277182755'
                  name: POMODORI SECCHI
                  price: '3.49'
                  department: ORTOFRUTTA
                  sku: ART-9912
              mode: upsert
      responses:
        '200':
          description: Partial success allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
  /v1/integration/stores/{storeId}/prices:
    post:
      summary: Update prices by EAN
      parameters:
        - $ref: '#/components/parameters/storeId'
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PricesRequest'
            example:
              items:
                - ean: '8051277182755'
                  price: '3.29'
              queuePolicy: snapshot_only
              reviewPolicy: require_review
      responses:
        '200':
          description: Partial success allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
components:
  securitySchemes:
    IntegrationBearer:
      type: http
      scheme: bearer
      bearerFormat: integration_token
    PriceTagKey:
      type: apiKey
      in: header
      name: X-PriceTag-Key
  parameters:
    storeId:
      name: storeId
      in: path
      required: true
      schema: { type: string, format: uuid }
    idempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 128 }
  schemas:
    CatalogRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            type: object
            required: [ean, name, price]
            properties:
              ean: { type: string }
              name: { type: string }
              price:
                oneOf:
                  - { type: string }
                  - { type: number }
                description: Decimale con punto ("3.49"); the server normalizes it to Italian display
              department: { type: string }
              brand: { type: string, description: Opzionale/legacy — non richiesto nel contratto partner }
              sku: { type: string }
        mode:
          type: string
          enum: [upsert]
    PricesRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            type: object
            required: [ean, price]
            properties:
              ean: { type: string }
              price:
                oneOf:
                  - { type: string }
                  - { type: number }
        queuePolicy:
          type: string
          enum: [snapshot_only, enqueue_if_esl, enqueue_always]
          default: snapshot_only
        reviewPolicy:
          type: string
          enum: [apply_immediate, require_review]
          default: require_review
          description: |
            require_review = stage pending; la app JWT approva/rifiuta (default).
            apply_immediate = last-write al snapshot senza modal.
    BatchResult:
      type: object
      required: [accepted, created, updated, rejected]
      properties:
        accepted: { type: integer }
        created: { type: integer }
        updated: { type: integer }
        status:
          type: string
          enum: [pending_review, applied]
        reviewId: { type: string, format: uuid }
        rejected:
          type: array
          items:
            type: object
            required: [ean, code, message]
            properties:
              ean: { type: string }
              code: { type: string }
              message: { type: string }
  responses:
    BadRequest:
      description: Invalid body
    Unauthorized:
      description: Missing or invalid token
    Forbidden:
      description: Wrong store or integration disabled
    NotFound:
      description: Store or product not found
    Conflict:
      description: Idempotency key mismatch
    Unprocessable:
      description: Ops rejected against current snapshot
    TooManyRequests:
      description: Rate limit (60/min per token)
