openapi: 3.1.0
info:
  title: Shopwave API
  version: "2.0"
  description: |
    The Shopwave API is a comprehensive commerce platform API covering payments, loyalty,
    point of sale, and coupons. Developers can leverage the API to create intuitive and
    engaging applications for merchants and consumers.

    ## Authentication

    All API calls require an OAuth2 Bearer token passed via the `Authorization` header.
    Obtain tokens through the Shopwave Auth authorization code flow.

    ## Request conventions

    - All request and response bodies use `application/json`.
    - GET filter parameters are passed as **request headers** (not query strings).
    - POST bodies allow multiple objects in a single call, keyed by object reference.
    - To **update** a resource via POST, include the existing resource `id` in the body.

    ## Response envelope

    Every response includes an `api` object with structured success/error messages
    and server-side execution time for debugging.

    ## Pagination

    The API does not currently use paginated responses. List endpoints return full
    result sets, filterable via comma-separated IDs passed as request headers.

    ## Employee roles

    Employees are assigned a `roleId` that determines their permissions. The
    defined roles are:

    | roleId | Role               |
    |--------|--------------------|
    | 1      | Owner              |
    | 2      | Manager            |
    | 3      | Assistant          |
    | 4      | Guest              |
    | 5      | Assistant Manager  |

    The same role IDs are used at both the merchant level and the store level.

    ## Stability index

    Each endpoint carries a stability level indicating its maturity:

    | Level | Label        | Meaning |
    |-------|--------------|---------|
    | 0     | Experimental | Still under development; not ready for production use. Prone to changes and removal. |
    | 1     | Unstable     | In testing; usable but not fully stable. Additional features or changes possible. |
    | 2     | Stable       | Proven satisfactory. Minor changes possible but backwards-compatibility guaranteed. |
    | 3     | Frozen       | Extensively tested in production. Unlikely to change. |

    The stability level for each operation is indicated by the `x-stability` extension.

    ## API message codes

    Every response includes an `api.message` object containing success, error,
    and/or warning entries. Each entry has an `id`, `code`, `statusCode`,
    `title`, and `details` field. The full catalogue of codes is listed below.

    ### Success codes

    | id  | code              | statusCode | title                          |
    |-----|-------------------|------------|--------------------------------|
    | 200 | emailSent         | 200        | Email successfully sent        |
    | 201 | verifySuccess     | 200        | Account verified               |
    | 202 | tokenValid        | 200        | Token is valid                 |
    | 203 | requestSuccessful | 200        | Request Processed Successfully |
    | 204 | noContent         | 204        | No content to return           |
    | 205 | resetContent      | 205        | Content reset                  |
    | 206 | created           | 201        | Resource Created               |
    | 207 | accountChanged    | 205        | Account Updated                |

    ### API errors (devError)

    | id  | code                 | statusCode | title                                    |
    |-----|----------------------|------------|------------------------------------------|
    | 899 | seriousError         | 500        | Unknown Error                            |
    | 900 | unknownError         | 503        | Service Down                             |
    | 901 | invalidClient        | 400        | Invalid Client                           |
    | 902 | unauthorizedClient   | 401        | Unauthorised Client                      |
    | 903 | redirectUriMismatch  | 400        | Redirect URI Mismatch                    |
    | 904 | accessDenied         | 401        | Access Denied                            |
    | 905 | invalidScope         | 400        | Invalid Scope                            |
    | 906 | invalidGrant         | 400        | Invalid Grant                            |
    | 907 | invalidToken         | 401        | Invalid Token                            |
    | 908 | expiredToken         | 401        | Expired Token                            |
    | 909 | invalidCode          | 401        | Invalid Code                             |
    | 910 | notFound             | 404        | Not Found                                |
    | 911 | authHeaderInvalid    | 401        | Authorization Header Invalid             |
    | 912 | resourceNotAllowed   | 403        | Resource Not Allowed For Specified User  |
    | 913 | missingRequiredParam | 400        | Required parameter or object missing     |
    | 914 | invalidEmail         | 400        | Email address is not valid               |
    | 915 | promotionCodeUnique  | 400        | Promotion code is unique                 |
    | 916 | unsupportedMediaType | 415        | Unsupported Media Type                   |
    | 917 | serverError          | 500        | Internal Server Error                    |
    | 918 | jsonWebTokenNotFound | 400        | JSON Web Token Invalid or Not Found      |
    | 919 | tooManyRequest       | 429        | Cannot handle too many requests          |

    ### User errors

    | id | code                     | statusCode | title                        |
    |----|--------------------------|------------|------------------------------|
    | 0  | serviceDown              | 503        | Service Down Temporarily     |
    | 1  | unknownError             | 500        | Unknown Error                |
    | 2  | loginFailed              | 401        | Login Failed                 |
    | 3  | usernameInvalid          | 400        | Username Invalid             |
    | 4  | passwordInvalid          | 400        | Password Invalid             |
    | 5  | firstnameEmpty           | 400        | Firstname Empty              |
    | 6  | lastnameEmpty            | 400        | Lastname Empty               |
    | 7  | emailInvalid             | 400        | Email Invalid                |
    | 8  | usernameAlreadyExist     | 400        | Username Already Exist       |
    | 10 | passwordMismatch         | 400        | Password mismatch            |
    | 11 | captchaNotValid          | 400        | Captcha Error                |
    | 12 | verifyInvalid            | 400        | Verification Invalid         |
    | 13 | emailNotSent             | 500        | Email server down            |
    | 14 | emailAlreadyExist        | 400        | Email Already Exists         |
    | 15 | passwordResetLinkInvalid | 400        | Password Reset Link Invalid  |
    | 16 | currentPasswordInvalid   | 400        | Check Your Current Password  |
    | 17 | accountNotFound          | 400        | Account Not Found            |
    | 18 | oauthRedirectUriMismatch | 400        | Redirect URI Mismatch        |
    | 19 | oauthInvalidScope        | 400        | Invalid Scope                |
    | 20 | oauthUnauthorizedClient  | 401        | Unauthorised Application     |

    ### Warnings (devWarning)

    | id  | code                   | statusCode | title                         |
    |-----|------------------------|------------|-------------------------------|
    | 601 | basketCompletedDeleted | 403        | Baskets Already Complete      |
    | 602 | transactionCompleted   | 403        | Transaction Already Complete  |
    | 603 | multipleStoreExist     | 400        | Multiple Stores Exist         |
    | 604 | deprecationNotice      | 200        | Newer Version Available       |
    | 605 | orderCompletedDeleted  | 403        | Orders Already Complete       |
    | 606 | basketNotCompleted     | 403        | Baskets Not Complete          |

    ## Normalisation decisions

    The following ambiguities or inconsistencies were found in the source
    documentation and resolved as noted:

    - **employee/delete.js documents Application DELETE, not Employee DELETE.**
      The file title, endpoint, and parameters all reference `/application` with
      `applicationIds`. Treated as a copy-paste error; Employee DELETE was omitted
      from the spec since no actual employee deletion contract is documented.
    - **Basket GET uses an older doc format** (flat numbered keys instead of the
      `page[]` array structure). Fields were inferred from the limited data available.
    - **PUT endpoints are mostly stubs.** `category/put.js`, `employee/put.js`,
      `merchant/put.js`, `product/put.js`, `supplier/put.js`, `supplier/store/put.js`,
      and `product/component/put.js` are all empty files. Updates are handled via POST
      (supply an existing `id`), so no separate PUT operations were generated for these.
    - **Basket PUT is a genuine two-phase PUT** (not a stub) and is included.
    - **Transaction PUT** is documented and included (completes a transaction).
    - **Basket DELETE and Log DELETE** are marked Stability 0 (Experimental) with
      no request/response detail. Included with minimal parameter definitions.
    - **User POST** is marked Stability 0 with no field definitions. Included with
      a sensible minimal schema (firstName, lastName, email).
    - **Price fields** are typed as `string` (not number) because the API returns
      them as strings representing pence/minor currency units (e.g. `"1600.0"`).
    - **ID format:** Consistently `integer` across all resources, matching the
      majority of the source docs. Log IDs are the exception — they use composite
      string keys (e.g. `"1400776796990:1:12:STORE:12"`).
    - **Promotion `endDate`** is documented as `tinyint` in the source but the
      example value is a datetime string. Normalised to `format: date-time`.
    - **Stock reconcile GET** has a response status code typo (says 201 in the
      example but is a GET endpoint). Normalised to 200.
    - **`daysAvaliable`** (typo in source) normalised to `daysAvailable`.
    - **`qunatity`** (typo in source) normalised to `quantity`.
    - **Product component POST** documents the endpoint as `/product` but lives
      under the component directory. Mapped to `/product/component` for consistency.
    - **Report GET** uses a custom SWQL query language passed as a header-encoded
      JSON string. Documented as a string parameter; the SWQL grammar is described
      in the endpoint description rather than as a formal schema.
    - **No pagination** exists in any documented endpoint. All list endpoints use
      header-based ID filtering to scope results.
  license:
    name: Proprietary

servers:
  - url: https://api.dev.merchantstack.com
    description: Shopwave API

tags:
  - name: Applications
    description: Manage OAuth applications (identifiers, secrets, settings)
  - name: Baskets
    description: Create and complete baskets with products and promotions
  - name: Categories
    description: Manage merchant product categories
  - name: Consumers
    description: Retrieve consumer profiles associated with a merchant
  - name: Employees
    description: Manage merchant employees and roles
  - name: Invoices
    description: Track merchant purchase invoices and stock receipts
  - name: Logs
    description: General-purpose JSON data storage and retrieval
  - name: Merchants
    description: Manage merchant profile and settings
  - name: Orders
    description: Attach delivery orders to completed baskets
  - name: Products
    description: Manage products, variants, pricing, and stock
  - name: Product Components
    description: Manage product sub-item groups and component options
  - name: Product Tags
    description: Retrieve unique product and instance tags
  - name: Promotions
    description: Manage promotions and generate promotion codes
  - name: Reports
    description: Aggregated basket, transaction, and product reports
  - name: Status
    description: Server health check
  - name: Stock
    description: Stock levels and reconciliation
  - name: Stores
    description: Manage merchant store locations
  - name: Suppliers
    description: Manage supplier profiles and store locations
  - name: Transactions
    description: Create and complete payment transactions
  - name: Uploader
    description: Upload media files to the CDN
  - name: Users
    description: Manage user profiles and invitations

security: []

paths:
  # ──────────────────────────── Status ────────────────────────────
  /status:
    get:
      operationId: getStatus
      summary: Server health check
      description: Returns server status and timestamp. Used to verify that the API is up and running.
      tags: [Status]
      x-stability: 3
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      responses:
        "200":
          description: Server is healthy
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      status:
                        type: string
                        description: Static status message
                        example: OK
                      serverTimestamp:
                        type: string
                        format: date-time
                        description: Current server datetime in ISO 8601 format (UTC)
                        example: "2014-07-31T14:44:21.845Z"
                    required: [status, serverTimestamp]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Application ────────────────────────────
  /application:
    get:
      operationId: getApplications
      summary: List applications
      description: Retrieve details of your Shopwave applications including identifiers, secrets, and configuration.
      tags: [Applications]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: applicationIds
          in: header
          description: Comma-separated application IDs to retrieve. Returns all if omitted.
          required: false
          schema:
            type: string
            example: "1,2,4"
      responses:
        "200":
          description: Applications retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      applications:
                        type: object
                        description: Map of applications keyed by application ID
                        additionalProperties:
                          $ref: "#/components/schemas/Application"
                    required: [applications]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateApplications
      summary: Create or update applications
      description: |
        Create new applications or update existing ones. To update, include the application `id`
        in the request body. Each application receives a unique identifier and secret for OAuth integration.
      tags: [Applications]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApplicationRequest"
      responses:
        "201":
          description: Application(s) created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      applications:
                        type: object
                        description: Map of created/updated applications keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/ApplicationResponse"
                    required: [applications]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteApplications
      summary: Delete applications
      description: Permanently delete one or more applications from your account.
      tags: [Applications]
      x-stability: 1
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: applicationIds
          in: header
          description: Comma-separated application IDs to delete
          required: true
          schema:
            type: string
            example: "1,2,4"
      responses:
        "205":
          description: Applications deleted successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Basket ────────────────────────────
  /basket:
    get:
      operationId: getBaskets
      summary: List baskets
      description: Retrieve baskets by basket IDs or customer ID.
      tags: [Baskets]
      x-stability: 1
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: basketIds
          in: header
          description: Comma-separated basket IDs to retrieve
          required: false
          schema:
            type: string
            example: "100,200"
      responses:
        "200":
          description: Baskets retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      baskets:
                        type: object
                        description: Map of baskets keyed by basket ID
                        additionalProperties:
                          $ref: "#/components/schemas/Basket"
                    required: [baskets]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createBasket
      summary: Create a basket
      description: |
        Pre-generate a basket ID by creating an empty basket tied to a store.
        Use this before the PUT flow if you need the basket ID upfront.
      tags: [Baskets]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - $ref: "#/components/parameters/ApplyPromotionOriginalPriceOnly"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBasketRequest"
      responses:
        "201":
          description: Basket created
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      baskets:
                        type: object
                        description: Map of created baskets keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/BasketResponse"
                    required: [baskets]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    put:
      operationId: updateBasket
      summary: Update or complete a basket
      description: |
        Two-phase basket flow:

        **Part 1** — Send basket details (store, products, promotions) to create or update
        the basket. Returns a basket ID for further operations.

        **Part 2** — Set `completed: true` to finalise the basket. Once completed, no
        further modifications are allowed. Transactions should be attached between Part 1 and Part 2.
      tags: [Baskets]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - $ref: "#/components/parameters/ApplyPromotionOriginalPriceOnly"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateBasketRequest"
      responses:
        "201":
          description: Basket updated or completed
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      baskets:
                        type: object
                        description: Map of baskets keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/BasketResponse"
                    required: [baskets]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteBaskets
      summary: Delete baskets
      description: Delete non-completed baskets from the merchant account. Only baskets that have not been finalised can be deleted.
      tags: [Baskets]
      x-stability: 0
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: basketIds
          in: header
          description: Comma-separated basket IDs to delete
          required: true
          schema:
            type: string
            example: "100,200"
      responses:
        "205":
          description: Baskets deleted successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Category ────────────────────────────
  /category:
    get:
      operationId: getCategories
      summary: List categories
      description: Retrieve merchant product categories, optionally filtered by ID.
      tags: [Categories]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: categoryIds
          in: header
          description: Comma-separated category IDs to retrieve. Returns all if omitted.
          required: false
          schema:
            type: string
            example: "1,2,3"
      responses:
        "200":
          description: Categories retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      categories:
                        type: object
                        description: Map of categories keyed by category ID
                        additionalProperties:
                          $ref: "#/components/schemas/Category"
                    required: [categories]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateCategories
      summary: Create or update categories
      description: Create new categories or update existing ones. To update, include the category `id` in the request body.
      tags: [Categories]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCategoryRequest"
      responses:
        "201":
          description: Category created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      categories:
                        type: object
                        description: Map of categories keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/Category"
                    required: [categories]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteCategory
      summary: Delete a category
      description: Permanently delete a category by ID.
      tags: [Categories]
      x-stability: 2
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: categoryId
          in: header
          description: The category ID to delete
          required: true
          schema:
            type: string
            example: "1"
      responses:
        "205":
          description: Category deleted successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Consumer ────────────────────────────
  /consumer:
    get:
      operationId: getConsumers
      summary: List consumers
      description: |
        Retrieve consumer profiles for the authenticated merchant.

        Supports two query modes (passed as request headers):

        1. **By IDs** — pass a comma-separated `ids` header. Available to roles 1 (Owner), 2 (Manager), 3 (Assistant), and 5 (Assistant Manager). Roles 1, 2, and 5 receive full consumer details; role 3 receives masked email and abbreviated last name.
        2. **By date range** — pass `from` and `to` headers (format `YYYY-MM-DD HH:mm:ss`). Restricted to a specific merchant/user combination. Returns consumer ID, email, and active count.

        At least one mode must be specified; omitting both returns an error.
      tags: [Consumers]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: ids
          in: header
          description: Comma-separated consumer IDs to retrieve. Mutually exclusive with `from`/`to`.
          required: false
          schema:
            type: string
            example: "1,2,3"
        - name: from
          in: header
          description: Start date for date-range lookup (`YYYY-MM-DD HH:mm:ss`). Must be used together with `to`.
          required: false
          schema:
            type: string
            example: "2025-01-01 00:00:00"
        - name: to
          in: header
          description: End date for date-range lookup (`YYYY-MM-DD HH:mm:ss`). Must be used together with `from`.
          required: false
          schema:
            type: string
            example: "2025-12-31 23:59:59"
      responses:
        "200":
          description: Consumers retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      consumers:
                        type: object
                        description: Map of consumers keyed by consumer ID
                        additionalProperties:
                          $ref: "#/components/schemas/Consumer"
                    required: [consumers]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Employee ────────────────────────────
  /employee:
    get:
      operationId: getEmployees
      summary: List employees
      description: Retrieve employees of the merchant, including role and date information.
      tags: [Employees]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: employeeIds
          in: header
          description: Comma-separated employee IDs to retrieve. Returns all if omitted.
          required: false
          schema:
            type: string
            example: "1,2,4"
      responses:
        "200":
          description: Employees retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      employees:
                        type: object
                        description: Map of employees keyed by employee ID
                        additionalProperties:
                          $ref: "#/components/schemas/Employee"
                    required: [employees]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: updateEmployees
      summary: Update employees
      description: Update employee details such as role and dates. Supply the employee `id` in the request body.
      tags: [Employees]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateEmployeeRequest"
      responses:
        "201":
          description: Employees updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      employees:
                        type: object
                        description: Map of updated employees keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/EmployeeUpdate"
                    required: [employees]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Invoice ────────────────────────────
  /invoice:
    get:
      operationId: getInvoices
      summary: List invoices
      description: Retrieve merchant invoices, optionally filtered by store or invoice ID.
      tags: [Invoices]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: storeIds
          in: header
          description: Comma-separated store IDs to filter invoices
          required: false
          schema:
            type: string
            example: "53,54,55"
        - name: invoiceId
          in: header
          description: Comma-separated invoice IDs to retrieve
          required: false
          schema:
            type: string
            example: "567,5678"
      responses:
        "200":
          description: Invoices retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      invoices:
                        type: object
                        description: Map of invoices keyed by invoice ID
                        additionalProperties:
                          $ref: "#/components/schemas/Invoice"
                    required: [invoices]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateInvoices
      summary: Create or update invoices
      description: |
        Create new invoices or update existing ones with line items.
        Invoices with product IDs will trigger automatic stock recalculation.
        To update, include the invoice `id` in the request body.
      tags: [Invoices]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInvoiceRequest"
      responses:
        "201":
          description: Invoice created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      invoices:
                        type: object
                        description: Map of invoices keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/InvoiceResponse"
                    required: [invoices]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Log ────────────────────────────
  /log:
    get:
      operationId: getLogs
      summary: Retrieve log entries
      description: Retrieve stored JSON log entries filtered by tag, object type, and identifier.
      tags: [Logs]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: tag
          in: header
          description: Custom tags to filter by
          required: true
          schema:
            type: string
            example: "StockTake, Product"
        - name: object
          in: header
          description: Object type key to filter by
          required: true
          schema:
            type: string
            example: STORE
        - name: identifier
          in: header
          description: Identifier value for the object key
          required: true
          schema:
            type: string
            example: "12"
        - name: id
          in: header
          description: Previously stored log ID for direct retrieval
          required: false
          schema:
            type: string
            example: "1400776796990:1:12:STORE:12"
      responses:
        "200":
          description: Log entries retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      log:
                        type: object
                        description: Map of log entries keyed by log ID
                        additionalProperties:
                          $ref: "#/components/schemas/LogEntry"
                    required: [log]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateLog
      summary: Store a log entry
      description: |
        Store custom JSON data on the server. Data is tagged and keyed by object type
        and identifier for later retrieval. Supply an existing `id` header to update.
      tags: [Logs]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: tag
          in: header
          description: Custom tags for categorisation
          required: true
          schema:
            type: string
            example: "StockTake, Product"
        - name: object
          in: header
          description: Key to store data against
          required: true
          schema:
            type: string
            example: Store
        - name: identifier
          in: header
          description: Value of the key supplied in object
          required: true
          schema:
            type: string
            example: "1"
        - name: id
          in: header
          description: Existing log ID to update
          required: false
          schema:
            type: string
        - name: completed
          in: header
          description: Mark the log entry as completed
          required: false
          schema:
            type: string
            enum: ["true", "false"]
      requestBody:
        required: true
        description: Freeform JSON data to store
        content:
          application/json:
            schema:
              type: object
              description: Any valid JSON object to persist
              example:
                products:
                  "1": 99
                  "2": 44
                  "98": 10
      responses:
        "201":
          description: Log entry created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      log:
                        type: object
                        description: Map of log entries keyed by log ID
                        additionalProperties:
                          $ref: "#/components/schemas/LogEntry"
                    required: [log]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteLog
      summary: Delete a log entry
      description: Delete log records from the merchant account.
      tags: [Logs]
      x-stability: 0
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: id
          in: header
          description: The log entry ID to delete
          required: true
          schema:
            type: string
      responses:
        "205":
          description: Log entry deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Merchant ────────────────────────────
  /merchant:
    get:
      operationId: getMerchant
      summary: Get merchant details
      description: Retrieve the current user's merchant profile including branding, company info, and settings. A user can only have one merchant.
      tags: [Merchants]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      responses:
        "200":
          description: Merchant retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      merchant:
                        $ref: "#/components/schemas/Merchant"
                    required: [merchant]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateMerchant
      summary: Create or update merchant
      description: Create a new merchant or update the existing one. A user may only have one merchant.
      tags: [Merchants]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMerchantRequest"
      responses:
        "201":
          description: Merchant created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      merchant:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/Merchant"
                    required: [merchant]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteMerchant
      summary: Delete merchant
      description: |
        Permanently delete the merchant and all associated data including sales,
        products, and promotions. This action is irreversible.
      tags: [Merchants]
      x-stability: 2
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      responses:
        "205":
          description: Merchant deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Order ────────────────────────────
  /order:
    get:
      operationId: getOrders
      summary: List orders
      description: |
        Retrieve orders for the merchant, optionally filtered by order IDs, store IDs, or completion status.

        Each order in the response now includes its current lifecycle `status`, the full `history` of status/note transitions, and the `deliveryAddress` JSON object (when set).
      tags: [Orders]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: orderIds
          in: header
          description: Comma-separated order IDs to retrieve. Returns all if omitted.
          required: false
          schema:
            type: string
            example: "1,2,4"
        - name: storeIds
          in: header
          description: Comma-separated store IDs to filter by
          required: false
          schema:
            type: string
            example: "10,20"
        - name: completed
          in: header
          description: Filter by completion status
          required: false
          schema:
            type: string
            example: "true"
      responses:
        "200":
          description: Orders retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      orders:
                        type: object
                        description: Map of orders keyed by order ID
                        additionalProperties:
                          $ref: "#/components/schemas/Order"
                    required: [orders]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: completeOrders
      summary: Create / update orders
      description: |
        Create or update one or more orders after basket finalisation.

        ### Updating status, history, and delivery address

        Each order in the request body may carry its own optional `status`, `note`, and/or `deliveryAddress` field. Equivalent `status`, `note`, and `deliveryAddress` request headers can also be supplied as **defaults** that apply to every order in the call.

        - `status` and/or `note` append a new entry to the order's `history` JSON column of the form `{ "status": <status>, "at": <iso-timestamp>, "note": <note> }`. When `status` is supplied it also updates the order's top-level `status` column. When only `note` is supplied, the entry uses the order's current `status` value.
        - `deliveryAddress` (any JSON object, schema-flexible) is written to the order's `deliveryAddress` JSON column.

        Per-order body fields **override** the corresponding request header for that specific order; orders that omit a body field fall back to the header value (if any). All inputs are optional — if none are set, behaviour is unchanged.

        These updates are applied as fire-and-forget side effects after the response is dispatched, so the values returned in the response body reflect the state *prior* to this request's updates.

        ### Customer notification email

        A customer notification email is sent **only** for orders whose effective status applied by this call (per-order body `status`, falling back to the `status` header) is `READY`. Orders with any other effective status — or where this call sets no status at all — do not trigger the email. Other notification channels are unaffected.
      tags: [Orders]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: status
          in: header
          description: Default lifecycle status to record against every order in this call. Overridden by a per-order `status` field in the body.
          required: false
          schema:
            $ref: "#/components/schemas/OrderStatus"
        - name: note
          in: header
          description: Default free-text note (max 50 characters) to record against every order in this call. Overridden by a per-order `note` field in the body.
          required: false
          schema:
            type: string
            maxLength: 50
            example: "walk-in customer"
        - name: deliveryAddress
          in: header
          description: |
            Default JSON-encoded delivery address to attach to every order in this call. Overridden by a per-order `deliveryAddress` field in the body. The schema is intentionally flexible — any JSON object is accepted. See the `DeliveryAddress` schema for the conventional shape.
          required: false
          schema:
            type: string
            example: '{"name":"Julian Smith","line1":"12 Example Street","city":"London","postcode":"SW1A 1AA","countryCode":"GB"}'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CompleteOrderRequest"
      responses:
        "201":
          description: Orders completed
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      orders:
                        type: object
                        description: Map of completed orders keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/OrderResponse"
                    required: [orders]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Product ────────────────────────────
  /product:
    get:
      operationId: getProducts
      summary: List products
      description: |
        Retrieve products with full details including instances (variants), pricing,
        stock levels, categories, and images. Supports filtering by product IDs,
        store ID (for stock), and category IDs.
      tags: [Products]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: productIds
          in: header
          description: Comma-separated product IDs to retrieve
          required: false
          schema:
            type: string
            example: "1,2,3"
        - name: storeId
          in: header
          description: Store ID to include stock levels for
          required: false
          schema:
            type: string
            example: "1"
        - name: categoryIds
          in: header
          description: Comma-separated category IDs to filter by
          required: false
          schema:
            type: string
            example: "1,2,3"
      responses:
        "200":
          description: Products retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      products:
                        type: object
                        description: Map of products keyed by product ID
                        additionalProperties:
                          $ref: "#/components/schemas/Product"
                    required: [products]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateProducts
      summary: Create or update products
      description: |
        Create new products or update existing ones with instances (variants),
        categories, and images. To update, include the product `id`.

        **Note:** Product instances cannot be updated — they are stored for historical
        reference. To change pricing, remove the old instance and create a new one.
      tags: [Products]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProductRequest"
      responses:
        "201":
          description: Products created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      products:
                        type: object
                        description: Map of products keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/Product"
                    required: [products]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteProduct
      summary: Delete a product
      description: Permanently delete a product by ID.
      tags: [Products]
      x-stability: 2
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: productId
          in: header
          description: The product ID to delete
          required: true
          schema:
            type: string
            example: "1"
      responses:
        "205":
          description: Product deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Product Component ────────────────────────────
  /product/component:
    get:
      operationId: getProductComponents
      summary: List product components
      description: Retrieve product components (sub-item groups) with their product options and instances.
      tags: [Product Components]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      responses:
        "200":
          description: Product components retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      productComponents:
                        type: object
                        description: Map of product components keyed by component ID
                        additionalProperties:
                          $ref: "#/components/schemas/ProductComponent"
                    required: [productComponents]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateProductComponents
      summary: Create or update product components
      description: Create new product components or update existing ones. To update, include the component `id`.
      tags: [Product Components]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProductComponentRequest"
      responses:
        "201":
          description: Product component created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      productComponents:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/ProductComponent"
                    required: [productComponents]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteProductComponents
      summary: Delete product components
      description: |
        Delete product components. Supply `productComponentIds` to delete entire groups,
        or both `productComponentIds` and `productComponentInstanceIds` to delete specific items.
      tags: [Product Components]
      x-stability: 2
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: productComponentIds
          in: header
          description: Comma-separated component IDs to delete
          required: true
          schema:
            type: string
            example: "1,2,3"
        - name: productComponentInstanceIds
          in: header
          description: Comma-separated instance IDs to delete (within the specified components)
          required: false
          schema:
            type: string
            example: "1,2,3"
      responses:
        "205":
          description: Product component(s) deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Product Tags ────────────────────────────
  /product/tags:
    get:
      operationId: getProductTags
      summary: List product tags
      description: Retrieve all unique tags across the merchant's product catalogue. Internal SKU tags are excluded.
      tags: [Product Tags]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      responses:
        "200":
          description: Tags retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      tags:
                        type: array
                        description: Flat array of unique tag strings
                        items:
                          type: string
                        example: ["Drinks", "Hot", "Cold", "Vegan", "Seasonal"]
                    required: [tags]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /product/instance/tags:
    get:
      operationId: getProductInstanceTags
      summary: List product instance tags
      description: Retrieve all unique tags across the merchant's product instances (variants). Internal SKU tags are excluded.
      tags: [Product Tags]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      responses:
        "200":
          description: Instance tags retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      tags:
                        type: array
                        description: Flat array of unique instance tag strings
                        items:
                          type: string
                        example: ["Small", "Medium", "Large", "Red", "Blue"]
                    required: [tags]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Promotion ────────────────────────────
  /promotion:
    get:
      operationId: getPromotions
      summary: List promotions
      description: Retrieve merchant promotions with optional filtering by store, status, or ID.
      tags: [Promotions]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: storeIds
          in: header
          description: Comma-separated store IDs to filter by
          required: false
          schema:
            type: string
            example: "1,2,4"
        - name: promotionIds
          in: header
          description: Comma-separated promotion IDs to retrieve
          required: false
          schema:
            type: string
            example: "1,2,4"
        - name: imminent
          in: header
          description: Filter for promotions starting soon
          required: false
          schema:
            type: string
            enum: ["true", "false"]
        - name: expired
          in: header
          description: Filter for expired promotions
          required: false
          schema:
            type: string
            enum: ["true", "false"]
        - name: active
          in: header
          description: Filter for currently active promotions
          required: false
          schema:
            type: string
            enum: ["true", "false"]
      responses:
        "200":
          description: Promotions retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      promotions:
                        type: object
                        description: Map of promotions keyed by promotion ID
                        additionalProperties:
                          $ref: "#/components/schemas/Promotion"
                    required: [promotions]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdatePromotions
      summary: Create or update promotions
      description: Create new promotions or update existing ones with store, category, and product associations.
      tags: [Promotions]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePromotionRequest"
      responses:
        "201":
          description: Promotion created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      promotions:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/PromotionResponse"
                    required: [promotions]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /promotion/code:
    get:
      operationId: getPromotionCode
      summary: Generate a promotion code
      description: |
        Generate a unique promotion code for a given promotion.
        Optionally associate the code with a consumer by providing their email.
        If the consumer does not exist, they will be created.
      tags: [Promotions]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: promotionId
          in: header
          description: The promotion ID to generate a code for
          required: true
          schema:
            type: string
            example: "55"
        - name: email
          in: header
          description: Consumer email to associate the code with
          required: false
          schema:
            type: string
            example: customer@example.com
        - name: firstName
          in: header
          description: Consumer first name (used when creating a new consumer)
          required: false
          schema:
            type: string
        - name: lastName
          in: header
          description: Consumer last name
          required: false
          schema:
            type: string
        - name: dob
          in: header
          description: Consumer date of birth
          required: false
          schema:
            type: string
            format: date
            example: "1990-01-15"
        - name: gender
          in: header
          description: Consumer gender
          required: false
          schema:
            type: string
            example: male
      responses:
        "200":
          description: Promotion code generated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      promotionCode:
                        $ref: "#/components/schemas/PromotionCode"
                    required: [promotionCode]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Report ────────────────────────────
  /report:
    get:
      operationId: getReport
      summary: Run a report query
      description: |
        Execute a SWQL (Shopwave Query Language) report query to aggregate basket,
        transaction, and product data. Results are restricted to stores the
        authenticated user has access to.

        The response uses a compact format with `keys` describing the data structure
        and `data` as arrays for reduced payload size.
      tags: [Reports]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: query
          in: header
          description: SWQL query as a JSON string with FROM, WHERE, optional SELECT and GROUP clauses
          required: true
          schema:
            type: string
            example: '{"0":{"FROM":"Basket","WHERE":{"AND":["Basket.completeDate > ''2017-01-21 00:00:00''","Basket.completeDate < ''2017-01-21 23:00:00''"]}}}'
        - name: data
          in: header
          description: Optional pre-fetched basket data as JSON to run the report against instead of the database
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Report data retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      keys:
                        type: array
                        description: Describes the structure and column types of the data array
                        items:
                          type: object
                      data:
                        type: array
                        description: Result rows as arrays (compact format)
                        items:
                          type: array
                          items: {}
                    required: [keys, data]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /report/basket:
    get:
      operationId: getBasketReport
      summary: Get basket report
      description: Retrieve aggregated basket reports with transaction and product data for completed baskets in a given timeframe.
      tags: [Reports]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: reportFrom
          in: header
          description: Report start date-time
          required: true
          schema:
            type: string
            format: date-time
            example: "2015-01-01 00:00:00"
        - name: reportTo
          in: header
          description: Report end date-time
          required: true
          schema:
            type: string
            format: date-time
            example: "2015-01-10 00:00:00"
        - name: storeIds
          in: header
          description: Comma-separated store IDs to filter by
          required: false
          schema:
            type: string
            example: "1,2,3"
      responses:
        "200":
          description: Basket report retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      basketReports:
                        type: object
                        description: Map of basket reports keyed by basket ID
                        additionalProperties:
                          $ref: "#/components/schemas/BasketReport"
                      cat:
                        type: object
                        description: Map of categories referenced in the report
                        additionalProperties:
                          $ref: "#/components/schemas/ReportCategory"
                    required: [basketReports]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /report/stock:
    get:
      operationId: getStockReport
      summary: Get stock report
      description: |
        Retrieve a stock report for a given store, including product details,
        pricing, supplier information, and current stock levels. Optionally
        filter by product IDs. Available from API version 0.7 onwards.
      tags: [Reports]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: storeId
          in: header
          description: The store ID to report on.
          required: true
          schema:
            type: string
            example: "1"
        - name: productIds
          in: header
          description: Comma-separated product IDs to filter. Returns all products if omitted.
          required: false
          schema:
            type: string
            example: "10,11,12"
      responses:
        "200":
          description: Stock report retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      stockReports:
                        type: object
                        description: Map of stock report entries keyed by product ID
                        additionalProperties:
                          $ref: "#/components/schemas/StockReportEntry"
                    required: [stockReports]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Stock ────────────────────────────
  /stock:
    get:
      operationId: getStock
      summary: Get stock levels
      description: Retrieve current stock levels for specified products across specified stores.
      tags: [Stock]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: productIds
          in: header
          description: Comma-separated product IDs to retrieve stock for.
          required: true
          schema:
            type: string
            example: "10,11,12"
        - name: storeIds
          in: header
          description: Comma-separated store IDs to retrieve stock for.
          required: true
          schema:
            type: string
            example: "1,2"
      responses:
        "200":
          description: Stock levels retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      stock:
                        type: object
                        description: Stock data keyed by product/store combination
                    required: [stock]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Stock Reconcile ────────────────────────────
  /stock/reconcile:
    get:
      operationId: getStockReconcile
      summary: Get stock reconciliations
      description: Retrieve wastage and surplus stock reconciliation records within a date range.
      tags: [Stock]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: from
          in: header
          description: Start date for the reconciliation period
          required: true
          schema:
            type: string
            format: date-time
            example: "2015-01-01 00:00:00"
        - name: to
          in: header
          description: End date for the reconciliation period
          required: true
          schema:
            type: string
            format: date-time
            example: "2015-05-01 00:00:00"
        - name: storeIds
          in: header
          description: Comma-separated store IDs
          required: true
          schema:
            type: string
            example: "53,54,55"
      responses:
        "200":
          description: Stock reconciliation data retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      stockReconcile:
                        $ref: "#/components/schemas/StockReconcileResponse"
                    required: [stockReconcile]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createStockReconcile
      summary: Submit stock reconciliation
      description: |
        Submit stock discrepancies from a stock take. Negative quantities represent
        wastage (creates a wastage basket), positive quantities represent surplus
        (creates an invoice). Automatically recalculates stock levels.
      tags: [Stock]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateStockReconcileRequest"
      responses:
        "201":
          description: Stock reconciliation processed
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      baskets:
                        type: object
                        description: Wastage baskets created (for negative quantities)
                        additionalProperties:
                          $ref: "#/components/schemas/BasketResponse"
                      invoices:
                        type: object
                        description: Invoices created (for positive quantities / surplus)
                        additionalProperties:
                          $ref: "#/components/schemas/InvoiceResponse"
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Store ────────────────────────────
  /store:
    get:
      operationId: getStores
      summary: List stores
      description: Retrieve merchant store locations with addresses and geo-coordinates.
      tags: [Stores]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: storeIds
          in: header
          description: Comma-separated store IDs to retrieve. Returns all if omitted.
          required: false
          schema:
            type: string
            example: "1,2,3"
      responses:
        "200":
          description: Stores retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      stores:
                        type: object
                        description: Map of stores keyed by store ID
                        additionalProperties:
                          $ref: "#/components/schemas/Store"
                    required: [stores]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateStores
      summary: Create or update stores
      description: Create new store locations or update existing ones. To update, include the store `id`.
      tags: [Stores]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateStoreRequest"
      responses:
        "201":
          description: Store created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      stores:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/Store"
                    required: [stores]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteStore
      summary: Delete a store
      description: Permanently delete a store location.
      tags: [Stores]
      x-stability: 2
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: storeId
          in: header
          description: The store ID to delete
          required: true
          schema:
            type: string
            example: "1"
      responses:
        "205":
          description: Store deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Supplier ────────────────────────────
  /supplier:
    get:
      operationId: getSuppliers
      summary: List suppliers
      description: Retrieve supplier profiles for the merchant.
      tags: [Suppliers]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      responses:
        "200":
          description: Suppliers retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      suppliers:
                        type: object
                        description: Map of suppliers keyed by supplier ID
                        additionalProperties:
                          $ref: "#/components/schemas/Supplier"
                    required: [suppliers]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateSuppliers
      summary: Create or update suppliers
      description: Create new suppliers or update existing ones. To update, include the supplier `id`.
      tags: [Suppliers]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSupplierRequest"
      responses:
        "201":
          description: Supplier created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      suppliers:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/Supplier"
                    required: [suppliers]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteSupplier
      summary: Delete a supplier
      description: Permanently delete a supplier by ID.
      tags: [Suppliers]
      x-stability: 2
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: supplierId
          in: header
          description: The supplier ID to delete
          required: true
          schema:
            type: string
            example: "2"
      responses:
        "205":
          description: Supplier deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Supplier Store ────────────────────────────
  /supplier/store:
    get:
      operationId: getSupplierStores
      summary: List supplier stores
      description: Retrieve supplier store locations, optionally filtered by supplier IDs.
      tags: [Suppliers]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: supplierIds
          in: header
          description: Comma-separated supplier IDs to filter by
          required: false
          schema:
            type: string
            example: "1,2,3"
      responses:
        "200":
          description: Supplier stores retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      supplierStores:
                        type: object
                        description: Map of supplier stores keyed by store ID
                        additionalProperties:
                          $ref: "#/components/schemas/SupplierStore"
                    required: [supplierStores]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createOrUpdateSupplierStores
      summary: Create or update supplier stores
      description: Create new supplier store locations or update existing ones.
      tags: [Suppliers]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSupplierStoreRequest"
      responses:
        "201":
          description: Supplier store created or updated
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      supplierStores:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/SupplierStore"
                    required: [supplierStores]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    delete:
      operationId: deleteSupplierStore
      summary: Delete a supplier store
      description: Permanently delete a supplier store location.
      tags: [Suppliers]
      x-stability: 2
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: supplierId
          in: header
          description: The supplier ID
          required: true
          schema:
            type: string
            example: "1"
        - name: supplierstoreIds
          in: header
          description: The supplier store IDs to delete
          required: true
          schema:
            type: string
            example: "1"
      responses:
        "205":
          description: Supplier store deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Transaction ────────────────────────────
  /transaction:
    get:
      operationId: getTransactions
      summary: List transactions
      description: |
        Retrieve transactions for the authenticated merchant's stores, filtered by basket IDs
        and optionally by transaction IDs. Available from API version 0.6 onwards.
      tags: [Transactions]
      x-stability: 1
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: basketIds
          in: header
          description: Comma-separated basket IDs whose transactions to retrieve.
          required: true
          schema:
            type: string
            example: "100,101,102"
        - name: transactionIds
          in: header
          description: Comma-separated transaction IDs to filter results. If omitted, returns all transactions for the given baskets.
          required: false
          schema:
            type: string
            example: "50,51"
      responses:
        "200":
          description: Transactions retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      transactions:
                        type: object
                        description: Map of transactions keyed by transaction ID
                        additionalProperties:
                          $ref: "#/components/schemas/Transaction"
                    required: [transactions]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createTransaction
      summary: Create a transaction
      description: |
        Create a new payment transaction attached to a basket.
        This generates a transaction ID which can then be completed via PUT.
        Transactions should be created between basket Part 1 and Part 2.
      tags: [Transactions]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTransactionRequest"
      responses:
        "201":
          description: Transaction created
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      transactions:
                        type: object
                        description: Map of transactions keyed by object reference
                        additionalProperties:
                          $ref: "#/components/schemas/Transaction"
                    required: [transactions]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

    put:
      operationId: completeTransaction
      summary: Complete a transaction
      description: |
        Finalise a transaction, which disables all further modifications.
        Set `completed` to true or provide a value to mark as complete.
      tags: [Transactions]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CompleteTransactionRequest"
      responses:
        "201":
          description: Transaction completed
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      transactions:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/Transaction"
                    required: [transactions]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── Uploader ────────────────────────────
  /uploader:
    put:
      operationId: uploadFile
      summary: Upload a media file
      description: |
        Upload a single media file to the Shopwave CDN. Returns the generated
        filename and public URL. Supported content types include user profile pictures,
        application logos, application screenshots, merchant images, and product images.
      tags: [Uploader]
      x-stability: 2
      security:
        - oauth2: [write]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
        - name: contentType
          in: header
          description: |
            Type of content being uploaded. Determines image dimensions and storage path.
            Valid values: user (200x200), applicaionLogo (200x200), applicaionImages (480x351),
            merchant (576x325), product (576x325).
          required: true
          schema:
            type: string
            enum: [user, applicaionLogo, applicaionImages, merchant, product]
            example: product
        - name: prefix
          in: header
          description: Optional filename prefix (alphanumeric, hyphens, and underscores only)
          required: false
          schema:
            type: string
            example: my-product
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: The file to upload
              required: [file]
      responses:
        "201":
          description: File uploaded successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      fileName:
                        type: string
                        description: Generated unique filename
                        example: my-product_c3499c2729730a7f807efb8676a92dcb6f8a3f8f.jpg
                      path:
                        type: string
                        description: Full public CDN URL
                        example: "http://static.merchantstack.com/images/merchant/11/product/my-product_c3499c2729730a7f807efb8676a92dcb6f8a3f8f.jpg"
                    required: [fileName, path]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "415":
          description: Unsupported media type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────── User ────────────────────────────
  /user:
    get:
      operationId: getUser
      summary: Get current user
      description: Retrieve the authenticated user's profile and employee details.
      tags: [Users]
      x-stability: 2
      security:
        - oauth2: [read]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      responses:
        "200":
          description: User retrieved
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      user:
                        $ref: "#/components/schemas/User"
                    required: [user]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      operationId: createUser
      summary: Create a user
      description: Create a new user and invite them to the channel.
      tags: [Users]
      x-stability: 0
      security:
        - oauth2: [admin]
      parameters:
        - $ref: "#/components/parameters/AcceptVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      user:
                        $ref: "#/components/schemas/User"
                    required: [user]
                  - $ref: "#/components/schemas/ApiEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"

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

  # ──────── Security Schemes ────────
  securitySchemes:
    oauth2:
      type: oauth2
      description: Shopwave Auth OAuth 2.0 authorization code flow
      x-default-scopes:
        - user
        - application
        - merchant
        - store
        - product
        - category
        - basket
        - promotion
      flows:
        authorizationCode:
          authorizationUrl: https://secure.dev.merchantstack.com/oauth/authorize
          tokenUrl: https://secure.dev.merchantstack.com/oauth/token
          x-scalar-client-id: e58f76e24c275d275d3581b8ad1b8be1689dfa85
          x-scalar-credentials-location: body
          scopes:
            user: Access to user resources
            application: Access to application resources
            merchant: Access to merchant resources
            store: Access to store resources
            product: Access to product resources
            category: Access to category resources
            basket: Access to basket resources
            promotion: Access to promotion resources

  # ──────── Shared Parameters ────────
  parameters:
    AcceptVersion:
      name: x-accept-version
      in: header
      description: API version number. Recommended to always send the latest version.
      required: true
      schema:
        type: string
        default: "2.0"
        example: "2.0"
    ApplyPromotionOriginalPriceOnly:
      name: x-apply-promotion-original-price-only
      in: header
      description: |
        When `true` (or `1`), promotions are only applied to basket products that retain their
        original catalog price. Any product with a manually adjusted `price` on the basket
        request is excluded from the promotion engine entirely — it does not count toward
        quantity thresholds and does not receive any discount. Default off; omit the header
        or send any other value to preserve existing behaviour where all basket products are
        eligible regardless of manual price overrides.
      required: false
      schema:
        type: string
        enum: ["true", "1", "false", "0"]
        example: "true"

  # ──────── Shared Response Headers ────────
  headers:
    X-Api-Version:
      description: The API version used to process the request
      schema:
        type: string
        example: "2.0"

  # ──────── Shared Responses ────────
  responses:
    BadRequest:
      description: The request body is malformed or contains invalid data
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Authentication is missing or the token is invalid/expired
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: The authenticated user does not have permission to perform this action
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: The requested resource was not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    UnprocessableEntity:
      description: The request was well-formed but contains semantic errors (e.g. missing required fields)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InternalServerError:
      description: An unexpected server error occurred
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

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

    # ─── Error ───
    Error:
      type: object
      description: A structured error returned by the Shopwave API
      required: [code, message]
      properties:
        code:
          type: integer
          description: HTTP status code or application-specific error code
          example: 400
        message:
          type: string
          description: Human-readable error summary
          example: Validation failed
        details:
          type: array
          description: Specific error details (e.g. per-field validation messages)
          items:
            type: string
          example: ["name is required", "storeId must be an integer"]

    ErrorResponse:
      type: object
      description: Standard error response wrapper
      properties:
        error:
          $ref: "#/components/schemas/Error"
        api:
          type: object
          description: API processing metadata (may be present even on errors)
          properties:
            message:
              type: object
              properties:
                error:
                  type: object
                  description: Map of error messages keyed by error ID
                  additionalProperties:
                    $ref: "#/components/schemas/ApiMessage"
            codeBaseVersion:
              type: string
              description: Server codebase version
            executionTime_milliSeconds:
              type: integer
              description: Server-side execution time in milliseconds
          required: [message]
      required: [error]

    # ─── API Message (internal envelope message) ───
    ApiMessage:
      type: object
      description: A single success or error message within the API response envelope
      properties:
        id:
          type: integer
          description: Unique message identifier
          example: 202
        code:
          type: string
          description: Machine-readable message code
          example: tokenValid
        statusCode:
          type: integer
          description: Associated HTTP status code
          example: 200
        title:
          type: string
          description: Short human-readable title
          example: Token is valid
        details:
          type: string
          description: Detailed explanation
          example: Token is validated and found valid.
        moreInfo:
          type: string
          description: Additional context or debugging information
        objectRef:
          type: string
          description: Reference to the specific object this message relates to
      required: [id, title, details]

    # ─── API Envelope ───
    ApiEnvelope:
      type: object
      description: Standard API response metadata included in every response
      properties:
        api:
          type: object
          properties:
            message:
              type: object
              properties:
                success:
                  type: object
                  description: Map of success messages keyed by message ID
                  additionalProperties:
                    $ref: "#/components/schemas/ApiMessage"
                error:
                  type: object
                  description: Map of error messages keyed by error ID (present when partial failures occur)
                  additionalProperties:
                    $ref: "#/components/schemas/ApiMessage"
              required: [success]
            codeBaseVersion:
              type: string
              description: Server codebase version number
              example: "2.0"
            executionTime_milliSeconds:
              type: integer
              description: Server-side execution time in milliseconds (excludes network latency)
              example: 29
          required: [message, codeBaseVersion, executionTime_milliSeconds]
      required: [api]

    # ─── Application ───
    Application:
      type: object
      description: A registered Shopwave OAuth application
      properties:
        applicationId:
          type: integer
          description: Unique application identifier
          example: 2
        identifier:
          type: string
          description: Encrypted 40-character key to uniquely identify the application
          example: 932ef614dbfb31ccfe129f29e9ce03732cfcc0ec
        secret:
          type: string
          description: Encrypted 40-character secret key for token requests
          example: 1cede75aba205077387d33483c1aafe71b739fc7
        applicationName:
          type: string
          description: Display name of the application
          example: Ace Accounting
        applicationDesc:
          type: string
          description: Short description of the application
          example: An accounting software which takes care of your tax returns.
        url:
          type: array
          description: Redirect URLs — index 0 is live, 1 is staging, 2 is development
          items:
            type: string
          example: ["http://aceaccounting.com/callback", "http://staging.aceaccounting.com/callback"]
        approvedDate:
          type: string
          format: date-time
          description: Date the application was approved
          example: "2013-05-28T14:15:23.000Z"
        trustedPartner:
          type: integer
          description: "Set by Shopwave — 1 skips access grant check, 0 requires it"
          enum: [0, 1]
          example: 0
        sandbox:
          type: integer
          description: "1 for sandbox mode (shows developer errors), 0 for live"
          enum: [0, 1]
          example: 1
        logoIds:
          type: array
          description: Application logo image URLs
          items:
            type: string
          example: ["http://static.merchantstack.com/images/application/images/logo.jpg"]
        imageIds:
          type: array
          description: Application screenshot and image URLs
          items:
            type: string
      required: [applicationId, identifier, secret, applicationName]

    CreateApplicationRequest:
      type: object
      description: Request body for creating or updating applications
      properties:
        applications:
          type: object
          description: Map of application objects keyed by object reference
          additionalProperties:
            type: object
            properties:
              name:
                type: string
                description: Application name
                example: Ace Accounting
              description:
                type: string
                description: Short description of the application
                example: An accounting software which takes care of your tax returns.
              url:
                type: string
                description: Comma-separated redirect URLs (live, staging, development)
                example: "http://aceaccounting.com/callback, http://staging.aceaccounting.com/callback"
              sandbox:
                type: integer
                description: "1 for sandbox mode, 0 for live"
                enum: [0, 1]
                example: 1
            required: [name, url]
      required: [applications]

    ApplicationResponse:
      type: object
      description: Application details returned after creation or update
      properties:
        id:
          type: integer
          description: Unique application ID
          example: 2
        identifier:
          type: string
          description: Generated application identifier
          example: bf4003228076bec022ab82cde8f5efe1d39a4a32
        secret:
          type: string
          description: Generated application secret
          example: 474d5494f4bd8eb71c3b70aa3578a6629e7b5e94
        name:
          type: string
          description: Application name
          example: Ace Accounting
        description:
          type: string
          description: Application description
          example: An accounting software which takes care of your tax returns.
        url:
          type: string
          description: Redirect URLs
        sandbox:
          type: integer
          description: Sandbox mode flag
          enum: [0, 1]
      required: [id, identifier, secret, name]

    # ─── Basket ───
    BasketProduct:
      type: object
      description: A product line item within a basket
      properties:
        id:
          type: integer
          description: Unique basket product ID (generated on creation)
          example: 51859392
        productInstanceId:
          type: integer
          description: Product instance (variant) ID
          example: 146611
        quantity:
          type: integer
          description: Number of units
          example: 1
        price:
          type: string
          description: Price in pence (null if using instance default price)
          example: "1250.0"
        note:
          type: string
          description: Note for this line item
          example: "Tax 10"
        taxPercentage:
          type: number
          description: Tax percentage as a decimal (e.g. 0.1 for 10%)
          example: 0.1
        addedDate:
          type: string
          format: date-time
          description: Date the product was added to the basket
          example: "2017-11-28T15:22:07.000Z"
      required: [productInstanceId, quantity]

    Basket:
      type: object
      description: A shopping basket containing products and optional promotions
      properties:
        id:
          type: integer
          description: Unique basket ID
          example: 16339951
        storeId:
          type: integer
          description: Store ID this basket belongs to
          example: 2890
        consumerId:
          type: integer
          description: Consumer ID associated with the basket
          example: null
        name:
          type: string
          description: Basket display name
          example: Basket 68
        note:
          type: string
          description: Additional notes
        completeDate:
          type: string
          format: date-time
          description: Date the basket was completed (null if still open)
          example: "2017-11-28T15:22:14.000Z"
        basketProducts:
          type: object
          description: Map of basket products keyed by reference ID
          additionalProperties:
            $ref: "#/components/schemas/BasketProduct"
      required: [id, storeId]

    CreateBasketRequest:
      type: object
      properties:
        baskets:
          type: object
          additionalProperties:
            type: object
            properties:
              storeId:
                type: integer
                description: Store ID to create the basket in
                example: 2890
            required: [storeId]
      required: [baskets]

    UpdateBasketRequest:
      type: object
      description: Request body for basket PUT (Part 1 or Part 2)
      properties:
        baskets:
          type: object
          additionalProperties:
            type: object
            properties:
              storeId:
                type: integer
                description: Store ID
                example: 2890
              basketId:
                type: integer
                description: Existing basket ID (required for Part 2)
                example: 16339951
              consumerId:
                type: integer
                description: Consumer ID to associate
              basketName:
                type: string
                description: Basket display name
                example: Basket 68
              note:
                type: string
                description: Additional notes
              completed:
                type: boolean
                description: Set to true to finalise the basket (Part 2)
                example: true
              basketProducts:
                type: object
                description: Map of products keyed by reference ID
                additionalProperties:
                  type: object
                  properties:
                    productInstanceId:
                      type: integer
                      description: Product instance ID
                      example: 146611
                    quantity:
                      type: integer
                      description: Number of units
                      example: 1
                    price:
                      type: string
                      description: Override price in pence
                    note:
                      type: string
                      description: Line item note
                    taxPercentage:
                      type: number
                      description: Tax percentage decimal
                      example: 0.1
                  required: [productInstanceId, quantity]
            required: [storeId]
      required: [baskets]

    BasketResponse:
      type: object
      description: Basket returned in responses (creation, update, reconciliation)
      properties:
        id:
          type: integer
          description: Unique basket ID
          example: 16339951
        storeId:
          type: integer
          description: Store ID
          example: 2890
        consumerId:
          type: integer
          description: Consumer ID
        name:
          type: string
          description: Basket name
          example: Basket 68
        completeDate:
          type: string
          format: date-time
          description: Completion timestamp (null if not completed)
        basketProducts:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/BasketProduct"
      required: [id, storeId]

    # ─── Category ───
    Category:
      type: object
      description: A product category for organising merchant inventory
      properties:
        id:
          type: integer
          description: Unique category ID
          example: 921
        title:
          type: string
          description: Category display name
          example: Hot Drinks
        parentId:
          type: integer
          description: Parent category ID for hierarchical categories (null for top-level)
          example: null
        activeDate:
          type: string
          format: date-time
          description: Date the category became active (null if inactive)
          example: "2014-03-21T11:19:39.000Z"
        deleteDate:
          type: string
          format: date-time
          description: Date the category was deleted (null if not deleted)
          example: null
        tags:
          type: string
          description: Comma-separated category tags
          example: ""
        images:
          type: array
          description: Category image URLs
          items:
            type: string
          example: ["http://static.merchantstack.com/images/merchant/4511/category/image.jpg"]
      required: [id, title]

    CreateCategoryRequest:
      type: object
      properties:
        categories:
          type: object
          additionalProperties:
            type: object
            properties:
              id:
                type: integer
                description: Category ID (include to update an existing category)
              title:
                type: string
                description: Category name
                example: Hot Drinks
              parentId:
                type: integer
                description: Parent category ID
              activeDate:
                type: string
                format: date-time
                description: Date the category should become active
              tags:
                type: string
                description: Comma-separated tags
              images:
                type: array
                description: Image filenames (uploaded via /uploader)
                items:
                  type: string
            required: [title]
      required: [categories]

    # ─── Consumer ───
    Consumer:
      type: object
      description: A consumer profile associated with a merchant
      properties:
        id:
          type: integer
          description: Unique consumer ID
          example: 42
        firstName:
          type: string
          description: Consumer first name
          example: Jane
        lastName:
          type: string
          description: Consumer last name (may be abbreviated to initial for restricted roles)
          example: Doe
        email:
          type: string
          description: Consumer email address (may be partially masked for restricted roles)
          example: jane@example.com
        activeCount:
          type: integer
          description: Number of active records associated with this consumer
          example: 3
      required: [id, firstName]

    # ─── Employee ───
    Employee:
      type: object
      description: An employee of the merchant
      properties:
        id:
          type: integer
          description: Unique employee ID
          example: 3
        firstName:
          type: string
          description: First name
          example: Karthik
        lastName:
          type: string
          description: Last name
          example: Vasudevan
        email:
          type: string
          format: email
          description: Email address
          example: karthik@example.com
        roleId:
          type: integer
          description: "Employee role ID: 1 = Owner, 2 = Manager, 3 = Assistant, 4 = Guest, 5 = Assistant Manager"
          enum: [1, 2, 3, 4, 5]
          example: 1
        joinedDate:
          type: string
          format: date-time
          description: Date the employee joined
          example: "2013-05-04T00:00:00.000Z"
        exitDate:
          type: string
          format: date-time
          description: Date the employee left (null if still active)
          example: null
      required: [id, firstName, lastName, email, roleId]

    EmployeeUpdate:
      type: object
      description: Employee update fields
      properties:
        id:
          type: integer
          description: Employee ID to update
          example: 3
        roleId:
          type: integer
          description: "New role ID: 1 = Owner, 2 = Manager, 3 = Assistant, 4 = Guest, 5 = Assistant Manager"
          enum: [1, 2, 3, 4, 5]
          example: 1
        joinedDate:
          type: string
          format: date-time
          description: Joined date
        exitDate:
          type: string
          format: date-time
          description: Exit date
      required: [id]

    UpdateEmployeeRequest:
      type: object
      properties:
        employees:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/EmployeeUpdate"
      required: [employees]

    # ─── Invoice ───
    InvoiceItem:
      type: object
      description: A line item in a purchase invoice
      properties:
        id:
          type: integer
          description: Unique invoice item ID (generated on creation)
          example: 5093
        invoiceId:
          type: integer
          description: Parent invoice ID
          example: 848
        barcode:
          type: string
          description: Product barcode
          example: "56789"
        name:
          type: string
          description: Item name
          example: Hibiscus
        note:
          type: string
          description: Item note
        price:
          type: string
          description: Item price
          example: "91.7"
        vatPercentage:
          type: number
          description: VAT percentage as a decimal
          example: 0.2
        quantity:
          type: integer
          description: Item quantity
          example: 10
        productId:
          type: integer
          description: Associated product ID (triggers stock recalculation)
          example: 58
        accountId:
          type: integer
          description: Accounting category ID
          example: 80
      required: [name, price, quantity]

    Invoice:
      type: object
      description: A purchase invoice from a supplier
      properties:
        id:
          type: integer
          description: Unique invoice ID
          example: 86
        supplierId:
          type: integer
          description: Supplier ID
          example: 106
        supplierName:
          type: string
          description: Supplier name
          example: Coffee Warehouse
        storeId:
          type: integer
          description: Merchant store ID receiving the goods
          example: 53
        supplierStoreId:
          type: integer
          description: Supplier store ID
          example: 150
        reference:
          type: string
          description: Supplier invoice reference number
          example: INV123123
        addedDate:
          type: string
          format: date-time
          description: Date the invoice was added
          example: "2014-06-17T15:04:25.000Z"
        completeDate:
          type: string
          format: date-time
          description: Date the invoice was completed
          example: "2014-06-17T15:05:26.000Z"
        note:
          type: string
          description: Invoice notes
          example: new orders
        totalQuantity:
          type: integer
          description: Total quantity of all items
          example: 150
        totalPrice:
          type: string
          description: Total invoice price
          example: "105"
        totalVat:
          type: string
          description: Total VAT amount
          example: "20"
        totalIncVat:
          type: string
          description: Total price including VAT
          example: "125"
      required: [id, storeId]

    CreateInvoiceRequest:
      type: object
      properties:
        invoices:
          type: object
          additionalProperties:
            type: object
            properties:
              storeId:
                type: integer
                description: Merchant store ID
                example: 1
              supplierId:
                type: integer
                description: Supplier ID
              supplierStoreId:
                type: integer
                description: Supplier store ID
                example: 122
              reference:
                type: string
                description: Supplier invoice reference
                example: INV78340911
              completed:
                type: boolean
                description: Mark as completed
              note:
                type: string
                description: Invoice notes
              invoiceItems:
                type: object
                additionalProperties:
                  $ref: "#/components/schemas/InvoiceItem"
            required: [storeId]
      required: [invoices]

    InvoiceResponse:
      type: object
      description: Invoice returned after creation
      properties:
        id:
          type: integer
          description: Invoice ID
          example: 848
        storeId:
          type: integer
          description: Store ID
        supplierStoreId:
          type: integer
          description: Supplier store ID
        reference:
          type: string
          description: Invoice reference
        completeDate:
          type: string
          format: date-time
          description: Completion date
        note:
          type: string
          description: Invoice notes
        invoiceItems:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/InvoiceItem"
      required: [id]

    # ─── Log ───
    LogEntry:
      type: object
      description: A stored JSON log entry
      properties:
        id:
          type: string
          description: Unique composite log ID
          example: "1400776796990:1:12:STORE:12"
        tag:
          type: string
          description: Tags for categorisation
          example: "StockTake, Product"
        object:
          type: string
          description: Object type key
          example: STORE
        identifier:
          type: string
          description: Object identifier value
          example: "12"
        value:
          type: object
          description: Stored custom JSON data
        addedDate:
          type: string
          format: date-time
          description: Date the log was created
          example: "2014-05-22T16:39:56.989Z"
        completeDate:
          type: string
          format: date-time
          description: Date the log was marked as completed
          example: "2014-05-22T16:39:56.000Z"
      required: [id, tag, object, identifier]

    # ─── Merchant ───
    MerchantImageIds:
      type: object
      description: Merchant branding images
      properties:
        logo:
          type: object
          properties:
            receipt:
              type: string
              description: Logo URL for receipts
              example: "http://static.merchantstack.com/images/merchant/1/logo/receipt.jpg"
            square:
              type: string
              description: Square logo URL
              example: "http://static.merchantstack.com/images/merchant/1/logo/square.jpg"
        feature:
          type: array
          description: Feature images
          items:
            type: string

    MerchantColours:
      type: object
      description: Merchant branding colour scheme
      properties:
        primary:
          type: object
          properties:
            main:
              type: string
              description: Primary colour hex code
              example: "777777"
            highlight:
              type: string
              description: Highlight colour hex code
              example: "333333"
            contrast:
              type: string
              description: Contrast colour hex code
              example: ffffff

    Merchant:
      type: object
      description: A merchant profile on the Shopwave platform
      properties:
        id:
          type: integer
          description: Unique merchant ID
          example: 1
        name:
          type: string
          description: Merchant business name
          example: Coffee Bean
        description:
          type: string
          description: Short description of the business
          example: A coffee shop
        companyNumber:
          type: string
          description: Registered company number
          example: "34522113"
        vatNumber:
          type: string
          description: VAT registration number
          example: "12345"
        imageIds:
          $ref: "#/components/schemas/MerchantImageIds"
        colours:
          $ref: "#/components/schemas/MerchantColours"
        links:
          type: object
          description: External links (e.g. website)
          properties:
            website:
              type: object
              properties:
                home:
                  type: string
                  description: Homepage URL
                  example: "http://google.com"
        categoryId:
          type: integer
          description: Business type category ID
          example: 2
        estAnnualRevenue:
          type: number
          description: Estimated annual revenue in pence
          example: 20000000.00
        note:
          type: string
          description: Additional notes about the merchant
          example: Google organic search conversion
      required: [id, name]

    CreateMerchantRequest:
      type: object
      properties:
        merchant:
          type: object
          properties:
            name:
              type: string
              description: Merchant name
              example: Coffee Bean
            description:
              type: string
              description: Short description
            companyNumber:
              type: string
              description: Company number
            vatNumber:
              type: string
              description: VAT number
            imageIds:
              $ref: "#/components/schemas/MerchantImageIds"
            colours:
              $ref: "#/components/schemas/MerchantColours"
            links:
              type: object
            categoryId:
              type: integer
              description: Business type category ID
            estAnnualRevenue:
              type: number
              description: Estimated annual revenue in pence
            note:
              type: string
          required: [name]
      required: [merchant]

    # ─── Order ───
    Order:
      type: object
      description: A delivery order attached to a completed basket
      properties:
        id:
          type: integer
          description: Unique order ID
          example: 140397
        timestamp:
          type: string
          format: date-time
          description: Order creation timestamp
          example: "2018-01-10T12:30:00.000Z"
        completeDate:
          type: string
          format: date-time
          description: Date the order was completed
          example: "2018-01-12T23:00:16.000Z"
        storeId:
          type: integer
          description: Store ID the order belongs to
          example: 5
        merchantId:
          type: integer
          description: Merchant ID
          example: 12
        status:
          allOf:
            - $ref: "#/components/schemas/OrderStatus"
          description: Current lifecycle status of the order. Defaults to `PENDING` for newly created orders. Set via the per-order `status` field on POST `/order`, or via the `status` request header.
        history:
          type: array
          nullable: true
          readOnly: true
          description: |
            Append-only, server-managed log of status / note transitions. Read-only — clients cannot write `history` directly. Entries are appended by sending `status` and/or `note` (per-order in the body, or via the request headers) on POST `/order`.
          items:
            $ref: "#/components/schemas/OrderHistoryEntry"
        deliveryAddress:
          allOf:
            - $ref: "#/components/schemas/DeliveryAddress"
          nullable: true
          description: Delivery address attached to the order. Set via the per-order `deliveryAddress` field on POST `/order`, or via the `deliveryAddress` request header. May be `null` if never set.
      required: [id, storeId, merchantId]
      example:
        id: 140397
        timestamp: "2018-01-10T12:30:00.000Z"
        completeDate: "2018-01-12T23:00:16.000Z"
        storeId: 5
        merchantId: 12
        status: READY
        history:
          - status: PENDING
            at: "2018-01-10T12:30:00Z"
            note: "walk-in customer"
          - status: CONFIRMED
            at: "2018-01-10T12:32:14Z"
            note: ""
          - status: READY
            at: "2018-01-10T12:45:02Z"
            note: "buzzer broken, call"
        deliveryAddress:
          name: "Julian Smith"
          company: null
          line1: "12 Example Street"
          line2: "Apt 4"
          city: "London"
          region: "Greater London"
          postcode: "SW1A 1AA"
          countryCode: "GB"
          phone: "+447700900000"
          latitude: 51.5014
          longitude: -0.1419
          deliveryInstructions: "Buzzer is broken, call on arrival"

    OrderStatus:
      type: string
      description: Lifecycle status for an order.
      enum:
        - PENDING
        - HELD
        - CONFIRMED
        - READY
        - FULFILLED
        - COMPLETED
        - CANCELLED
        - RETURNED
      example: CONFIRMED

    OrderHistoryEntry:
      type: object
      description: A single status / note transition appended to an order's `history` log.
      properties:
        status:
          allOf:
            - $ref: "#/components/schemas/OrderStatus"
          description: Status recorded at this point in time. When the originating request only set the `note` header, this carries the order's then-current status.
        at:
          type: string
          format: date-time
          description: ISO-8601 UTC timestamp the entry was written.
          example: "2026-04-09T14:32:11Z"
        note:
          type: string
          maxLength: 50
          description: Free-text note (max 50 characters). Empty string when only `status` was set.
          example: "walk-in customer"
      required: [status, at, note]

    DeliveryAddress:
      type: object
      description: |
        Delivery address attached to an order. The schema is intentionally flexible — any JSON object is accepted by the API and additional fields beyond the conventional ones below are preserved verbatim.
      additionalProperties: true
      properties:
        name:
          type: string
          nullable: true
          example: "Julian Smith"
        company:
          type: string
          nullable: true
          example: null
        line1:
          type: string
          nullable: true
          example: "12 Example Street"
        line2:
          type: string
          nullable: true
          example: "Apt 4"
        city:
          type: string
          nullable: true
          example: "London"
        region:
          type: string
          nullable: true
          example: "Greater London"
        postcode:
          type: string
          nullable: true
          example: "SW1A 1AA"
        countryCode:
          type: string
          nullable: true
          description: ISO 3166-1 alpha-2 country code.
          example: "GB"
        phone:
          type: string
          nullable: true
          example: "+447700900000"
        latitude:
          type: number
          format: double
          nullable: true
          example: 51.5014
        longitude:
          type: number
          format: double
          nullable: true
          example: -0.1419
        deliveryInstructions:
          type: string
          nullable: true
          example: "Buzzer is broken, call on arrival"

    CompleteOrderRequest:
      type: object
      properties:
        orders:
          type: object
          description: Map of orders to create or update, keyed by an arbitrary client-side reference.
          additionalProperties:
            type: object
            properties:
              id:
                type: integer
                description: Order ID to create or update
                example: 140397
              completed:
                type: boolean
                description: Set to `true` to mark the order as completed.
                example: true
              status:
                allOf:
                  - $ref: "#/components/schemas/OrderStatus"
                description: |
                  Optional new lifecycle status for this specific order. Updates the order's `status` column and appends an entry to its `history` log. Overrides the `status` request header for this order.
              note:
                type: string
                maxLength: 50
                description: |
                  Optional free-text note (max 50 characters) for this specific order. Appends an entry to its `history` log. Can be supplied with or without `status`. Overrides the `note` request header for this order.
                example: "walk-in customer"
              deliveryAddress:
                allOf:
                  - $ref: "#/components/schemas/DeliveryAddress"
                nullable: true
                description: |
                  Optional delivery address for this specific order. Stored verbatim as JSON. Overrides the `deliveryAddress` request header for this order.
            required: [id]
      required: [orders]
      example:
        orders:
          "1":
            id: 45905471
            status: CONFIRMED
            deliveryAddress:
              name: "Julian Smith"
              company: null
              line1: "12 Example Street"
              line2: "Apt 4"
              city: "London"
              region: "Greater London"
              postcode: "SW1A 1AA"
              countryCode: "GB"
              phone: "+447700900000"
              latitude: 51.5014
              longitude: -0.1419
              deliveryInstructions: "Buzzer is broken, call on arrival"
          "2":
            id: 45905472
            completed: true
            status: FULFILLED
            note: "delivered to neighbour"

    OrderResponse:
      type: object
      description: |
        An order returned in the POST `/order` response. For freshly inserted orders this echoes the request payload (`id`, `completeDate`); for orders that already existed, the merged stored row is returned, including `status`, `history`, and `deliveryAddress` as they were *before* this request's header-driven updates were written.
      properties:
        id:
          type: integer
          description: Order ID
          example: 199133
        completeDate:
          type: string
          format: date-time
          description: Order completion date
          example: "2018-01-12T23:00:16.000Z"
        status:
          allOf:
            - $ref: "#/components/schemas/OrderStatus"
          description: Lifecycle status of the order at the time this row was read from the database. Reflects the state *prior* to any update written by the current request.
        history:
          type: array
          nullable: true
          readOnly: true
          description: |
            Append-only, server-managed log of status / note transitions. Read-only — clients cannot write `history` directly. Entries are appended by sending `status` and/or `note` (per-order in the body, or via the request headers) on POST `/order`. The array shown here reflects the state *prior* to any entry written by the current request.
          items:
            $ref: "#/components/schemas/OrderHistoryEntry"
        deliveryAddress:
          allOf:
            - $ref: "#/components/schemas/DeliveryAddress"
          nullable: true
          description: Delivery address attached to the order. Reflects the state *prior* to any update written by the current request.
      required: [id, completeDate]
      example:
        id: 199133
        completeDate: "2018-01-12T23:00:16.000Z"
        status: CONFIRMED
        history:
          - status: PENDING
            at: "2018-01-12T22:58:41Z"
            note: "walk-in customer"
          - status: CONFIRMED
            at: "2018-01-12T23:00:16Z"
            note: ""
        deliveryAddress:
          name: "Julian Smith"
          company: null
          line1: "12 Example Street"
          line2: "Apt 4"
          city: "London"
          region: "Greater London"
          postcode: "SW1A 1AA"
          countryCode: "GB"
          phone: "+447700900000"
          latitude: 51.5014
          longitude: -0.1419
          deliveryInstructions: "Buzzer is broken, call on arrival"

    # ─── Product ───
    ProductInstance:
      type: object
      description: A product variant with its own pricing and attributes
      properties:
        id:
          type: integer
          description: Unique product instance ID
          example: 531661
        price:
          type: string
          description: Price in pence
          example: "1600.0"
        taxPercentage:
          type: string
          description: Tax percentage as a decimal string
          example: "0.200"
        timestamp:
          type: string
          format: date-time
          description: Date the instance was created
        name:
          type: string
          description: Optional instance-specific name override
          example: Le Burger
        activeDate:
          type: string
          format: date-time
          description: Date the instance became active (null if inactive)
          example: "2023-03-28T13:58:00.000Z"
        tags:
          type: string
          description: Comma-separated tags for this instance
          example: "London Take Out"
        size:
          type: string
          description: Size designation
      required: [id, price, taxPercentage]

    Product:
      type: object
      description: A product for sale on the Shopwave platform
      properties:
        id:
          type: integer
          description: Unique product ID
          example: 135592
        barcode:
          type: string
          description: Product barcode
          example: "4280380013331"
        name:
          type: string
          description: Product display name
          example: Classic Beef Burger
        details:
          type: string
          description: Product description
          example: Our burgers are handmade just for you
        tags:
          type: string
          description: Comma-separated product tags
          example: ""
        unit:
          type: integer
          description: Unit type
        timestamp:
          type: string
          format: date-time
          description: Date the product was created
          example: "2023-03-28T12:58:27.000Z"
        activeDate:
          type: string
          format: date-time
          description: Date the product became active
          example: "2023-01-16T12:51:51.000Z"
        deleteDate:
          type: string
          format: date-time
          description: Date the product was deleted (null if active)
        images:
          type: array
          description: Product image URLs
          items:
            type: string
          example: ["http://static.merchantstack.com/images/merchant/4511/product/image.jpg"]
        categories:
          type: object
          description: Map of category IDs to category names
          additionalProperties:
            type: string
          example:
            "11949": Mains
        totalStock:
          type: integer
          description: Total stock for the requested store
        stockSold:
          type: integer
          description: Stock sold for the requested store
        stockLeft:
          type: integer
          description: Stock remaining for the requested store
        instances:
          type: object
          description: Map of product instances keyed by instance ID
          additionalProperties:
            $ref: "#/components/schemas/ProductInstance"
      required: [id, name]

    CreateProductRequest:
      type: object
      properties:
        products:
          type: object
          additionalProperties:
            type: object
            properties:
              id:
                type: integer
                description: Product ID (include to update)
              barcode:
                type: string
                description: Product barcode
              name:
                type: string
                description: Product name
                example: Classic Beef Burger
              details:
                type: string
                description: Product description
              tags:
                type: string
                description: Comma-separated tags
              activeDate:
                type: string
                format: date-time
                description: Activation date
              images:
                type: array
                description: Image filenames (uploaded via /uploader)
                items:
                  type: string
              categories:
                type: array
                description: Category IDs to associate
                items:
                  type: integer
              instances:
                type: object
                description: Product instances keyed by reference
                additionalProperties:
                  type: object
                  properties:
                    price:
                      type: string
                      description: Price in pence
                      example: "1250.0"
                    taxPercentage:
                      type: string
                      description: Tax percentage decimal
                      example: "0.200"
                    name:
                      type: string
                      description: Instance name
                    activeDate:
                      type: string
                      format: date-time
                    tags:
                      type: string
                      description: Comma-separated tags
                  required: [price, taxPercentage]
            required: [name]
      required: [products]

    # ─── Product Component ───
    ProductComponent:
      type: object
      description: A product sub-item group (e.g. sizes, toppings)
      properties:
        id:
          type: integer
          description: Unique component ID
          example: 5432
        name:
          type: string
          description: Component group name
          example: Size
        productId:
          type: integer
          description: Parent product ID
          example: 45678
        minQuantity:
          type: integer
          description: Minimum selectable quantity
          example: 1
        maxQuantity:
          type: integer
          description: Maximum selectable quantity
          example: 1
        productTimestamp:
          type: string
          format: date-time
          description: Date the component was created
        deleteDate:
          type: string
          format: date-time
          description: Date the component was deleted (null if active)
        products:
          type: object
          description: Map of product options keyed by product ID
          additionalProperties:
            type: object
            properties:
              id:
                type: integer
                description: Product ID
              name:
                type: string
                description: Product name
              barcode:
                type: string
              details:
                type: string
              activeDate:
                type: string
                format: date-time
              images:
                type: array
                items:
                  type: string
              productInstances:
                type: object
                description: Instances for this component product
                additionalProperties:
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
                    price:
                      type: string
                      description: Price in pence
                    activeDate:
                      type: string
                      format: date-time
                    vatPercentage:
                      type: string
                    quantity:
                      type: integer
                      description: Default quantity
      required: [id, name]

    CreateProductComponentRequest:
      type: object
      properties:
        productComponents:
          type: object
          additionalProperties:
            type: object
            properties:
              id:
                type: integer
                description: Component ID (include to update)
              name:
                type: string
                description: Component group name
                example: Size
              minQuantity:
                type: integer
                description: Minimum quantity
              maxQuantity:
                type: integer
                description: Maximum quantity
              products:
                type: object
                description: Product options keyed by reference
                additionalProperties:
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
                    barcode:
                      type: string
                    details:
                      type: string
                    activeDate:
                      type: string
                      format: date-time
                    productInstances:
                      type: object
                      additionalProperties:
                        type: object
                        properties:
                          price:
                            type: string
                          vatPercentage:
                            type: string
                          name:
                            type: string
                          quantity:
                            type: integer
            required: [name]
      required: [productComponents]

    # ─── Promotion ───
    Promotion:
      type: object
      description: A merchant promotion or discount offer
      properties:
        id:
          type: integer
          description: Unique promotion ID
          example: 55
        merchantId:
          type: integer
          description: Merchant ID
          example: 1
        storeId:
          type: array
          description: Associated store IDs
          items:
            type: integer
          example: [1, 2]
        title:
          type: string
          description: Promotion title
          example: "10% Off Student Discount"
        details:
          type: string
          description: Promotion description
          example: "10% Off Everything"
        terms:
          type: string
          description: Terms and conditions
          example: "Valid student ID required"
        startDate:
          type: string
          format: date-time
          description: Promotion start date
          example: "2024-01-01T00:00:00.000Z"
        endDate:
          type: string
          format: date-time
          description: Promotion end date
          example: "2024-12-31T23:59:59.000Z"
        daysAvailable:
          type: array
          description: Days of the week the promotion is valid (0=Sunday through 6=Saturday)
          items:
            type: integer
            minimum: 0
            maximum: 6
          example: [0, 1, 2, 3, 4, 5, 6]
        automatic:
          type: integer
          description: "1 for auto-applied, 0 for manual selection"
          enum: [0, 1]
          example: 0
      required: [id, title]

    CreatePromotionRequest:
      type: object
      properties:
        promotions:
          type: object
          additionalProperties:
            type: object
            properties:
              title:
                type: string
                description: Promotion title
                example: "10% Off Student Discount"
              details:
                type: string
                description: Description
              terms:
                type: string
                description: Terms and conditions
              startDate:
                type: string
                format: date-time
                description: Start date
              endDate:
                type: string
                format: date-time
                description: End date
              daysAvailable:
                type: array
                items:
                  type: integer
              automatic:
                type: integer
                enum: [0, 1]
              storeIds:
                type: array
                description: Store IDs to associate
                items:
                  type: integer
              categoryIds:
                type: array
                description: Category IDs to associate
                items:
                  type: integer
              productIds:
                type: array
                description: Product IDs to associate
                items:
                  type: integer
            required: [title, startDate, endDate]
      required: [promotions]

    PromotionResponse:
      type: object
      description: Promotion details after creation or update
      properties:
        id:
          type: integer
          description: Promotion ID
          example: 55
        title:
          type: string
          example: "10% Off Student Discount"
        details:
          type: string
        terms:
          type: string
        startDate:
          type: string
          format: date-time
        endDate:
          type: string
          format: date-time
        daysAvailable:
          type: array
          items:
            type: integer
        automatic:
          type: integer
        storeIds:
          type: array
          items:
            type: integer
        categoryIds:
          type: array
          items:
            type: integer
        productIds:
          type: array
          items:
            type: integer
      required: [id, title]

    PromotionCode:
      type: object
      description: A generated unique promotion code
      properties:
        promotionId:
          type: integer
          description: The promotion this code belongs to
          example: 55
        code:
          type: string
          description: Generated unique promotion code
          example: A1B2C3D4
      required: [promotionId, code]

    # ─── Report ───
    BasketReportProduct:
      type: object
      description: A product line within a basket report (uses abbreviated keys for compactness)
      properties:
        id:
          type: integer
          description: Product ID
          example: 64807
        bPIId:
          type: integer
          description: Unique basket product instance ID
          example: 3977243
        n:
          type: string
          description: Product name
          example: Dalmore
        q:
          type: integer
          description: Quantity
          example: 1
        pMP:
          type: string
          description: Modified price in pence (null if not modified)
        pIP:
          type: string
          description: Instance (original) price in pence
          example: "416.7"
        vP:
          type: string
          description: VAT percentage
          example: "0.2"
      required: [id, bPIId, n, q]

    BasketReport:
      type: object
      description: Aggregated basket report entry (uses abbreviated keys for compactness)
      properties:
        bId:
          type: integer
          description: Basket ID
          example: 1521084
        bN:
          type: string
          description: Basket name
          example: Basket 1
        cId:
          type: integer
          description: Consumer ID
        sId:
          type: integer
          description: Store ID
          example: 53
        c:
          type: string
          format: date-time
          description: Basket completion date
          example: "2015-01-06T15:33:00.000Z"
        ch:
          type: string
          description: Total cash payment amount
          example: "1500"
        cd:
          type: string
          description: Total card payment amount
          example: "0"
        t:
          type: string
          description: Total service payment amount
          example: "0"
        p:
          type: object
          description: Map of products keyed by basket product instance ID
          additionalProperties:
            $ref: "#/components/schemas/BasketReportProduct"
      required: [bId, sId, c]

    ReportCategory:
      type: object
      description: Category metadata within a report
      properties:
        p:
          type: array
          description: Product IDs in this category
          items:
            type: integer
          example: [64807, 64808, 64809]
        n:
          type: string
          description: Category name
          example: "Spirits & Beer"
      required: [p]

    # ─── Stock Reconcile ───
    WastageEntry:
      type: object
      description: A wastage record from stock reconciliation
      properties:
        id:
          type: integer
          description: Basket product instance ID
          example: 4210609
        name:
          type: string
          description: Entry type label
          example: WASTAGE
        storeId:
          type: integer
          description: Store ID
          example: 53
        completeDate:
          type: string
          format: date-time
          description: Completion timestamp
        productId:
          type: integer
          description: Product ID
          example: 7319
        vatPercentage:
          type: string
          description: VAT percentage
          example: "0"
        productInstancePrice:
          type: string
          description: Instance price in pence
          example: "300"
        productModifiedPrice:
          type: string
          description: Modified price if applicable
        quantity:
          type: integer
          description: Quantity wasted (negative value)
          example: -15
      required: [id, productId, quantity]

    SurplusEntry:
      type: object
      description: A surplus record from stock reconciliation
      properties:
        invoiceItemId:
          type: integer
          description: Invoice item ID
          example: 4559
        productId:
          type: integer
          description: Product ID
          example: 1
        storeId:
          type: integer
          description: Store ID
          example: 53
        quantity:
          type: integer
          description: Surplus quantity (positive value)
          example: 100
        price:
          type: string
          description: Price in pence
          example: "100"
        vatPercentage:
          type: number
          description: VAT percentage
          example: 0
        note:
          type: string
          description: Reconciliation note
          example: RECONCILE INCREMENT
      required: [invoiceItemId, productId, quantity]

    StockReconcileResponse:
      type: object
      description: Stock reconciliation results with wastage and surplus entries
      properties:
        wastage:
          type: object
          description: Map of wastage entries keyed by basket product instance ID
          additionalProperties:
            $ref: "#/components/schemas/WastageEntry"
        surplus:
          type: object
          description: Map of surplus entries keyed by invoice item ID
          additionalProperties:
            $ref: "#/components/schemas/SurplusEntry"

    StockReconcileProduct:
      type: object
      description: A product adjustment in a stock reconciliation request
      properties:
        productId:
          type: integer
          description: Product ID
          example: 7
        productName:
          type: string
          description: Product name
          example: Coke
        quantity:
          type: integer
          description: Quantity adjustment (negative for wastage, positive for surplus)
          example: -10
        note:
          type: string
          description: Adjustment note
          example: "Wastage - Items Broken"
        price:
          type: string
          description: Cost price (positive only, for surplus items)
          example: "10"
        vatPercentage:
          type: string
          description: VAT percentage (for surplus items)
          example: "0.20"
      required: [productId, quantity]

    CreateStockReconcileRequest:
      type: object
      properties:
        stockReconcile:
          type: object
          properties:
            products:
              type: object
              description: Map of product adjustments keyed by reference
              additionalProperties:
                $ref: "#/components/schemas/StockReconcileProduct"
            storeId:
              type: integer
              description: Store ID for the stock reconciliation
              example: 53
            completed:
              type: boolean
              description: Mark reconciliation as completed
              example: true
          required: [products, storeId]
      required: [stockReconcile]

    # ─── Stock Report ───
    StockReportEntry:
      type: object
      description: A stock report entry for a product at a given store
      properties:
        id:
          type: integer
          description: Product ID
          example: 42
        name:
          type: string
          description: Product name
          example: Flat White
        details:
          type: string
          description: Product description
        images:
          type: array
          description: CDN URLs for product images
          items:
            type: string
        deleteDate:
          type: string
          format: date-time
          description: Date the product was deleted (null if active)
        activeDate:
          type: string
          format: date-time
          description: Date the product became active
        productInstanceId:
          type: integer
          description: Product instance (variant) ID
        price:
          type: string
          description: Sale price in pence
          example: "250.0"
        vatPercentage:
          type: number
          description: VAT percentage as a decimal
          example: 0.2
        costPrice:
          type: string
          description: Cost price in pence (from supplier invoice)
          example: "120.0"
        costVatPercentage:
          type: number
          description: Cost VAT percentage as a decimal
          example: 0.2
        supplierId:
          type: integer
          description: Supplier ID
        supplierName:
          type: string
          description: Supplier name
        totalStock:
          type: integer
          description: Total stock received
        stockSold:
          type: integer
          description: Total stock sold
        stockLeft:
          type: integer
          description: Remaining stock (totalStock minus stockSold)
      required: [id, name]

    # ─── Store ───
    Store:
      type: object
      description: A merchant store location
      properties:
        id:
          type: integer
          description: Unique store ID
          example: 1
        lat:
          type: number
          format: float
          description: Latitude coordinate
          example: 51.4769
        lng:
          type: number
          format: float
          description: Longitude coordinate
          example: -0.168871
        addressLine1:
          type: string
          description: First line of the address
          example: Tower Bridge
        addressLine2:
          type: string
          description: Second line of the address
          example: Shad Thames
        addressLine3:
          type: string
          description: Third line of the address
          example: London
        phoneNumber:
          type: string
          description: Store phone number
          example: "0207 132 9412"
        city:
          type: string
          description: City name
          example: Greater London
        postcode:
          type: string
          description: Postal/zip code
          example: SE1
        countryId:
          type: string
          description: ISO 3166-1 alpha-2 country code
          example: GB
        timezoneId:
          type: integer
          description: Timezone ID from the Shopwave timezone reference list. IDs correspond to Moment.js timezone database entries (see momentjs.com/timezone).
          example: 371
        email:
          type: string
          format: email
          description: Store contact email (also enables customer receipts)
          example: store@email.com
        sandbox:
          type: boolean
          description: Whether the store is in sandbox mode
          example: false
        storeDeleteDate:
          type: string
          format: date-time
          description: Date the store was deleted (null if active)
      required: [id, addressLine1, city, postcode]

    CreateStoreRequest:
      type: object
      properties:
        stores:
          type: object
          additionalProperties:
            type: object
            properties:
              lat:
                type: number
                format: float
                description: Latitude
                example: 51.4769
              lng:
                type: number
                format: float
                description: Longitude
                example: -0.168871
              addressLine1:
                type: string
                description: Address line 1
                example: Tower Bridge
              addressLine2:
                type: string
              addressLine3:
                type: string
              phoneNumber:
                type: string
                example: "0207 132 9412"
              city:
                type: string
                example: Greater London
              postcode:
                type: string
                example: SE1
              countryId:
                type: string
                example: GB
              timezoneId:
                type: integer
                example: 371
              email:
                type: string
                format: email
              sandbox:
                type: boolean
            required: [lat, lng, addressLine1, phoneNumber, city, postcode]
      required: [stores]

    # ─── Supplier ───
    Supplier:
      type: object
      description: A supplier profile
      properties:
        id:
          type: integer
          description: Unique supplier ID
          example: 1
        name:
          type: string
          description: Supplier name
          example: Coffee Bean
        description:
          type: string
          description: Supplier description
          example: A coffee supplier
        companyNumber:
          type: string
          description: Company number
        vatNumber:
          type: string
          description: VAT number
          example: "12345"
        imageIds:
          type: array
          description: Supplier image URLs
          items:
            type: string
        note:
          type: string
          description: Additional notes
      required: [id, name]

    CreateSupplierRequest:
      type: object
      properties:
        suppliers:
          type: object
          additionalProperties:
            type: object
            properties:
              name:
                type: string
                description: Supplier name
                example: Coffee Bean
              description:
                type: string
              companyNumber:
                type: string
              vatNumber:
                type: string
              imageIds:
                type: array
                items:
                  type: string
              note:
                type: string
            required: [name]
      required: [suppliers]

    # ─── Supplier Store ───
    SupplierStore:
      type: object
      description: A supplier store location
      properties:
        id:
          type: integer
          description: Unique supplier store ID
          example: 53
        supplierId:
          type: integer
          description: Parent supplier ID
          example: 30
        lat:
          type: number
          format: float
          description: Latitude
          example: 15.972
        lng:
          type: number
          format: float
          description: Longitude
          example: 48.7904
        addressLine1:
          type: string
          description: Address line 1
          example: 1 Test Street
        addressLine2:
          type: string
          description: Address line 2
          example: Test Road
        addressLine3:
          type: string
          description: Address line 3
        phoneNumber:
          type: string
          description: Phone number
          example: "0207 123 4567"
        city:
          type: string
          description: City
          example: Test City
        postcode:
          type: string
          description: Postcode
          example: SE1 2NQ
        countryId:
          type: string
          description: ISO country code
        timezoneId:
          type: integer
          description: Timezone ID from the Shopwave timezone reference list. IDs correspond to Moment.js timezone database entries (see momentjs.com/timezone).
        storeDeleteDate:
          type: string
          format: date-time
          description: Deletion date (null if active)
      required: [id, supplierId, addressLine1, city, postcode]

    CreateSupplierStoreRequest:
      type: object
      properties:
        supplierStores:
          type: object
          additionalProperties:
            type: object
            properties:
              supplierId:
                type: integer
                description: Parent supplier ID
              lat:
                type: number
                format: float
                example: 15.972
              lng:
                type: number
                format: float
                example: 48.7904
              addressLine1:
                type: string
                example: 1 Test Street
              addressLine2:
                type: string
              addressLine3:
                type: string
              phoneNumber:
                type: string
              city:
                type: string
              postcode:
                type: string
              countryId:
                type: string
              timezoneId:
                type: integer
            required: [lat, lng, addressLine1, phoneNumber, city, postcode]
      required: [supplierStores]

    # ─── Transaction ───
    Transaction:
      type: object
      description: A payment transaction attached to a basket
      properties:
        id:
          type: integer
          description: Unique transaction ID
          example: 10119051
        basketId:
          type: integer
          description: Basket ID this transaction belongs to
          example: 16339951
        type:
          type: string
          description: Payment type (e.g. CASH, CARD, SERVICE)
          example: CARD
        amount:
          type: integer
          description: Transaction amount in pence
          example: 504
        tip:
          type: integer
          description: Tip amount in pence
          example: 0
        note:
          type: string
          description: Transaction notes
          example: ""
        completeDate:
          type: string
          format: date-time
          description: Date the transaction was completed (null if pending)
          example: "2017-11-28T15:22:11.000Z"
      required: [id, basketId, type, amount]

    CreateTransactionRequest:
      type: object
      properties:
        transactions:
          type: object
          additionalProperties:
            type: object
            properties:
              basketId:
                type: integer
                description: Basket ID to attach the transaction to
                example: 16339951
              type:
                type: string
                description: Payment type
                example: CARD
              amount:
                type: integer
                description: Amount in pence
                example: 504
              tip:
                type: integer
                description: Tip in pence
                example: 0
              note:
                type: string
                description: Transaction notes
            required: [basketId, type, amount]
      required: [transactions]

    CompleteTransactionRequest:
      type: object
      properties:
        transactions:
          type: object
          additionalProperties:
            type: object
            properties:
              transactionId:
                type: integer
                description: Transaction ID to complete
                example: 10119051
              basketId:
                type: integer
                description: Basket ID
                example: 16339951
              type:
                type: string
                description: Payment type
                example: CARD
              amount:
                type: integer
                description: Amount in pence
                example: 504
              tip:
                type: integer
                description: Tip in pence
                example: 0
              completed:
                type: boolean
                description: Set to true to complete the transaction
                example: true
              note:
                type: string
            required: [transactionId, basketId, type, amount, completed]
      required: [transactions]

    # ─── User ───
    User:
      type: object
      description: A Shopwave user profile
      properties:
        id:
          type: integer
          description: Unique user ID
          example: 273
        firstName:
          type: string
          description: First name
          example: Rich
        lastName:
          type: string
          description: Last name
          example: Murray
        email:
          type: string
          format: email
          description: Email address used to sign up
          example: richard.coffeebean@getshopwave.com
        employee:
          type: object
          description: Employee details if the user is an employee
          properties:
            roleId:
              type: integer
              description: "Role ID within the organisation: 1 = Owner, 2 = Manager, 3 = Assistant, 4 = Guest, 5 = Assistant Manager"
              enum: [1, 2, 3, 4, 5]
              example: 1
            merchantId:
              type: integer
              description: Merchant the user is employed by
              example: 30
      required: [id, firstName, lastName, email]

    CreateUserRequest:
      type: object
      properties:
        user:
          type: object
          properties:
            firstName:
              type: string
              description: First name
              example: Rich
            lastName:
              type: string
              description: Last name
              example: Murray
            email:
              type: string
              format: email
              description: Email address
              example: richard@example.com
          required: [firstName, lastName, email]
      required: [user]
