openapi: 3.0.0
paths:
  /v1/catalog/products:
    get:
      description: 'Paginated, filterable list of the external-safe product catalog for the authenticated tenant and allowed stores. Required scope: `catalog.products.read`.'
      operationId: ExternalCatalogController_listProducts
      parameters:
        - name: X-Request-Id
          in: header
          description: Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).
          required: false
          schema:
            type: string
        - name: page
          required: false
          in: query
          description: 1-indexed page number.
          schema:
            minimum: 1
            default: 1
            example: 1
            type: number
        - name: pageSize
          required: false
          in: query
          description: Rows per page.
          schema:
            minimum: 1
            maximum: 100
            default: 50
            example: 50
            type: number
        - name: storeId
          required: false
          in: query
          description: Restrict to one store. Must be a store this IntegrationClient is allowed to access; otherwise 404.
          schema:
            example: clx1store0000001
            type: string
        - name: categoryId
          required: false
          in: query
          schema:
            example: clx1category000001
            type: string
        - name: q
          required: false
          in: query
          description: Free-text search across name, SKU, barcode and brand (case-insensitive).
          schema:
            example: bread
            type: string
        - name: barcode
          required: false
          in: query
          schema:
            example: '5000000000001'
            type: string
        - name: sku
          required: false
          in: query
          schema:
            example: SKU-ALPHA-BREAD
            type: string
        - name: updatedSince
          required: false
          in: query
          description: Only return products updated at or after this instant.
          schema:
            format: date-time
            example: '2026-08-01T00:00:00.000Z'
            type: string
        - name: activeOnly
          required: false
          in: query
          description: When true (default), only active products are returned.
          schema:
            default: true
            type: boolean
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CatalogProductListResponseDto'
        '400':
          description: validation_failed - the request query/params failed validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '401':
          description: unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '403':
          description: scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '404':
          description: not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '413':
          description: payload_too_large - request exceeded the configured body size limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '429':
          description: rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 42
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '500':
          description: internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
      security:
        - IntegrationApiKey: []
      summary: List catalog products
      tags:
        - Catalog
      x-epos-scopes:
        - catalog.products.read
  /v1/catalog/products/{id}:
    get:
      description: 'Fetch a single product by ID. Returns 404 if the product does not exist, or exists outside the authenticated tenant/allowed stores. Required scope: `catalog.products.read`.'
      operationId: ExternalCatalogController_getProduct
      parameters:
        - name: X-Request-Id
          in: header
          description: Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).
          required: false
          schema:
            type: string
        - name: id
          required: true
          in: path
          description: Product ID.
          schema:
            example: clx1product000001
            type: string
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CatalogProductDto'
        '400':
          description: validation_failed - the request query/params failed validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '401':
          description: unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '403':
          description: scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '404':
          description: not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '413':
          description: payload_too_large - request exceeded the configured body size limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '429':
          description: rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 42
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '500':
          description: internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
      security:
        - IntegrationApiKey: []
      summary: Get a catalog product
      tags:
        - Catalog
      x-epos-scopes:
        - catalog.products.read
  /v1/catalog/categories:
    get:
      description: 'Filterable list of external-safe product categories for the authenticated tenant and allowed stores. Not paginated. Required scope: `catalog.categories.read`.'
      operationId: ExternalCatalogController_listCategories
      parameters:
        - name: X-Request-Id
          in: header
          description: Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).
          required: false
          schema:
            type: string
        - name: storeId
          required: false
          in: query
          description: Restrict to one store. Must be a store this IntegrationClient is allowed to access; otherwise 404.
          schema:
            example: clx1store0000001
            type: string
        - name: parentCategoryId
          required: false
          in: query
          schema:
            example: clx1category000001
            type: string
        - name: activeOnly
          required: false
          in: query
          description: When true (default), only active categories are returned. Archived categories are always excluded.
          schema:
            default: true
            type: boolean
        - name: sortBy
          required: false
          in: query
          description: Sort order. Defaults to sortOrder, then name.
          schema:
            example: sortOrder
            type: string
            enum:
              - name
              - sortOrder
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CatalogCategoryDto'
        '400':
          description: validation_failed - the request query/params failed validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '401':
          description: unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '403':
          description: scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '404':
          description: not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '413':
          description: payload_too_large - request exceeded the configured body size limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '429':
          description: rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 42
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '500':
          description: internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
      security:
        - IntegrationApiKey: []
      summary: List catalog categories
      tags:
        - Catalog
      x-epos-scopes:
        - catalog.categories.read
  /v1/catalog/promotions:
    get:
      description: 'Filterable list of customer-facing, external-safe promotions for the authenticated tenant and allowed stores. Not paginated. Coupon, staff-discount and manual-discount promotion types, POS-only system-generated presets, and DRAFT/ARCHIVED promotions are never returned by this endpoint, regardless of filters. Required scope: `catalog.promotions.read`.'
      operationId: ExternalCatalogController_listPromotions
      parameters:
        - name: X-Request-Id
          in: header
          description: Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).
          required: false
          schema:
            type: string
        - name: storeId
          required: false
          in: query
          description: Restrict to one store. Must be a store this IntegrationClient is allowed to access; otherwise 404.
          schema:
            example: clx1store0000001
            type: string
        - name: activeAt
          required: false
          in: query
          description: Return promotions active at this instant (status ACTIVE and inside [startsAt, endsAt]). Forces the same status+window restriction as activeOnly=true regardless of the activeOnly value passed alongside it.
          schema:
            format: date-time
            example: '2026-08-27T18:00:00.000Z'
            type: string
        - name: type
          required: false
          in: query
          description: Exact match on promotion type. A type outside the customer-facing allow-list simply returns an empty array.
          schema:
            example: MEAL_DEAL
            type: string
        - name: activeOnly
          required: false
          in: query
          description: When true (default), only ACTIVE promotions currently inside their active window are returned. When false, PAUSED promotions are also included (still never DRAFT/ARCHIVED).
          schema:
            default: true
            type: boolean
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CatalogPromotionDto'
        '400':
          description: validation_failed - the request query/params failed validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '401':
          description: unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '403':
          description: scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '404':
          description: not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '413':
          description: payload_too_large - request exceeded the configured body size limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '429':
          description: rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 42
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '500':
          description: internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
      security:
        - IntegrationApiKey: []
      summary: List catalog promotions
      tags:
        - Catalog
      x-epos-scopes:
        - catalog.promotions.read
  /v1/inventory/stock:
    get:
      description: 'Paginated, filterable list of exact stock levels for the authenticated tenant and allowed stores. Returns stock visibility only - no cost/margin/supplier/movement-history data. Required scope: `inventory.stock.read`.'
      operationId: ExternalInventoryController_listStock
      parameters:
        - name: X-Request-Id
          in: header
          description: Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).
          required: false
          schema:
            type: string
        - name: page
          required: false
          in: query
          description: 1-indexed page number.
          schema:
            minimum: 1
            default: 1
            example: 1
            type: number
        - name: pageSize
          required: false
          in: query
          description: Rows per page.
          schema:
            minimum: 1
            maximum: 100
            default: 50
            example: 50
            type: number
        - name: storeId
          required: false
          in: query
          description: Restrict to one store. Must be a store this IntegrationClient is allowed to access; otherwise 404.
          schema:
            example: clx1store0000001
            type: string
        - name: productId
          required: false
          in: query
          schema:
            example: clx1product000001
            type: string
        - name: sku
          required: false
          in: query
          schema:
            example: SKU-ALPHA-BREAD
            type: string
        - name: barcode
          required: false
          in: query
          schema:
            example: '5000000000001'
            type: string
        - name: categoryId
          required: false
          in: query
          schema:
            example: clx1category000001
            type: string
        - name: updatedSince
          required: false
          in: query
          description: Only return products updated at or after this instant.
          schema:
            format: date-time
            example: '2026-08-01T00:00:00.000Z'
            type: string
        - name: lowStockOnly
          required: false
          in: query
          description: When true, only products with availability=low_stock are returned.
          schema:
            type: boolean
        - name: activeOnly
          required: false
          in: query
          description: When true (default), only active products are returned.
          schema:
            default: true
            type: boolean
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryStockListResponseDto'
        '400':
          description: validation_failed - the request query/params failed validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '401':
          description: unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '403':
          description: scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '404':
          description: not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '413':
          description: payload_too_large - request exceeded the configured body size limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '429':
          description: rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 42
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
        '500':
          description: internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorDto'
      security:
        - IntegrationApiKey: []
      summary: List inventory stock
      tags:
        - Inventory
      x-epos-scopes:
        - inventory.stock.read
info:
  title: EPOS Developer API
  description: Public/partner API for approved EPOS integrations. Every route lives under `/v1` and is versioned, redacted and rate-limited independently of EPOS's internal (unversioned) API, which is out of scope for this document and never appears in it. Access requires an Integration API key issued to an approved IntegrationClient, scoped to specific capabilities and optionally restricted to specific stores. This document is a DEV review artifact and is not yet published to a public developer docs site.
  version: 1.0.0
  contact: {}
tags:
  - name: Catalog
    description: Products, categories and customer-facing promotions.
  - name: Inventory
    description: Exact inventory stock levels.
servers:
  - url: https://dev-api.theprioryshop.co.uk
    description: Development
  - url: https://api.theprioryshop.co.uk
    description: Production (this server existing does not mean the public API has launched - see the Phase 2 review workflow)
components:
  securitySchemes:
    IntegrationApiKey:
      scheme: bearer
      bearerFormat: EPOS integration API key issued to an IntegrationClient
      type: http
      description: 'EPOS integration API key issued to an IntegrationClient via Back Office Developer Tools > Integrations. Not a JWT, not OAuth2 - a static API key. Format: `Authorization: Bearer epos_live_sk_...` (production) or `Bearer epos_dev_sk_...` (development). Staff/Back Office JWT sessions are never valid for `/v1` - only Integration API keys are.'
  schemas:
    ExternalApiErrorBodyDto:
      type: object
      properties:
        code:
          type: string
          description: Stable machine-readable error code. See the Error codes table in the API reference.
          example: validation_failed
          enum:
            - validation_failed
            - unauthorized
            - scope_denied
            - not_found
            - payload_too_large
            - rate_limited
            - internal_error
        message:
          type: string
          description: Human-readable message. Safe to show to a developer, not localized, not guaranteed to be stable text - match on `code`, not this string.
          example: Request validation failed.
        details:
          type: object
          description: Additional machine-readable detail, shape depends on `code` (e.g. validation errors, required scopes). Empty object when there is nothing more to add.
          additionalProperties: true
          example: {}
        requestId:
          type: string
          description: Request ID for this response - same value as the `X-Request-Id` response header. Include this when reporting a problem.
          example: req_AbCdEfGh12345678
      required:
        - code
        - message
        - details
        - requestId
    ExternalApiErrorDto:
      type: object
      properties:
        error:
          $ref: '#/components/schemas/ExternalApiErrorBodyDto'
      required:
        - error
    CatalogProductVatRateDto:
      type: object
      properties:
        id:
          type: string
          example: clx1vatrate000001
        code:
          type: string
          description: Short VAT rate code.
          example: STD
        name:
          type: string
          example: Standard Rate
        percentage:
          type: string
          description: VAT percentage, serialized as a decimal string.
          example: '20.00'
      required:
        - id
        - code
        - name
        - percentage
    CatalogProductDto:
      type: object
      properties:
        id:
          type: string
          example: clx1product000001
        storeId:
          type: string
          example: clx1store0000001
        categoryId:
          type: object
          nullable: true
          example: clx1category000001
        name:
          type: string
          example: Alpha Bread 800g
        sku:
          type: string
          example: SKU-ALPHA-BREAD
        barcode:
          type: object
          nullable: true
          example: '5000000000001'
        brand:
          type: object
          nullable: true
          example: Priory
        size:
          type: object
          nullable: true
          example: 800g
        unitType:
          type: string
          description: Unit the product is sold/measured in.
          enum:
            - item
            - kg
            - g
            - litre
            - ml
          example: item
        sellingPriceGross:
          type: string
          description: Selling price including VAT, as a decimal string.
          example: '1.25'
        sellingPriceNet:
          type: string
          description: Selling price excluding VAT, as a decimal string.
          example: '1.04'
        taxCategory:
          type: string
          enum:
            - STANDARD
            - ZERO_RATED
            - REDUCED
            - EXEMPT
            - OUT_OF_SCOPE
            - UNCLASSIFIED
          example: STANDARD
        vatRate:
          nullable: true
          type: object
          allOf:
            - $ref: '#/components/schemas/CatalogProductVatRateDto'
        isActive:
          type: boolean
        variablePrice:
          type: boolean
          description: Whether this product is sold at a variable/scale-entered price (e.g. deli, bakery by weight).
        ageRestricted:
          type: boolean
        alcoholRestricted:
          type: boolean
        tobaccoRestricted:
          type: boolean
        vapeRestricted:
          type: boolean
        updatedAt:
          type: string
          format: date-time
          example: '2026-08-27T17:51:41.976Z'
      required:
        - id
        - storeId
        - categoryId
        - name
        - sku
        - barcode
        - brand
        - size
        - unitType
        - sellingPriceGross
        - sellingPriceNet
        - taxCategory
        - vatRate
        - isActive
        - variablePrice
        - ageRestricted
        - alcoholRestricted
        - tobaccoRestricted
        - vapeRestricted
        - updatedAt
    CatalogProductListResponseDto:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: '#/components/schemas/CatalogProductDto'
        page:
          type: number
          description: 1-indexed current page.
          example: 1
        pageSize:
          type: number
          description: Rows per page (max 100).
          example: 50
        total:
          type: number
          description: Total rows matching the filters, independent of pagination.
          example: 479
        totalPages:
          type: number
          description: ceil(total / pageSize), minimum 1.
          example: 10
      required:
        - rows
        - page
        - pageSize
        - total
        - totalPages
    CatalogCategoryDto:
      type: object
      properties:
        id:
          type: string
          example: clx1category000001
        storeId:
          type: string
          example: clx1store0000001
        parentCategoryId:
          type: object
          nullable: true
          example: null
        name:
          type: string
          example: Bakery
        code:
          type: object
          nullable: true
          example: BAKE
        description:
          type: object
          nullable: true
          example: null
        sortOrder:
          type: number
          example: 1
        isActive:
          type: boolean
        updatedAt:
          type: string
          format: date-time
          example: '2026-08-27T17:51:41.976Z'
      required:
        - id
        - storeId
        - parentCategoryId
        - name
        - code
        - description
        - sortOrder
        - isActive
        - updatedAt
    CatalogPromotionDto:
      type: object
      properties:
        id:
          type: string
          example: clx1promo00000001
        storeId:
          type: string
          example: clx1store0000001
        name:
          type: string
          example: 'Meal Deal: Main + Snack + Drink = £3.50'
        description:
          type: object
          nullable: true
          example: null
        type:
          type: string
          description: Customer-facing promotion type. Coupon, staff-discount and manual-discount promotion types are never returned by this API - see the API reference's Promotions section.
          enum:
            - MEAL_DEAL
            - MULTIBUY_SAME_PRODUCT
            - ANY_N_FIXED_PRICE
            - MIX_MATCH_FIXED_PRICE
            - MULTIBUY_FIXED_PRICE
            - PRICE_CUT_FIXED_PRICE
            - PRICE_CUT_PERCENT
            - PRICE_CUT_FIXED
            - TIMED_REDUCTION_PERCENT
            - TIMED_REDUCTION_FIXED
            - LOYALTY_PRICE
          example: MEAL_DEAL
        startsAt:
          type: object
          format: date-time
          nullable: true
          example: null
        endsAt:
          type: object
          format: date-time
          nullable: true
          example: null
        isActive:
          type: boolean
          description: True when status is ACTIVE and the current time is inside [startsAt, endsAt]. Computed independently of the activeOnly/activeAt query filters.
        eligibilitySummary:
          type: string
          description: Plain-language description of who/what qualifies. Never the raw internal conditions JSON.
          example: Buy one qualifying item from each of the Main, Snack and Drink groups
        offerSummary:
          type: string
          description: Plain-language description of the discount. Never the raw internal actions JSON.
          example: Fixed price £3.50
        updatedAt:
          type: string
          format: date-time
          example: '2026-08-27T17:51:41.976Z'
      required:
        - id
        - storeId
        - name
        - description
        - type
        - startsAt
        - endsAt
        - isActive
        - eligibilitySummary
        - offerSummary
        - updatedAt
    InventoryStockItemDto:
      type: object
      properties:
        productId:
          type: string
          example: clx1product000001
        storeId:
          type: string
          example: clx1store0000001
        sku:
          type: string
          example: SKU-ALPHA-BREAD
        barcode:
          type: object
          nullable: true
          example: '5000000000001'
        name:
          type: string
          example: Alpha Bread 800g
        trackStock:
          type: boolean
          description: Whether this product's stock level is tracked at all. When false, availability is always not_tracked.
        stockQuantity:
          type: string
          description: Exact current stock quantity, as a decimal string.
          example: '6.000'
        minimumStockLevel:
          type: string
          description: Informational only - mirrored from reorderLevel on save, not independently used for the availability calculation.
          example: '0.000'
        reorderLevel:
          type: string
          description: The threshold actually used to compute `low_stock`.
          example: '5.000'
        availability:
          type: string
          description: 'not_tracked when trackStock is false. Otherwise: out_of_stock (<=0), low_stock (>0 and <= reorderLevel), in_stock (> reorderLevel).'
          enum:
            - in_stock
            - low_stock
            - out_of_stock
            - not_tracked
          example: in_stock
        updatedAt:
          type: string
          format: date-time
          example: '2026-08-27T17:51:41.976Z'
      required:
        - productId
        - storeId
        - sku
        - barcode
        - name
        - trackStock
        - stockQuantity
        - minimumStockLevel
        - reorderLevel
        - availability
        - updatedAt
    InventoryStockListResponseDto:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: '#/components/schemas/InventoryStockItemDto'
        page:
          type: number
          description: 1-indexed current page.
          example: 1
        pageSize:
          type: number
          description: Rows per page (max 100).
          example: 50
        total:
          type: number
          description: Total rows matching the filters, independent of pagination.
          example: 479
        totalPages:
          type: number
          description: ceil(total / pageSize), minimum 1.
          example: 10
      required:
        - rows
        - page
        - pageSize
        - total
        - totalPages
