# GENERATED by tools/openapi/build.js from tools/openapi/spec.yaml and captured examples - do not edit here.
# Same content as /openapi.json. LLM-friendly walkthrough of the main flows: /api-guide.md
openapi: 3.0.3
info:
  title: Verdant Dock API
  version: 1.1.0
  license:
    name: Demonstration only - fictional store for software-testing practice
    url: https://shop.verdant-v1.testthis.now/llms.txt
  description: |-
    JSON API for the Verdant Dock demonstration store: self-driving AutoPlanters and exotic plants, delivered by
    truck with technician setup within 150 km of Melbourne, plus books and accessories (courier anywhere in
    Australia when ordered on their own). Built for software-testing practice; nothing is real.

    **New to this API? Read the guide first:** https://shop.verdant-v1.testthis.now/api-guide.md (plain text,
    about 15 KB) walks through every main flow with the exact requests. This document is the complete reference.

    **Authentication.** Customer operations need `Authorization: Bearer <accessToken>`. Tokens come from
    `POST /v1/auth/login` (password) followed by `POST /v1/auth/login/mfa` (TOTP or recovery code).
    Access tokens are HS256 JWTs valid for 15 minutes; refresh with `POST /v1/auth/token/refresh`
    (refresh tokens rotate on every use; replaying an old one revokes the session).
    Enrolment endpoints take the short-lived `enrolmentToken` from `POST /v1/auth/verify-email` instead.

    **Errors** are RFC 7807 `application/problem+json` with a stable machine-readable `code`
    and, for validation failures, an `errors` array of `{field, message}`.
    Every response carries `X-Correlation-Id` (send your own to trace a request end to end).

    **Request log.** Every request is logged with its source IP and time. Test scripts running from
    several load generators can send `X-Test-Partition: <tag>` (letters, digits, `.`, `-`, `_`; max 64)
    so all their accounts land in one partition for the staff view.

    **Money** is integer cents, AUD, GST-inclusive. **Dates** are `YYYY-MM-DD` in Australia/Melbourne.

    **Test clock.** A signed-in tester can move the *business* date up to 3650 days ahead for their own
    session (`PUT /v1/test-clock`). Delivery slots, cancellation cut-offs, order progress, cover periods and
    card-expiry checks then use the simulated date; security time (tokens, TOTP, lockouts) never moves.
    The offset resets to 0 at every login. Every customer response carries `X-Simulated-Offset-Days` and
    `X-Business-Date`, and the storefront shows a red TEST MODE banner while the offset is not 0.

    **Mock card gateway.** Visa or Mastercard numbers that pass Luhn are accepted. A CVV whose last digit
    is `0` is declined (`402 PAYMENT_DECLINED`, response code 05); any other last digit is approved.

    **Start here (people, LLMs and tools).**
    * Step-by-step guide to the main flows, written for LLMs and humans: `https://shop.verdant-v1.testthis.now/api-guide.md`
      (plain text; every step names the operationId, the request and where each value comes from).
    * Postman: import `https://shop.verdant-v1.testthis.now/postman/verdant-dock.postman_collection.json` and the
      environment `https://shop.verdant-v1.testthis.now/postman/verdant-dock.postman_environment.json`, then run the
      collection: it signs up a new customer (email code from the test mailbox, TOTP computed in a pre-request
      script), orders, raises a support case and signs out, saving every id and token in collection variables.
    * This description as JSON: `/openapi.json` (same content as `/openapi.yaml`). OpenAPI 3.2.1 edition of the same
      description: `/openapi-3.2.yaml` and `/openapi-3.2.json`.
    * Arazzo 1.1.0 workflows for the main flows (executable with an Arazzo runner): `/arazzo.yaml`.
    * Every operation has an `operationId`, and the examples are real responses captured from the running API
      (tokens and ids differ on every run).

    **Main flows (operationIds in order).**
    1. *Customer sign-up:* `signup` -> `getLatestMail` (`to`=the email, `subject=verify`, read `code`) -> `verifyEmail`
       (returns `enrolmentToken`) -> `startMfaEnrolment` (Bearer enrolmentToken; returns base32 `secret`) ->
       `activateMfa` (Bearer enrolmentToken; `code` = TOTP of the secret) -> tokens + recovery codes.
    2. *Sign-in:* `login` (returns `mfaToken`) -> `loginMfa` (`mfaToken` + TOTP `code`, or a `recoveryCode`) ->
       `accessToken` (15 min) + `refreshToken` -> `refreshToken` to rotate.
    3. *Truck order:* `listCatalogue` -> `putCartItem` (AutoPlanter, optional `coverPlan`; plants need a planter of the
       same size) -> `createAddress` (VIC, within 150 km of Melbourne) -> `quoteCheckout` -> `listDeliverySlots`
       (choose a day and an available window) -> `placeOrder` (`Idempotency-Key` header; CVV ending 0 = declined) ->
       `getOrder` / `getInvoicePdf` / `getLatestMail` (`subject=confirmed`) -> `getMailAttachment`.
    4. *Courier order:* cart with only books/accessories (`VD-BK-CT`, `VD-NC-DLX`) -> any Australian address ->
       `quoteCheckout` (`delivery.method` = `COURIER`, $25) -> `placeOrder` with no delivery slot.
    5. *After the order:* `updateDeliveryDetails`, `cancelOrder`, `setTestClock` (move the business date past delivery)
       -> `getOrder` (projected status) -> `listPolicies` -> `listClaims` -> `createClaimUpload` (presigned POST) ->
       `createClaim` -> `getClaim`; `resetTestClock` when done.
    6. *Support:* `createCase` -> `replyToCase` -> `getCase` -> `closeCase` / `reopenCase`; or email the support address
       with `sendTestMailToSupport`.
    7. *Staff portal:* `staffEnrol` -> mailbox code -> `staffVerifyEmail` -> `staffStartMfaEnrolment` -> `staffActivateMfa`
       -> `staffListCustomers`, `staffGetOrder`, `staffReplyToCase`, `staffSetClaimStatus`, `staffOrderReplacement`.
       Staff only see customers whose requests came from the same network (or `X-Test-Partition`).

    **TOTP.** RFC 6238: HMAC-SHA1, 6 digits, 30-second step, secret is base32 (the `secret` from the enrol call).
    Each code works once per account (a reused code in the same 30 s step is rejected with `MFA_CODE_INVALID`),
    so wait for the next step before a second sign-in, or use one of the recovery codes.

    **Test addresses.** Use `<anything>@verdant-v1.testthis.now`: no inbox needed, read the mail with
    `getLatestMail`. Real addresses also work (25 emails/day each).
servers:
  - url: https://api.verdant-v1.testthis.now
    description: verdant-v1, Singapore (ap-southeast-1)
  - url: https://shopapi.loadtest.help
    description: Sydney (ap-southeast-2)
  - url: http://localhost:8080/api
    description: Local dev server (npm start)
tags:
  - name: Reference
    description: "Public, read-only: catalogue, cover plans, localities, delivery-area check, health."
  - name: Auth
    description: Customer sign-up, email verification, TOTP enrolment, sign-in, token refresh, password reset, sign-out.
  - name: Account
    description: "The signed-in customer: profile, password, security events, addresses."
  - name: Cart
    description: Cart items with optional cover plans; plants need a planter of the same size.
  - name: Checkout
    description: Quote and delivery slots for an address (truck) or courier estimate (books/accessories only).
  - name: Orders
    description: Place, read, cancel orders; delivery details; invoice PDF.
  - name: Cover
    description: Protection-plan policies issued on delivery.
  - name: Claims
    description: Insurance claims with photo uploads.
  - name: Services
    description: Free expert check-ins earned by insuring a planter and a plant.
  - name: Testing
    description: Test clock and the test mailbox (read mail sent to the stack domain, send mail to support).
  - name: Warehouse
    description: "Warehouse app: truck schedule and fleet size (workforce account)."
  - name: Assistant
    description: AI chat assistant for signed-in customers.
  - name: Support
    description: Support cases (web and email).
  - name: Staff enrolment
    description: Staff portal enrolment, sign-in and password reset (workforce account).
  - name: Staff portal
    description: "Staff views of customers in their network: orders, deliveries, cases, claims, activity."
  - name: Observability
    description: Traces (OpenTelemetry into AWS X-Ray, cleaned for the public) and test runs, looked up by id. No sign-in.
security:
  - bearer: []
paths:
  /v1/health:
    get:
      operationId: getHealth
      tags:
        - Reference
      security: []
      summary: Liveness
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  app:
                    type: string
                  stage:
                    type: string
                  time:
                    type: string
                    format: date-time
              example: { status: ok, app: Verdant Dock, stage: verdant-v1, time: 2026-10-25T20:56:38.135Z }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/catalogue:
    get:
      operationId: listCatalogue
      tags:
        - Reference
      security: []
      summary: List products
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum:
              - PLANTER
              - PLANT
          example: PLANTER
        - name: size
          in: query
          schema:
            type: string
            enum:
              - S
              - M
              - L
      responses:
        "200":
          description: Products
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                  products:
                    type: array
                    items:
                      $ref: "#/components/schemas/Product"
              example:
                {
                  products:
                    [
                      {
                          sku: VD-AP-S,
                          type: PLANTER,
                          size: S,
                          name: AutoPlanter Small,
                          shortDescription: "A self-driving planter under a spherical fluorite dome that follows the light around your room and returns to its docking station each day.
                            Sized for a Small plant, with room to grow into a Medium.",
                          description: "The AutoPlanter Small is a sealed, climate-controlled world for an exotic specimen: a perfect spherical dome of optical-quality fluorite on a
                            polished metal base that holds the humidity control system and a 12 L reservoi...",
                          growthNote: "There's plenty of room for growth: the AutoPlanter Small can support a Medium plant (up to 20 kg with soil) as your specimen matures, but we
                            recommend starting with a Small plant so it settles into the dome's micro-climate.",
                          priceCents: 149000,
                          capacityKg: 10,
                          maxPlantKg: 20,
                          dimensionsCm: {},
                          dockCm: {},
                          package: {},
                          installMinutes: 60,
                          requiresTechnician: true,
                          features:
                            [
                              "Drives itself around the room on concealed wheels, following the light and rotating so every frond gets the right amount",
                              Perfect spherical dome of optical-quality fluorite (transmits deep UV to far infrared),
                              "Humidity control system and a 12 L reservoir in the metal base, with automatic fertiliser dosing",
                              "Upright docking station: the planter clicks in daily to top up water and fertiliser from a mains water connection"
                            ],
                          coverPlans: [ VC-2Y, VC-3Y, VC-5Y ],
                          image: planter-s,
                          coverOptions: [ {} ],
                          bundleOffer: {}
                        }
                    ],
                  count: 3
                }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/catalogue/{sku}:
    get:
      operationId: getProduct
      tags:
        - Reference
      security: []
      summary: Get one product
      parameters:
        - name: sku
          in: path
          required: true
          schema:
            type: string
            example: VD-AP-M
      responses:
        "200":
          description: Product
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
              example:
                {
                  sku: VD-AP-M,
                  type: PLANTER,
                  size: M,
                  name: AutoPlanter Medium,
                  shortDescription: "A self-driving planter under a spherical fluorite dome that follows the light around your room and returns to its docking station each day.
                    Sized for a Medium plant, with room to grow into a Large.",
                  description: "The AutoPlanter Medium is a sealed, climate-controlled world for an exotic specimen: a perfect spherical dome of optical-quality fluorite on a
                    polished metal base that holds the humidity control system and a 25 L reservo...",
                  growthNote: "There's plenty of room for growth: the AutoPlanter Medium can support a Large plant (up to 50 kg with soil) as your specimen matures, but we
                    recommend starting with a Medium plant so it settles into the dome's micro-climate.",
                  priceCents: 239000,
                  capacityKg: 20,
                  maxPlantKg: 50,
                  dimensionsCm: { w: 115, d: 115, h: 145 },
                  dockCm: { w: 40, d: 35, h: 130 },
                  package: { w: 125, d: 125, h: 160, kg: 70 },
                  installMinutes: 90,
                  requiresTechnician: true,
                  features:
                    [
                      "Drives itself around the room on concealed wheels, following the light and rotating so every frond gets the right amount",
                      Perfect spherical dome of optical-quality fluorite (transmits deep UV to far infrared),
                      "Humidity control system and a 25 L reservoir in the metal base, with automatic fertiliser dosing",
                      "Upright docking station: the planter clicks in daily to top up water and fertiliser from a mains water connection"
                    ],
                  coverPlans: [ VC-2Y, VC-3Y, VC-5Y ],
                  image: planter-m,
                  coverOptions:
                    [
                      {
                          code: VC-2Y,
                          appliesTo: PLANTER,
                          name: Verdant Care 2 years,
                          years: 2,
                          rateBp: 900,
                          summary: "Covers failure of any kind - mechanical, electrical, sensor, pump or dock - plus accidental damage. Includes technician call-out. Insure both an
                            AutoPlanter and a plant in the same order and you also get a free in-home e...",
                          unitPriceCents: 21600
                        }
                    ],
                  bundleOffer:
                    {
                      code: EXPERT_CHECKIN,
                      valueCents: 30000,
                      text: "Insure both an AutoPlanter and a plant in the same order and you also get a free in-home expert check-in, worth $300. It appears in your Orders as a
                        not-yet-activated order - book the visit whenever suits you, from a week after delivery."
                    }
                }
        "404":
          $ref: "#/components/responses/Problem"
  /v1/cover-plans:
    get:
      operationId: listCoverPlans
      tags:
        - Reference
      security: []
      summary: Protection plans (Verdant Care for planters; Thrive Cover for plants)
      responses:
        "200":
          description: Plans
          content:
            application/json:
              schema:
                type: object
                properties:
                  plans:
                    type: array
                    items: {}
                  bundleOffer:
                    type: object
              example:
                {
                  plans:
                    [
                      {
                          code: VC-2Y,
                          appliesTo: PLANTER,
                          name: Verdant Care 2 years,
                          years: 2,
                          rateBp: 900,
                          summary: "Covers failure of any kind - mechanical, electrical, sensor, pump or dock - plus accidental damage. Includes technician call-out. Insure both an
                            AutoPlanter and a plant in the same order and you also get a free in-home e..."
                        }
                    ],
                  bundleOffer:
                    {
                      code: EXPERT_CHECKIN,
                      valueCents: 30000,
                      text: "Insure both an AutoPlanter and a plant in the same order and you also get a free in-home expert check-in, worth $300. It appears in your Orders as a
                        not-yet-activated order - book the visit whenever suits you, from a week after delivery."
                    }
                }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/localities:
    get:
      operationId: listLocalities
      tags:
        - Reference
      security: []
      summary: Suburbs for a postcode
      parameters:
        - name: postcode
          in: query
          required: true
          schema:
            type: string
            pattern: ^\d{4}$
            example: "3150"
          example: "3150"
      responses:
        "200":
          description: Localities
          content:
            application/json:
              schema:
                type: object
                properties:
                  postcode:
                    type: string
                  localities:
                    type: array
                    items: {}
              example: { postcode: "3150", localities: [ { postcode: "3150", suburb: BRANDON PARK, state: VIC } ] }
        "422":
          $ref: "#/components/responses/Problem"
  /v1/delivery/check:
    get:
      operationId: checkDeliveryArea
      tags:
        - Reference
      security: []
      summary: Is a locality inside the delivery area? Indicative freight.
      parameters:
        - name: suburb
          in: query
          required: true
          schema:
            type: string
            example: Bendigo
          example: GLEN WAVERLEY
        - name: state
          in: query
          required: true
          schema:
            type: string
            example: VIC
          example: VIC
        - name: postcode
          in: query
          required: true
          schema:
            type: string
            example: "3550"
          example: "3150"
      responses:
        "200":
          description: Assessment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceArea"
              example:
                {
                  locality: { suburb: GLEN WAVERLEY, state: VIC, postcode: "3150" },
                  serviceable: true,
                  reason: null,
                  distanceFromMelbourneKm: 19.1,
                  roadKmFromWarehouse: 20,
                  zone: METRO,
                  indicativeFreightCents: 13000,
                  warehouse: { name: Verdant Dock Distribution Centre, suburb: DANDENONG SOUTH }
                }
        "422":
          $ref: "#/components/responses/Problem"
  /v1/auth/signup:
    post:
      operationId: signup
      tags:
        - Auth
      security: []
      summary: Register; emails a 6-digit verification code
      description: Always 202 for a well-formed request - an already-registered email is not disclosed (the owner is emailed instead). Any deliverable address when
        the stack runs with EmailRecipients=any (reserved domains and domains without mail servers get 422); otherwise only the hosted AllowedEmailDomains (422
        EMAIL_DOMAIN_NOT_ALLOWED). Hosted addresses can read their code from /v1/test-mailbox/latest.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
                - givenName
                - familyName
                - mobile
                - acceptTerms
              properties:
                email:
                  type: string
                  format: email
                  example: ava.nguyen@testthis.now
                password:
                  type: string
                  description: 12-128 chars; upper, lower, digit and symbol
                  example: Plant3r!Dock2026
                givenName:
                  type: string
                  example: Ava
                familyName:
                  type: string
                  example: Nguyen
                mobile:
                  type: string
                  description: Australian mobile, 04XXXXXXXX or +614XXXXXXXX
                  example: 0412 345 678
                acceptTerms:
                  type: boolean
                  enum:
                    - true
                marketingOptIn:
                  type: boolean
            example:
              {
                email: ava.example@verdant-v1.testthis.now,
                password: Plant3r!Dock2026,
                givenName: Ava,
                familyName: Nguyen,
                mobile: 0412 345 678,
                acceptTerms: true,
                marketingOptIn: false
              }
      responses:
        "202":
          description: Verification code sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  email:
                    type: string
                  message:
                    type: string
                  codeExpiresInSeconds:
                    type: integer
              example:
                {
                  status: VERIFICATION_SENT,
                  email: ava.example@verdant-v1.testthis.now,
                  message: Check your email for a 6-digit verification code.,
                  codeExpiresInSeconds: 900
                }
        "422":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
  /v1/auth/signup/resend:
    post:
      operationId: resendVerification
      tags:
        - Auth
      security: []
      summary: Resend the verification code (60 s cooldown)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
            example: { email: ava.example@verdant-v1.testthis.now }
      responses:
        "202":
          description: Sent if applicable
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  email:
                    type: string
                  message:
                    type: string
              example: { status: VERIFICATION_SENT, email: ava.example@verdant-v1.testthis.now, message: "If that account is awaiting verification, a new code is on its way." }
        "429":
          $ref: "#/components/responses/Problem"
  /v1/auth/verify-email:
    post:
      operationId: verifyEmail
      tags:
        - Auth
      security: []
      summary: Confirm the emailed code; returns an enrolment token for TOTP setup
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - code
              properties:
                email:
                  type: string
                code:
                  type: string
                  example: "482913"
            example: { email: ava.example@verdant-v1.testthis.now, code: "099914" }
      responses:
        "200":
          description: status MFA_ENROLMENT_REQUIRED, enrolmentToken (15 min)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  enrolmentToken:
                    type: string
                  expiresIn:
                    type: integer
                  message:
                    type: string
              example:
                {
                  status: MFA_ENROLMENT_REQUIRED,
                  enrolmentToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  message: Email verified. Set up your authenticator app to finish.
                }
        "400":
          description: CODE_INVALID (with attemptsRemaining), CODE_EXPIRED, CODE_ATTEMPTS_EXCEEDED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          $ref: "#/components/responses/Problem"
  /v1/auth/mfa/enrol:
    post:
      operationId: startMfaEnrolment
      tags:
        - Auth
      security:
        - enrolment: []
      summary: Generate a TOTP secret (returns secret, otpauth URI and QR code SVG)
      responses:
        "200":
          description: secret, otpauthUri, qrSvg, algorithm SHA1, digits 6, period 30
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret:
                    type: string
                  otpauthUri:
                    type: string
                  qrSvg:
                    type: string
                  algorithm:
                    type: string
                  digits:
                    type: integer
                  period:
                    type: integer
                  message:
                    type: string
              example:
                {
                  secret: H32RZNYZAXNQUJAEZ34RIGBCE4TUYH2L,
                  otpauthUri: otpauth://totp/Verdant%20Dock%3Aava.example%40verdant-v1.testthis.now?secret=H32RZNYZAXNQUJAEZ34RIGBCE4TUYH2L&issuer=Verdant%20Dock&algorithm=SHA1&digits=6&period=30,
                  qrSvg: <svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>,
                  algorithm: SHA1,
                  digits: 6,
                  period: 30,
                  message: "Scan the QR code (or type the secret) into an authenticator app, then confirm with a 6-digit code."
                }
        "409":
          $ref: "#/components/responses/Problem"
  /v1/auth/mfa/activate:
    post:
      operationId: activateMfa
      tags:
        - Auth
      security:
        - enrolment: []
      summary: Confirm a TOTP code; activates the account, returns 8 recovery codes and signs in
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - code
              properties:
                code:
                  type: string
            example: { code: "379172" }
      responses:
        "200":
          description: Tokens + recoveryCodes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tokens"
              example:
                {
                  status: ACTIVE,
                  recoveryCodes: [ 947R-HAQS, FBWG-6GXW, XPND-SKPA, 78A2-QCDK, ENBB-EGDG, EVCE-808D, QN3T-S4DR, 8ZMM-YQ67 ],
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: usr_btgygbap02w2g1cd3qcn.ses_3b88pt72aqk...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: usr_btgygbap02w2g1cd3qcn,
                      email: ava.example@verdant-v1.testthis.now,
                      givenName: Ava,
                      familyName: Nguyen,
                      mobile: "+61412345678",
                      status: ACTIVE,
                      mfaEnabled: true,
                      marketingOptIn: false,
                      createdAt: 2026-10-25T20:56:38.293Z
                    }
                }
        "400":
          $ref: "#/components/responses/Problem"
  /v1/auth/login:
    post:
      operationId: login
      tags:
        - Auth
      security: []
      summary: Step 1 - password
      description: Five consecutive failures (password or MFA) lock the account for 15 minutes (423 ACCOUNT_LOCKED + Retry-After).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                password:
                  type: string
            example: { email: ava.example@verdant-v1.testthis.now, password: Plant3r!Dock2026 }
      responses:
        "200":
          description: status MFA_REQUIRED with mfaToken (5 min), or MFA_ENROLMENT_REQUIRED with enrolmentToken
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  mfaToken:
                    type: string
                  expiresIn:
                    type: integer
                  methods:
                    type: array
                    items: {}
              example: { status: MFA_REQUIRED, mfaToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp..., expiresIn: 300, methods: [ totp, recovery_code ] }
        "401":
          description: INVALID_CREDENTIALS
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: EMAIL_NOT_VERIFIED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "423":
          description: ACCOUNT_LOCKED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/auth/login/mfa:
    post:
      operationId: loginMfa
      tags:
        - Auth
      security: []
      summary: Step 2 - TOTP code or recovery code
      description: A TOTP code can be used once (replays within the same 30 s step are rejected). Accepts the previous and next step for clock drift.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - mfaToken
              properties:
                mfaToken:
                  type: string
                code:
                  type: string
                recoveryCode:
                  type: string
                  example: K7QM-2X9D
            example: { mfaToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp..., code: "406231" }
      responses:
        "200":
          description: Tokens
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tokens"
              example:
                {
                  status: AUTHENTICATED,
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: usr_btgygbap02w2g1cd3qcn.ses_2pp62y8n71j...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: usr_btgygbap02w2g1cd3qcn,
                      email: ava.example@verdant-v1.testthis.now,
                      givenName: Ava,
                      familyName: Nguyen,
                      mobile: "+61412345678",
                      status: ACTIVE,
                      mfaEnabled: true,
                      marketingOptIn: false,
                      createdAt: 2026-10-25T20:56:38.293Z
                    },
                  recoveryCodesRemaining: 8
                }
        "401":
          $ref: "#/components/responses/Problem"
  /v1/auth/token/refresh:
    post:
      operationId: refreshToken
      tags:
        - Auth
      security: []
      summary: Exchange a refresh token for new tokens (rotates the refresh token)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                refreshToken:
                  type: string
            example: { refreshToken: usr_btgygbap02w2g1cd3qcn.ses_2pp62y8n71j... }
      responses:
        "200":
          description: Tokens
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokenType:
                    type: string
                  accessToken:
                    type: string
                  expiresIn:
                    type: integer
                  refreshToken:
                    type: string
                  refreshExpiresIn:
                    type: integer
                  user:
                    type: object
              example:
                {
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: usr_btgygbap02w2g1cd3qcn.ses_2pp62y8n71j...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: usr_btgygbap02w2g1cd3qcn,
                      email: ava.example@verdant-v1.testthis.now,
                      givenName: Ava,
                      familyName: Nguyen,
                      mobile: "+61412345678",
                      status: ACTIVE,
                      mfaEnabled: true,
                      marketingOptIn: false,
                      createdAt: 2026-10-25T20:56:38.293Z
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
  /v1/auth/logout:
    post:
      operationId: logout
      tags:
        - Auth
      summary: End this session (or ?all=true for every session). Access tokens stop working immediately.
      parameters:
        - name: all
          in: query
          schema:
            type: string
            enum:
              - "true"
      responses:
        "204":
          description: Signed out
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/auth/password/forgot:
    post:
      operationId: forgotPassword
      tags:
        - Auth
      security: []
      summary: Email a 6-digit reset code (always 202)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
            example: { email: ava.example@verdant-v1.testthis.now }
      responses:
        "202":
          description: Sent if the account exists
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string
                  codeExpiresInSeconds:
                    type: integer
              example: { status: RESET_CODE_SENT, message: "If an account exists for that email, a reset code is on its way.", codeExpiresInSeconds: 900 }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/auth/password/reset:
    post:
      operationId: resetPassword
      tags:
        - Auth
      security: []
      summary: Set a new password with the emailed code; revokes all sessions. MFA is still required at next login.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - code
                - newPassword
              properties:
                email:
                  type: string
                code:
                  type: string
                newPassword:
                  type: string
            example: { email: ava.example@verdant-v1.testthis.now, code: "066879", newPassword: N3w!Planter2027 }
      responses:
        "200":
          description: Password changed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string
              example: { status: PASSWORD_RESET, message: Your password has been changed. Sign in with your new password and authenticator code. }
        "400":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/me:
    get:
      operationId: getMe
      tags:
        - Account
      summary: Current customer
      responses:
        "200":
          description: Profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  userId:
                    type: string
                  email:
                    type: string
                  givenName:
                    type: string
                  familyName:
                    type: string
                  mobile:
                    type: string
                  status:
                    type: string
                  mfaEnabled:
                    type: boolean
                  marketingOptIn:
                    type: boolean
                  createdAt:
                    type: string
                    format: date-time
                  recoveryCodesRemaining:
                    type: integer
                  lastLoginAt:
                    type: string
                    format: date-time
                  testClock:
                    type: object
              example:
                {
                  userId: usr_btgygbap02w2g1cd3qcn,
                  email: ava.example@verdant-v1.testthis.now,
                  givenName: Ava,
                  familyName: Nguyen,
                  mobile: "+61412345678",
                  status: ACTIVE,
                  mfaEnabled: true,
                  marketingOptIn: false,
                  createdAt: 2026-10-25T20:56:38.293Z,
                  recoveryCodesRemaining: 8,
                  lastLoginAt: 2026-10-25T20:58:19.423Z,
                  testClock: { enabled: true, offsetDays: 0, simulated: false, realDate: 2026-10-26, businessDate: 2026-10-26, maxOffsetDays: 3650, notice: null, resetsOn: login }
                }
        "401":
          $ref: "#/components/responses/Problem"
    patch:
      operationId: updateMe
      tags:
        - Account
      summary: Update name, mobile, marketing preference
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                givenName:
                  type: string
                familyName:
                  type: string
                mobile:
                  type: string
                marketingOptIn:
                  type: boolean
            example: { marketingOptIn: true }
      responses:
        "200":
          description: Profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  userId:
                    type: string
                  email:
                    type: string
                  givenName:
                    type: string
                  familyName:
                    type: string
                  mobile:
                    type: string
                  status:
                    type: string
                  mfaEnabled:
                    type: boolean
                  marketingOptIn:
                    type: boolean
                  createdAt:
                    type: string
                    format: date-time
              example:
                {
                  userId: usr_btgygbap02w2g1cd3qcn,
                  email: ava.example@verdant-v1.testthis.now,
                  givenName: Ava,
                  familyName: Nguyen,
                  mobile: "+61412345678",
                  status: ACTIVE,
                  mfaEnabled: true,
                  marketingOptIn: true,
                  createdAt: 2026-10-25T20:56:38.293Z
                }
        "422":
          $ref: "#/components/responses/Problem"
  /v1/me/password:
    post:
      operationId: changePassword
      tags:
        - Account
      summary: Change password (current password + TOTP); signs out other sessions
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - currentPassword
                - newPassword
                - code
              properties:
                currentPassword:
                  type: string
                newPassword:
                  type: string
                code:
                  type: string
            example: { currentPassword: Plant3r!Dock2026, newPassword: N3w!Planter2026, code: "108318" }
      responses:
        "200":
          description: Changed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string
              example: { status: PASSWORD_CHANGED, message: Password changed. Other devices have been signed out. }
        "403":
          $ref: "#/components/responses/Problem"
  /v1/me/security-events:
    get:
      operationId: listSecurityEvents
      tags:
        - Account
      summary: Last 50 security events (logins, failures, lockouts, password changes)
      responses:
        "200":
          description: Events
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items: {}
              example: { events: [ { type: PROFILE_UPDATED, at: 2026-10-25T20:58:19.462Z, ip: 203.0.113.77, userAgent: node-test, detail: {} } ] }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/me/addresses:
    get:
      operationId: listAddresses
      tags:
        - Account
      summary: Address book
      responses:
        "200":
          description: Addresses
          content:
            application/json:
              schema:
                type: object
                properties:
                  addresses:
                    type: array
                    items: {}
              example:
                {
                  addresses:
                    [
                      {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: "Side gate, code 4821. Dog is friendly.",
                          isDefault: true,
                          serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                          createdAt: 2026-10-25T20:58:19.473Z
                        }
                    ]
                }
        4XX:
          $ref: "#/components/responses/Problem"
    post:
      operationId: createAddress
      tags:
        - Account
      summary: Add an address (validated against Australian localities; service area assessed)
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddressInput"
            example:
              {
                label: Home,
                recipientName: Ava Nguyen,
                line1: 12 Fern Street,
                suburb: Glen Waverley,
                state: VIC,
                postcode: "3150",
                phone: 0412 345 678,
                siteAccess: GROUND,
                instructions: "Side gate, code 4821"
              }
      responses:
        "201":
          description: Address
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Address"
              example:
                {
                  addressId: adr_2pf7jq915g9jfmzhma33,
                  label: Home,
                  recipientName: Ava Nguyen,
                  phone: "+61412345678",
                  line1: 12 Fern Street,
                  line2: "",
                  suburb: GLEN WAVERLEY,
                  state: VIC,
                  postcode: "3150",
                  siteAccess: GROUND,
                  instructions: "Side gate, code 4821",
                  isDefault: true,
                  serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                  createdAt: 2026-10-25T20:58:19.473Z
                }
          links:
            QuoteCheckout:
              operationId: quoteCheckout
              parameters: {}
              description: Use addressId from this response in the quote request body.
            ListDeliverySlots:
              operationId: listDeliverySlots
              parameters:
                addressId: $response.body#/addressId
              description: Delivery slots for this address.
        "422":
          $ref: "#/components/responses/Problem"
  /v1/me/addresses/{addressId}:
    parameters:
      - name: addressId
        in: path
        required: true
        schema:
          type: string
    put:
      operationId: updateAddress
      tags:
        - Account
      summary: Update an address
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddressInput"
            example:
              {
                label: Home,
                recipientName: Ava Nguyen,
                line1: 12 Fern Street,
                suburb: Glen Waverley,
                state: VIC,
                postcode: "3150",
                phone: 0412 345 678,
                siteAccess: GROUND,
                instructions: "Side gate, code 4821. Dog is friendly."
              }
      responses:
        "200":
          description: Address
          content:
            application/json:
              schema:
                type: object
                properties:
                  addressId:
                    type: string
                  label:
                    type: string
                  recipientName:
                    type: string
                  phone:
                    type: string
                  line1:
                    type: string
                  line2:
                    type: string
                  suburb:
                    type: string
                  state:
                    type: string
                  postcode:
                    type: string
                  siteAccess:
                    type: string
                  instructions:
                    type: string
                  isDefault:
                    type: boolean
                  serviceArea:
                    type: object
                  createdAt:
                    type: string
                    format: date-time
              example:
                {
                  addressId: adr_2pf7jq915g9jfmzhma33,
                  label: Home,
                  recipientName: Ava Nguyen,
                  phone: "+61412345678",
                  line1: 12 Fern Street,
                  line2: "",
                  suburb: GLEN WAVERLEY,
                  state: VIC,
                  postcode: "3150",
                  siteAccess: GROUND,
                  instructions: "Side gate, code 4821. Dog is friendly.",
                  isDefault: true,
                  serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                  createdAt: 2026-10-25T20:58:19.473Z
                }
        "404":
          $ref: "#/components/responses/Problem"
    delete:
      operationId: deleteAddress
      tags:
        - Account
      summary: Delete an address
      responses:
        "204":
          description: Deleted
        "404":
          $ref: "#/components/responses/Problem"
  /v1/cart:
    get:
      operationId: getCart
      tags:
        - Cart
      summary: Cart with priced lines and rule violations (issues)
      responses:
        "200":
          description: Cart
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Cart"
              example:
                {
                  items: [ { sku: VD-AP-M, quantity: 1, coverPlan: VC-2Y, addedAt: 2026-10-25T20:58:19.495Z } ],
                  version: 2,
                  currency: AUD,
                  lines:
                    [
                      {
                          sku: VD-AP-M,
                          name: AutoPlanter Medium,
                          type: PLANTER,
                          size: M,
                          quantity: 1,
                          unitPriceCents: 239000,
                          itemTotalCents: 239000,
                          cover: { code: VC-2Y, name: Verdant Care 2 years, years: 2, unitPriceCents: 21600, totalCents: 21600 },
                          lineTotalCents: 260600
                        }
                    ],
                  deliveryBreakdown: [ { code: SETUP_M, label: Technician setup & dock commissioning - AutoPlanter Medium × 1, amountCents: 24000 } ],
                  totals: { merchandiseCents: 424000, coverCents: 43800, deliveryCents: 24000, totalCents: 491800, gstCents: 44709, deliveryIncluded: false },
                  deliveryMethod: TRUCK,
                  benefits:
                    [
                      {
                          code: EXPERT_CHECKIN,
                          title: Expert check-in (in-home visit),
                          valueCents: 30000,
                          description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                            health under the dome, and tune its care plan. About 90 minutes. Free when you..."
                        }
                    ],
                  issues: []
                }
        4XX:
          $ref: "#/components/responses/Problem"
    delete:
      operationId: clearCart
      tags:
        - Cart
      summary: Empty the cart
      responses:
        "200":
          description: Cart
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: {}
                  version:
                    type: integer
                  currency:
                    type: string
                  lines:
                    type: array
                    items: {}
                  deliveryBreakdown:
                    type: array
                    items: {}
                  totals:
                    type: object
                  deliveryMethod:
                    type: string
                  benefits:
                    type: array
                    items: {}
                  issues:
                    type: array
                    items: {}
              example:
                {
                  items: [],
                  version: 10,
                  currency: AUD,
                  lines: [],
                  deliveryBreakdown: [],
                  totals: { merchandiseCents: 0, coverCents: 0, deliveryCents: 0, totalCents: 0, gstCents: 0, deliveryIncluded: false },
                  deliveryMethod: TRUCK,
                  benefits: [],
                  issues: [ { code: CART_EMPTY, message: Your cart is empty. } ]
                }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/cart/items/{sku}:
    parameters:
      - name: sku
        in: path
        required: true
        schema:
          type: string
          example: VD-AP-M
    put:
      operationId: putCartItem
      tags:
        - Cart
      summary: Set quantity (0-4, 0 removes) and optional cover plan for a SKU
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - quantity
              properties:
                quantity:
                  type: integer
                  minimum: 0
                  maximum: 4
                coverPlan:
                  type: string
                  nullable: true
                  enum:
                    - VC-2Y
                    - VC-3Y
                    - VC-5Y
                    - TC-1Y
                    - TC-2Y
                expectedVersion:
                  type: integer
                  description: Optimistic concurrency check
            example: { quantity: 1, coverPlan: VC-2Y }
      responses:
        "200":
          description: Cart
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: {}
                  version:
                    type: integer
                  currency:
                    type: string
                  lines:
                    type: array
                    items: {}
                  deliveryBreakdown:
                    type: array
                    items: {}
                  totals:
                    type: object
                  deliveryMethod:
                    type: string
                  benefits:
                    type: array
                    items: {}
                  issues:
                    type: array
                    items: {}
              example:
                {
                  items: [ { sku: VD-AP-M, quantity: 1, coverPlan: VC-2Y, addedAt: 2026-10-25T20:58:19.495Z } ],
                  version: 1,
                  currency: AUD,
                  lines:
                    [
                      {
                          sku: VD-AP-M,
                          name: AutoPlanter Medium,
                          type: PLANTER,
                          size: M,
                          quantity: 1,
                          unitPriceCents: 239000,
                          itemTotalCents: 239000,
                          cover: { code: VC-2Y, name: Verdant Care 2 years, years: 2, unitPriceCents: 21600, totalCents: 21600 },
                          lineTotalCents: 260600
                        }
                    ],
                  deliveryBreakdown: [ { code: SETUP_M, label: Technician setup & dock commissioning - AutoPlanter Medium × 1, amountCents: 24000 } ],
                  totals: { merchandiseCents: 239000, coverCents: 21600, deliveryCents: 24000, totalCents: 284600, gstCents: 25873, deliveryIncluded: false },
                  deliveryMethod: TRUCK,
                  benefits: [],
                  issues: []
                }
        "409":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
    delete:
      operationId: removeCartItem
      tags:
        - Cart
      summary: Remove a SKU
      responses:
        "200":
          description: Cart
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: {}
                  version:
                    type: integer
                  currency:
                    type: string
                  lines:
                    type: array
                    items: {}
                  deliveryBreakdown:
                    type: array
                    items: {}
                  totals:
                    type: object
                  deliveryMethod:
                    type: string
                  benefits:
                    type: array
                    items: {}
                  issues:
                    type: array
                    items: {}
              example:
                {
                  items: [],
                  version: 9,
                  currency: AUD,
                  lines: [],
                  deliveryBreakdown: [],
                  totals: { merchandiseCents: 0, coverCents: 0, deliveryCents: 0, totalCents: 0, gstCents: 0, deliveryIncluded: false },
                  deliveryMethod: TRUCK,
                  benefits: [],
                  issues: [ { code: CART_EMPTY, message: Your cart is empty. } ]
                }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/checkout/quote:
    post:
      operationId: quoteCheckout
      tags:
        - Checkout
      summary: Full price (items, cover, freight, technician setup) and truck job plan for an address
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - addressId
              properties:
                addressId:
                  type: string
            example: { addressId: adr_2pf7jq915g9jfmzhma33 }
      responses:
        "200":
          description: Quote with canCheckout, issues, delivery (jobMinutes, windowsAllowed...)
          content:
            application/json:
              schema:
                type: object
                properties:
                  cartVersion:
                    type: integer
                  address:
                    type: object
                  currency:
                    type: string
                  lines:
                    type: array
                    items: {}
                  deliveryBreakdown:
                    type: array
                    items: {}
                  totals:
                    type: object
                  deliveryMethod:
                    type: string
                  benefits:
                    type: array
                    items: {}
                  issues:
                    type: array
                    items: {}
                  canCheckout:
                    type: boolean
                  delivery:
                    type: object
              example:
                {
                  cartVersion: 2,
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  currency: AUD,
                  lines:
                    [
                      {
                          sku: VD-AP-M,
                          name: AutoPlanter Medium,
                          type: PLANTER,
                          size: M,
                          quantity: 1,
                          unitPriceCents: 239000,
                          itemTotalCents: 239000,
                          cover: { code: VC-2Y, name: Verdant Care 2 years, years: 2, unitPriceCents: 21600, totalCents: 21600 },
                          lineTotalCents: 260600
                        }
                    ],
                  deliveryBreakdown: [ { code: FREIGHT, label: Truck delivery (20 km from our Dandenong South DC), amountCents: 13000 } ],
                  totals: { merchandiseCents: 424000, coverCents: 43800, deliveryCents: 37000, totalCents: 504800, gstCents: 45891, deliveryIncluded: true },
                  deliveryMethod: TRUCK,
                  benefits:
                    [
                      {
                          code: EXPERT_CHECKIN,
                          title: Expert check-in (in-home visit),
                          valueCents: 30000,
                          description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                            health under the dome, and tune its care plan. About 90 minutes. Free when you..."
                        }
                    ],
                  issues: [],
                  canCheckout: true,
                  delivery:
                    {
                      method: TRUCK,
                      spaceUnits: 4,
                      truckSpaceUnits: 20,
                      jobMinutes: 210,
                      onSiteMinutes: 110,
                      driveMinutesOneWay: 34,
                      shippingKg: 89,
                      shippingM3: 2.73,
                      windowsAllowed: [ AM, PM ],
                      deliverable: true,
                      issues: []
                    }
                }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/delivery/slots:
    get:
      operationId: listDeliverySlots
      tags:
        - Checkout
      summary: Bookable delivery days and windows for the current cart and an address
      description: Business days (Mon-Fri, excluding Victorian public holidays) from 2 business days ahead to 28 days ahead. Jobs up to 300 minutes offer AM/PM
        windows; longer (regional) jobs need a FULL day on an empty truck. firstAvailable is the earliest day and window with room (null if none), so clients
        need not search days.
      parameters:
        - name: addressId
          in: query
          required: true
          schema:
            type: string
          example: adr_2pf7jq915g9jfmzhma33
      responses:
        "200":
          description: Availability
          content:
            application/json:
              schema:
                type: object
                properties:
                  method:
                    type: string
                  firstDate:
                    type: string
                    format: date
                    nullable: true
                  lastDate:
                    type: string
                    format: date
                    nullable: true
                  firstAvailable:
                    type: object
                  job:
                    type: object
                  days:
                    type: array
                    items: {}
                  courier:
                    type: object
              example:
                {
                  method: TRUCK,
                  firstDate: 2026-10-28,
                  lastDate: 2026-11-23,
                  firstAvailable: { date: 2026-10-28, window: AM },
                  job:
                    {
                      spaceUnits: 4,
                      truckSpaceUnits: 20,
                      jobMinutes: 210,
                      onSiteMinutes: 110,
                      driveMinutesOneWay: 34,
                      shippingKg: 89,
                      shippingM3: 2.73,
                      windowsAllowed: [ AM, PM ],
                      deliverable: true,
                      issues: []
                    },
                  days:
                    [
                      {
                          date: 2026-10-28,
                          dayOfWeek: Wednesday,
                          closed: false,
                          closedReason: null,
                          windows: [ { code: AM, label: Morning, times: 7:00am - 12:00pm, minutes: 300, available: true, trucksAvailable: 3 } ],
                          spaceUnitsFreeOnBestTruck: 20
                        }
                    ]
                }
        "422":
          $ref: "#/components/responses/Problem"
  /v1/orders:
    get:
      operationId: listOrders
      tags:
        - Orders
      summary: Order history
      description: Includes earned expert check-ins as entries with kind SERVICE (status NOT_ACTIVATED until booked). Orders are recorded against the real date
        (CONFIRMED -> DISPATCH_SCHEDULED -> OUT_FOR_DELIVERY -> DELIVERED, cover issued on delivery). With the test clock set, undelivered orders are
        additionally projected to the simulated date for display (projected true, recordedStatus holds the saved status).
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - CONFIRMED
              - DISPATCH_SCHEDULED
              - OUT_FOR_DELIVERY
              - DELIVERED
              - CANCELLED
              - PAYMENT_DECLINED
              - FAILED
      responses:
        "200":
          description: Orders
          content:
            application/json:
              schema:
                type: object
                properties:
                  orders:
                    type: array
                    items: {}
              example:
                {
                  orders:
                    [
                      {
                          kind: SERVICE,
                          orderId: chk_a9ngr9g42trr594bv8xv,
                          orderNumber: VD-261025-KEHJJ-EC,
                          title: Expert check-in (in-home visit),
                          status: NOT_ACTIVATED,
                          recordedStatus: NOT_ACTIVATED,
                          projected: false,
                          createdAt: 2026-10-25T20:58:19.619Z,
                          totalCents: 0,
                          valueCents: 30000,
                          itemCount: 1,
                          parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                          deliveryDate: null,
                          deliveryWindow: null,
                          canBook: true
                        }
                    ]
                }
        4XX:
          $ref: "#/components/responses/Problem"
    post:
      operationId: placeOrder
      tags:
        - Orders
      summary: Place an order - books a truck, takes payment, emails a confirmation with a NOT A REAL TAX INVOICE PDF attached
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            pattern: ^[A-Za-z0-9_-]{8,64}$
          description: "Replaying a key returns the stored 201/402 response with Idempotent-Replayed: true"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - addressId
                - delivery
                - payment
              properties:
                addressId:
                  type: string
                delivery:
                  type: object
                  description: "Truck orders: date and window from /v1/delivery/slots. Courier orders (books and accessories only) need no slot; anything sent here is ignored."
                  properties:
                    method:
                      type: string
                      enum:
                        - TRUCK
                        - COURIER
                    date:
                      type: string
                      format: date
                    window:
                      type: string
                      enum:
                        - AM
                        - PM
                        - FULL
                payment:
                  type: object
                  properties:
                    nameOnCard:
                      type: string
                    cardNumber:
                      type: string
                      example: 4111 1111 1111 1111
                    expiryMonth:
                      type: string
                      example: "12"
                    expiryYear:
                      type: string
                      example: "2030"
                    cvv:
                      type: string
                      example: "123"
                cartVersion:
                  type: integer
                  description: "Optional: 409 CART_CHANGED if the cart changed since the quote"
                expectedTotalCents:
                  type: integer
                  description: "Optional: 409 PRICE_CHANGED if the total differs"
            example:
              {
                addressId: adr_2pf7jq915g9jfmzhma33,
                cartVersion: 2,
                expectedTotalCents: 504800,
                delivery: { date: 2026-10-28, window: AM },
                payment: { nameOnCard: Ava Nguyen, cardNumber: 4111 1111 1111 1111, expiryMonth: "12", expiryYear: "2030", cvv: "123" }
              }
      responses:
        "201":
          description: Order confirmed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
              example:
                {
                  orderId: ord_xwykyn0axh01bp3k8xvr,
                  orderNumber: VD-261025-KEHJJ,
                  status: CONFIRMED,
                  recordedStatus: CONFIRMED,
                  projected: false,
                  createdAt: 2026-10-25T20:58:19.594Z,
                  updatedAt: 2026-10-25T20:58:19.616Z,
                  lines:
                    [
                      {
                          sku: VD-AP-M,
                          name: AutoPlanter Medium,
                          type: PLANTER,
                          size: M,
                          quantity: 1,
                          unitPriceCents: 239000,
                          itemTotalCents: 239000,
                          cover: {},
                          lineTotalCents: 260600
                        }
                    ],
                  deliveryBreakdown: [ { code: FREIGHT, label: Truck delivery (20 km from our Dandenong South DC), amountCents: 13000 } ],
                  totals: { merchandiseCents: 424000, coverCents: 43800, deliveryCents: 37000, totalCents: 504800, gstCents: 45891, deliveryIncluded: true },
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  delivery:
                    {
                      method: TRUCK,
                      date: 2026-10-28,
                      window: AM,
                      windowLabel: Morning,
                      windowTimes: 7:00am - 12:00pm,
                      spaceUnits: 4,
                      jobMinutes: 210,
                      installMinutes: 110,
                      shippingKg: 89,
                      shippingM3: 2.73,
                      warehouse: DC-DAN,
                      status: BOOKED,
                      truck: T01,
                      booking: { truck: T01, window: AM, units: 4, reservations: [ {} ] }
                    },
                  payment:
                    {
                      transactionId: txn_pk491ngehh2yvm32,
                      brand: Visa,
                      last4: "1111",
                      expiry: 12/30,
                      token: tok_mdgb1krjhf2j8n290714,
                      amountCents: 504800,
                      approved: true,
                      responseCode: "00",
                      responseText: Approved,
                      authCode: Z9D96F,
                      processedAt: 2026-10-25T20:58:19.616Z,
                      gateway: VerdantPay sandbox
                    },
                  cancellable: true,
                  kind: PRODUCT,
                  replacementFor: null,
                  statusHistory: [ { status: PENDING_PAYMENT, at: 2026-10-25T20:58:19.594Z, simulated: false, businessDate: 2026-10-26, label: "Order received, awaiting payment" } ],
                  placedWithTestClockOffsetDays: 0,
                  policies: [],
                  checkin:
                    {
                      checkinId: chk_a9ngr9g42trr594bv8xv,
                      number: VD-261025-KEHJJ-EC,
                      kind: SERVICE,
                      title: Expert check-in (in-home visit),
                      description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                        health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                      status: NOT_ACTIVATED,
                      projected: false,
                      recordedStatus: NOT_ACTIVATED,
                      valueCents: 30000,
                      visitMinutes: 90,
                      parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                      parentOrderNumber: VD-261025-KEHJJ,
                      deliveryDate: 2026-10-28,
                      address:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: "Side gate, code 4821. Dog is friendly.",
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        },
                      booking: null,
                      createdAt: 2026-10-25T20:58:19.619Z,
                      bookableFrom: 2026-11-04,
                      bookableUntil: 2027-10-28,
                      canBook: true,
                      canCancelBooking: false,
                      history: [ {} ]
                    },
                  deliveryDetails:
                    {
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      editable: true,
                      reason: null,
                      editableUntil: 2026-10-27,
                      message: null,
                      history: []
                    },
                  invoice: { invoiceNumber: INV-261025-KEHJJ, filename: INV-261025-KEHJJ.pdf, notARealTaxInvoice: true, pdf: /v1/orders/ord_xwykyn0axh01bp3k8xvr/invoice.pdf }
                }
          links:
            GetOrder:
              operationId: getOrder
              parameters:
                orderId: $response.body#/orderId
              description: Read the order back (status, delivery, invoice link).
            GetInvoicePdf:
              operationId: getInvoicePdf
              parameters:
                orderId: $response.body#/orderId
              description: Download the NOT A REAL TAX INVOICE PDF (same bytes as the emailed attachment).
            UpdateDeliveryDetails:
              operationId: updateDeliveryDetails
              parameters:
                orderId: $response.body#/orderId
              description: Change recipient, phone or instructions before delivery day.
            CancelOrder:
              operationId: cancelOrder
              parameters:
                orderId: $response.body#/orderId
              description: "Cancel (truck: up to 2 business days before delivery; courier: before dispatch)."
        "402":
          description: PAYMENT_DECLINED (CVV ending in 0); the truck booking is released
          content:
            application/problem+json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Problem"
                  - type: object
                    properties:
                      orderId:
                        type: string
                      orderNumber:
                        type: string
                      responseCode:
                        type: string
                        example: "05"
                      type:
                        type: string
                      title:
                        type: string
                      status:
                        type: integer
                      code:
                        type: string
                      detail:
                        type: string
                      correlationId:
                        type: string
              example:
                {
                  type: https://docs.verdantdock.example/problems/payment-declined,
                  title: Payment Required,
                  status: 402,
                  code: PAYMENT_DECLINED,
                  detail: Your bank declined the payment (05 Do not honour). You have not been charged.,
                  orderId: ord_q35f203vbexqe5sh4y4a,
                  orderNumber: VD-261011-RJXYE,
                  responseCode: "05",
                  correlationId: req-1234
                }
        "409":
          description: SLOT_UNAVAILABLE (lost the race for the last truck), CART_CHANGED, PRICE_CHANGED, REQUEST_IN_PROGRESS
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: VALIDATION_FAILED (card), CART_INVALID, ADDRESS_NOT_SERVICEABLE, INVALID_DELIVERY_SLOT, DELIVERY_NOT_POSSIBLE
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "428":
          description: IDEMPOTENCY_KEY_REQUIRED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/orders/{orderId}:
    get:
      operationId: getOrder
      tags:
        - Orders
      summary: One order
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Order
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
              example:
                {
                  orderId: ord_xwykyn0axh01bp3k8xvr,
                  orderNumber: VD-261025-KEHJJ,
                  status: CONFIRMED,
                  recordedStatus: CONFIRMED,
                  projected: false,
                  createdAt: 2026-10-25T20:58:19.594Z,
                  updatedAt: 2026-10-25T20:58:19.616Z,
                  lines:
                    [
                      {
                          sku: VD-AP-M,
                          name: AutoPlanter Medium,
                          type: PLANTER,
                          size: M,
                          quantity: 1,
                          unitPriceCents: 239000,
                          itemTotalCents: 239000,
                          cover: {},
                          lineTotalCents: 260600
                        }
                    ],
                  deliveryBreakdown: [ { code: FREIGHT, label: Truck delivery (20 km from our Dandenong South DC), amountCents: 13000 } ],
                  totals: { merchandiseCents: 424000, coverCents: 43800, deliveryCents: 37000, totalCents: 504800, gstCents: 45891, deliveryIncluded: true },
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  delivery:
                    {
                      method: TRUCK,
                      date: 2026-10-28,
                      window: AM,
                      windowLabel: Morning,
                      windowTimes: 7:00am - 12:00pm,
                      spaceUnits: 4,
                      jobMinutes: 210,
                      installMinutes: 110,
                      shippingKg: 89,
                      shippingM3: 2.73,
                      warehouse: DC-DAN,
                      status: BOOKED,
                      truck: T01,
                      booking: { truck: T01, window: AM, units: 4, reservations: [ {} ] }
                    },
                  payment:
                    {
                      transactionId: txn_pk491ngehh2yvm32,
                      brand: Visa,
                      last4: "1111",
                      expiry: 12/30,
                      token: tok_mdgb1krjhf2j8n290714,
                      amountCents: 504800,
                      approved: true,
                      responseCode: "00",
                      responseText: Approved,
                      authCode: Z9D96F,
                      processedAt: 2026-10-25T20:58:19.616Z,
                      gateway: VerdantPay sandbox
                    },
                  cancellable: true,
                  kind: PRODUCT,
                  replacementFor: null,
                  statusHistory: [ { status: PENDING_PAYMENT, at: 2026-10-25T20:58:19.594Z, simulated: false, businessDate: 2026-10-26, label: "Order received, awaiting payment" } ],
                  placedWithTestClockOffsetDays: 0,
                  policies: [],
                  checkin:
                    {
                      checkinId: chk_a9ngr9g42trr594bv8xv,
                      number: VD-261025-KEHJJ-EC,
                      kind: SERVICE,
                      title: Expert check-in (in-home visit),
                      description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                        health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                      status: NOT_ACTIVATED,
                      projected: false,
                      recordedStatus: NOT_ACTIVATED,
                      valueCents: 30000,
                      visitMinutes: 90,
                      parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                      parentOrderNumber: VD-261025-KEHJJ,
                      deliveryDate: 2026-10-28,
                      address:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: "Side gate, code 4821. Dog is friendly.",
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        },
                      booking: null,
                      createdAt: 2026-10-25T20:58:19.619Z,
                      bookableFrom: 2026-11-04,
                      bookableUntil: 2027-10-28,
                      canBook: true,
                      canCancelBooking: false,
                      history: [ {} ]
                    },
                  deliveryDetails:
                    {
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      editable: true,
                      reason: null,
                      editableUntil: 2026-10-27,
                      message: null,
                      history: []
                    },
                  invoice: { invoiceNumber: INV-261025-KEHJJ, filename: INV-261025-KEHJJ.pdf, notARealTaxInvoice: true, pdf: /v1/orders/ord_xwykyn0axh01bp3k8xvr/invoice.pdf }
                }
        "404":
          $ref: "#/components/responses/Problem"
  /v1/orders/{orderId}/delivery-details:
    patch:
      operationId: updateDeliveryDetails
      tags:
        - Orders
      summary: Change the recipient, contact phone or delivery instructions of a placed order
      description: Allowed while the order is on its way (CONFIRMED / DISPATCH_SCHEDULED) and the delivery date is after the business date (test clock included). On
        delivery day the order shows deliveryDetails.reason DELIVERY_DAY with a message to raise a support case, and this call returns 409. The date, window and
        address cannot be changed here (422). Every change is emailed and kept in deliveryDetails.history; the invoice keeps the recipient it was issued to.
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                recipientName:
                  type: string
                  minLength: 2
                  maxLength: 80
                phone:
                  type: string
                  example: 0412 345 678
                  description: Australian mobile
                instructions:
                  type: string
                  maxLength: 300
                  description: Empty string clears them
            example: { instructions: Ring the bell twice. Park in the driveway. }
      responses:
        "200":
          description: Updated order
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
              example:
                {
                  orderId: ord_xwykyn0axh01bp3k8xvr,
                  orderNumber: VD-261025-KEHJJ,
                  status: CONFIRMED,
                  recordedStatus: CONFIRMED,
                  projected: false,
                  createdAt: 2026-10-25T20:58:19.594Z,
                  updatedAt: 2026-10-25T20:58:19.803Z,
                  lines:
                    [
                      {
                          sku: VD-AP-M,
                          name: AutoPlanter Medium,
                          type: PLANTER,
                          size: M,
                          quantity: 1,
                          unitPriceCents: 239000,
                          itemTotalCents: 239000,
                          cover: {},
                          lineTotalCents: 260600
                        }
                    ],
                  deliveryBreakdown: [ { code: FREIGHT, label: Truck delivery (20 km from our Dandenong South DC), amountCents: 13000 } ],
                  totals: { merchandiseCents: 424000, coverCents: 43800, deliveryCents: 37000, totalCents: 504800, gstCents: 45891, deliveryIncluded: true },
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: Ring the bell twice. Park in the driveway.,
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  delivery:
                    {
                      method: TRUCK,
                      date: 2026-10-28,
                      window: AM,
                      windowLabel: Morning,
                      windowTimes: 7:00am - 12:00pm,
                      spaceUnits: 4,
                      jobMinutes: 210,
                      installMinutes: 110,
                      shippingKg: 89,
                      shippingM3: 2.73,
                      warehouse: DC-DAN,
                      status: BOOKED,
                      truck: T01,
                      booking: { truck: T01, window: AM, units: 4, reservations: [ {} ] }
                    },
                  payment:
                    {
                      transactionId: txn_pk491ngehh2yvm32,
                      brand: Visa,
                      last4: "1111",
                      expiry: 12/30,
                      token: tok_mdgb1krjhf2j8n290714,
                      amountCents: 504800,
                      approved: true,
                      responseCode: "00",
                      responseText: Approved,
                      authCode: Z9D96F,
                      processedAt: 2026-10-25T20:58:19.616Z,
                      gateway: VerdantPay sandbox
                    },
                  cancellable: true,
                  kind: PRODUCT,
                  replacementFor: null,
                  statusHistory: [ { status: PENDING_PAYMENT, at: 2026-10-25T20:58:19.594Z, simulated: false, businessDate: 2026-10-26, label: "Order received, awaiting payment" } ],
                  placedWithTestClockOffsetDays: 0,
                  policies: [],
                  checkin:
                    {
                      checkinId: chk_a9ngr9g42trr594bv8xv,
                      number: VD-261025-KEHJJ-EC,
                      kind: SERVICE,
                      title: Expert check-in (in-home visit),
                      description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                        health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                      status: NOT_ACTIVATED,
                      projected: false,
                      recordedStatus: NOT_ACTIVATED,
                      valueCents: 30000,
                      visitMinutes: 90,
                      parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                      parentOrderNumber: VD-261025-KEHJJ,
                      deliveryDate: 2026-10-28,
                      address:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: "Side gate, code 4821. Dog is friendly.",
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        },
                      booking: null,
                      createdAt: 2026-10-25T20:58:19.619Z,
                      bookableFrom: 2026-11-04,
                      bookableUntil: 2027-10-28,
                      canBook: true,
                      canCancelBooking: false,
                      history: [ {} ]
                    },
                  deliveryDetails:
                    {
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: Ring the bell twice. Park in the driveway.,
                      editable: true,
                      reason: null,
                      editableUntil: 2026-10-27,
                      message: null,
                      history: [ {} ]
                    },
                  invoice: { invoiceNumber: INV-261025-KEHJJ, filename: INV-261025-KEHJJ.pdf, notARealTaxInvoice: true, pdf: /v1/orders/ord_xwykyn0axh01bp3k8xvr/invoice.pdf }
                }
        "404":
          $ref: "#/components/responses/Problem"
        "409":
          description: DELIVERY_DETAILS_LOCKED (reason DELIVERY_DAY, DELIVERED, CANCELLED, NOT_ACTIVE), ORDER_CHANGED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: VALIDATION_FAILED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/orders/{orderId}/invoice.pdf:
    get:
      operationId: getInvoicePdf
      tags:
        - Orders
      summary: The order's invoice PDF (marked NOT A REAL TAX INVOICE) - byte-for-byte the copy attached to the confirmation email
      description: Paid orders only (free claim replacements and declined orders have none). Invoice number = order number with INV- in place of VD-. Prices include
        GST at 10%; GST = round(total / 11). The ABN printed is the ATO published example ABN, labelled as not issued to Verdant Dock. Text is uncompressed, and
        the document info carries InvoiceNumber, OrderNumber, TotalIncludingGst, Gst and Currency. X-Content-SHA256 is the SHA-256 of the file.
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: PDF
          headers:
            X-Content-SHA256:
              schema:
                type: string
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "404":
          description: ORDER_NOT_FOUND, NO_INVOICE
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/orders/{orderId}/cancel:
    post:
      operationId: cancelOrder
      tags:
        - Orders
      summary: Cancel (up to 2 business days before delivery) - releases the truck and refunds in full
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
            example: { reason: Changed my mind }
      responses:
        "200":
          description: Cancelled order
          content:
            application/json:
              schema:
                type: object
                properties:
                  orderId:
                    type: string
                  orderNumber:
                    type: string
                  status:
                    type: string
                  recordedStatus:
                    type: string
                  projected:
                    type: boolean
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  lines:
                    type: array
                    items: {}
                  deliveryBreakdown:
                    type: array
                    items: {}
                  totals:
                    type: object
                  address:
                    type: object
                  delivery:
                    type: object
                  payment:
                    type: object
                  refund:
                    type: object
                  cancellable:
                    type: boolean
                  kind:
                    type: string
                  replacementFor:
                    type: string
                    nullable: true
                  statusHistory:
                    type: array
                    items: {}
                  placedWithTestClockOffsetDays:
                    type: integer
                  policies:
                    type: array
                    items: {}
                  checkin:
                    type: string
                    nullable: true
                  deliveryDetails:
                    type: object
                  invoice:
                    type: object
              example:
                {
                  orderId: ord_vjnhd758df9hxe0d273p,
                  orderNumber: VD-261025-0X19C,
                  status: CANCELLED,
                  recordedStatus: CANCELLED,
                  projected: false,
                  createdAt: 2026-10-25T20:58:19.998Z,
                  updatedAt: 2026-10-25T20:58:20.075Z,
                  lines:
                    [
                      {
                          sku: VD-AP-S,
                          name: AutoPlanter Small,
                          type: PLANTER,
                          size: S,
                          quantity: 1,
                          unitPriceCents: 149000,
                          itemTotalCents: 149000,
                          cover: null,
                          lineTotalCents: 149000
                        }
                    ],
                  deliveryBreakdown: [ { code: FREIGHT, label: Truck delivery (20 km from our Dandenong South DC), amountCents: 13000 } ],
                  totals: { merchandiseCents: 149000, coverCents: 0, deliveryCents: 31000, totalCents: 180000, gstCents: 16364, deliveryIncluded: true },
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  delivery:
                    {
                      method: TRUCK,
                      date: 2026-11-23,
                      window: AM,
                      windowLabel: Morning,
                      windowTimes: 7:00am - 12:00pm,
                      spaceUnits: 1,
                      jobMinutes: 150,
                      installMinutes: 60,
                      shippingKg: 42,
                      shippingM3: 1.13,
                      warehouse: DC-DAN,
                      status: RELEASED,
                      truck: T01,
                      booking: { truck: T01, window: AM, units: 1, reservations: [ {} ] }
                    },
                  payment:
                    {
                      transactionId: txn_ztmg4hph8t45e2qk,
                      brand: Visa,
                      last4: "1111",
                      expiry: 12/30,
                      token: tok_9m16fyn4qhqbqd6atgt9,
                      amountCents: 180000,
                      approved: true,
                      responseCode: "00",
                      responseText: Approved,
                      authCode: PB5MY4,
                      processedAt: 2026-10-25T20:58:20.017Z,
                      gateway: VerdantPay sandbox
                    },
                  refund: { reference: rfd_kdbftr6xqjap, originalTransactionId: txn_ztmg4hph8t45e2qk, amountCents: 180000, approved: true, processedAt: 2026-10-25T20:58:20.075Z },
                  cancellable: false,
                  kind: PRODUCT,
                  replacementFor: null,
                  statusHistory: [ { status: PENDING_PAYMENT, at: 2026-10-25T20:58:19.998Z, simulated: false, businessDate: 2026-10-26, label: "Order received, awaiting payment" } ],
                  placedWithTestClockOffsetDays: 0,
                  policies: [],
                  checkin: null,
                  deliveryDetails:
                    {
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      editable: false,
                      reason: CANCELLED,
                      message: null,
                      history: []
                    },
                  invoice: { invoiceNumber: INV-261025-0X19C, filename: INV-261025-0X19C.pdf, notARealTaxInvoice: true, pdf: /v1/orders/ord_vjnhd758df9hxe0d273p/invoice.pdf }
                }
        "409":
          $ref: "#/components/responses/Problem"
  /v1/test-mailbox/latest:
    get:
      operationId: getLatestMail
      tags:
        - Testing
      security:
        - {}
        - mailboxKey: []
      summary: Latest mail received at an address on the stack's own domain (verification and reset codes)
      description: Only on stacks with inbound mail. No key needed unless the stack was deployed with MailboxAccess=key. Waits up to `wait` seconds (max 10) for a
        message newer than `since` and/or whose subject contains `subject`. The response includes the extracted 6-digit `code` and any links.
      parameters:
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: email
            example: ava@verdant-v1.testthis.now
          example: ava.example@verdant-v1.testthis.now
        - name: wait
          in: query
          schema:
            type: integer
            minimum: 0
            maximum: 10
          example: 10
        - name: since
          in: query
          schema:
            type: string
          description: ISO time or epoch milliseconds
        - name: subject
          in: query
          schema:
            type: string
          example: verify
      responses:
        "200":
          description: Message
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Mail"
              example:
                {
                  messageId: local-2,
                  to: ava.example@verdant-v1.testthis.now,
                  from: Verdant Dock <noreply@verdant-v1.testthis.now>,
                  subject: Verify your email - code 099914,
                  receivedAt: 2026-10-25T20:57:48.338Z,
                  code: "099914",
                  links: [],
                  text: "Hi Ava,


                    Use this code to verify your email address for Verdant Dock.


                    Code: 099914


                    It expires in 15 minutes. If you didn't sign up, ignore this email.",
                  html: '<!doctype html><html><body style="margin:0;background:#f3f5f1;font-family:Segoe UI,Helvetica,Arial,sans-serif;color:#1d2a22">

                    <table width="100%" cellpadding="0" cellspacing="0"><tr><td align="center" style="padding:24px...',
                  attachments: []
                }
          links:
            GetMailAttachment:
              operationId: getMailAttachment
              parameters:
                to: $request.query.to
                messageId: $response.body#/messageId
              description: Download an attachment (index 0 is the invoice PDF on order confirmations).
        "401":
          description: MAILBOX_KEY_INVALID (MailboxAccess=key only)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: MAILBOX_DISABLED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: NO_MAIL
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/test-mailbox/attachment:
    get:
      operationId: getMailAttachment
      tags:
        - Testing
      security:
        - {}
        - mailboxKey: []
      summary: One attachment of a received message - the file, or (format=text, PDFs) its extracted text
      description: Use the downloadUrl / textUrl given for each entry in a message's attachments. Attachments are kept while they fit (about 160 KB of base64 per
        message); larger ones are listed with stored false. format=text returns { pages, text, pageText[] } with lines in reading order (two spaces between
        items on a line), e.g. "GST (10%)  $183.09".
      parameters:
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: email
          example: ava.example@verdant-v1.testthis.now
        - name: messageId
          in: query
          required: true
          schema:
            type: string
          example: local-4
        - name: index
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
        - name: format
          in: query
          schema:
            type: string
            enum:
              - text
          example: text
      responses:
        "200":
          description: The file (its own content type) or JSON text
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            application/json:
              schema:
                type: object
              example:
                {
                  messageId: local-4,
                  index: 0,
                  filename: INV-261025-KEHJJ.pdf,
                  contentType: application/pdf,
                  size: 9030,
                  sha256: 3aa0c5208fbd6eec6d341d3e62c09aa25d989f834e0015967ec99f92a1c99112,
                  pages: 1,
                  text: "NOT A REAL TAX INVOICE

                    Verdant Dock

                    Demonstration document - no tax obligations

                    NOT A REAL TAX INVOICE

                    Verdant Dock is a fictional business used for software-testing practice. No goods were supplied, no real payment was ...",
                  pageText:
                    [
                      "NOT A REAL TAX INVOICE

                        Verdant Dock

                        Demonstration document - no tax obligations

                        NOT A REAL TAX INVOICE

                        Verdant Dock is a fictional business used for software-testing practice. No goods were supplied, no real payment was ..."
                    ]
                }
        "404":
          description: NO_MAIL, NO_ATTACHMENT, ATTACHMENT_NOT_STORED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: NOT_A_PDF, PDF_UNREADABLE (format=text)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/test-mailbox/messages:
    get:
      operationId: listMail
      tags:
        - Testing
      security:
        - {}
        - mailboxKey: []
      summary: Recent mail (newest first) for an address on the stack's own domain
      parameters:
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: email
          example: ava.example@verdant-v1.testthis.now
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 50
          example: 5
      responses:
        "200":
          description: Messages
          content:
            application/json:
              schema:
                type: object
                properties:
                  to:
                    type: string
                  messages:
                    type: array
                    items: {}
              example:
                {
                  to: ava.example@verdant-v1.testthis.now,
                  messages:
                    [
                      {
                          messageId: local-2,
                          to: ava.example@verdant-v1.testthis.now,
                          from: Verdant Dock <noreply@verdant-v1.testthis.now>,
                          subject: Verify your email - code 099914,
                          receivedAt: 2026-10-25T20:57:48.338Z,
                          code: "099914",
                          links: [],
                          text: "Hi Ava,


                            Use this code to verify your email address for Verdant Dock.


                            Code: 099914


                            It expires in 15 minutes. If you didn't sign up, ignore this email.",
                          html: '<!doctype html><html><body style="margin:0;background:#f3f5f1;font-family:Segoe UI,Helvetica,Arial,sans-serif;color:#1d2a22">

                            <table width="100%" cellpadding="0" cellspacing="0"><tr><td align="center" style="padding:24px...',
                          attachments: []
                        }
                    ]
                }
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
  /v1/policies:
    get:
      operationId: listPolicies
      tags:
        - Cover
      summary: Protection-plan policies (one per insured unit) with status for the business date
      description: Policies are issued when an order is really delivered - ACTIVE from the delivery date for the plan term, then EXPIRED. With the test clock set past
        an undelivered order's delivery date, its cover is included as projected (projected true, never saved). Claims are made through /v1/claims.
      responses:
        "200":
          description: Policies
          content:
            application/json:
              schema:
                type: object
                properties:
                  businessDate:
                    type: string
                    format: date
                  simulated:
                    type: boolean
                  summary:
                    type: object
                    properties:
                      active:
                        type: integer
                      expired:
                        type: integer
                      projected:
                        type: integer
                  uncoveredDeliveredItems:
                    type: array
                    items:
                      type: object
                  policies:
                    type: array
                    items:
                      $ref: "#/components/schemas/Policy"
                  claims:
                    type: object
              example:
                {
                  businessDate: 2026-11-07,
                  simulated: true,
                  summary: { active: 2, expired: 0, projected: 2 },
                  policies:
                    [
                      {
                          policyId: pol_c329a8e67492a63461db,
                          policyNumber: TC-C329A8E6,
                          status: ACTIVE,
                          planCode: TC-1Y,
                          planName: Thrive Cover 1 year,
                          termYears: 1,
                          summary: "If the plant fails for any reason - disease, pests, transplant shock or neglect - we replace it with one of the same size. Insure both an AutoPlanter
                            and a plant in the same order and you also get a free in-home expert check-in, worth $300.",
                          coveredSku: VD-PL-MT-M,
                          coveredItem: Monstera 'Thai Constellation' - Medium,
                          coveredType: PLANT,
                          unit: 1,
                          unitsOnLine: 1,
                          premiumCents: 22200,
                          orderId: ord_xwykyn0axh01bp3k8xvr,
                          orderNumber: VD-261025-KEHJJ,
                          startDate: 2026-10-28,
                          endDate: 2027-10-27,
                          daysRemaining: 355,
                          projected: true,
                          claims: { available: true, page: "#/claims" }
                        }
                    ],
                  uncoveredDeliveredItems: [],
                  claims: { available: true, page: "#/claims", api: /v1/claims }
                }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/checkins/{checkinId}:
    parameters:
      - name: checkinId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getCheckin
      tags:
        - Services
      summary: Free expert check-in earned by insuring both an AutoPlanter and a plant in one order
      description: Created with the order as NOT_ACTIVATED (listed in GET /v1/orders with kind SERVICE). BOOKED once a visit is chosen; COMPLETED after the visit
        date; EXPIRED if not booked within 12 months of delivery; VOID if the order is cancelled. COMPLETED/EXPIRED are judged against the business date
        (projected true under the test clock).
      responses:
        "200":
          description: Check-in
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Checkin"
              example:
                {
                  checkinId: chk_a9ngr9g42trr594bv8xv,
                  number: VD-261025-KEHJJ-EC,
                  kind: SERVICE,
                  title: Expert check-in (in-home visit),
                  description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                    health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                  status: NOT_ACTIVATED,
                  projected: false,
                  recordedStatus: NOT_ACTIVATED,
                  valueCents: 30000,
                  visitMinutes: 90,
                  parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                  parentOrderNumber: VD-261025-KEHJJ,
                  deliveryDate: 2026-10-28,
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  booking: null,
                  createdAt: 2026-10-25T20:58:19.619Z,
                  bookableFrom: 2026-11-10,
                  bookableUntil: 2027-10-28,
                  canBook: true,
                  canCancelBooking: false,
                  history: [ { status: NOT_ACTIVATED, at: 2026-10-25T20:58:19.620Z, label: Earned with order - not yet booked } ]
                }
        "404":
          $ref: "#/components/responses/Problem"
  /v1/checkins/{checkinId}/slots:
    parameters:
      - name: checkinId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: listCheckinSlots
      tags:
        - Services
      summary: Open visit windows (business days, AM 8-12 / PM 12-4) from 7 days after delivery, at least 2 business days ahead
      responses:
        "200":
          description: Availability
          content:
            application/json:
              schema:
                type: object
                properties:
                  checkinId:
                    type: string
                  visitMinutes:
                    type: integer
                  firstDate:
                    type: string
                    format: date
                  lastDate:
                    type: string
                    format: date
                  bookableUntil:
                    type: string
                    format: date
                  days:
                    type: array
                    items: {}
              example:
                {
                  checkinId: chk_a9ngr9g42trr594bv8xv,
                  visitMinutes: 90,
                  firstDate: 2026-11-10,
                  lastDate: 2027-02-05,
                  bookableUntil: 2027-10-28,
                  days:
                    [
                      {
                          date: 2026-11-10,
                          dayOfWeek: Tuesday,
                          closed: false,
                          closedReason: null,
                          windows: [ { code: AM, label: Morning, times: 8:00am - 12:00pm, available: true, expertsAvailable: 4 } ]
                        }
                    ]
                }
        "409":
          $ref: "#/components/responses/Problem"
  /v1/checkins/{checkinId}/book:
    parameters:
      - name: checkinId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: bookCheckin
      tags:
        - Services
      summary: Book the visit (emails a confirmation)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - date
                - window
              properties:
                date:
                  type: string
                  format: date
                window:
                  type: string
                  enum:
                    - AM
                    - PM
            example: { date: 2026-11-10, window: AM }
      responses:
        "200":
          description: Booked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Checkin"
              example:
                {
                  checkinId: chk_a9ngr9g42trr594bv8xv,
                  number: VD-261025-KEHJJ-EC,
                  kind: SERVICE,
                  title: Expert check-in (in-home visit),
                  description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                    health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                  status: BOOKED,
                  projected: false,
                  recordedStatus: BOOKED,
                  valueCents: 30000,
                  visitMinutes: 90,
                  parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                  parentOrderNumber: VD-261025-KEHJJ,
                  deliveryDate: 2026-10-28,
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  booking: { date: 2026-11-10, window: AM, windowLabel: Morning, windowTimes: 8:00am - 12:00pm, bookedAt: 2026-10-25T20:58:20.563Z, bookedUnderTestClock: true },
                  createdAt: 2026-10-25T20:58:19.619Z,
                  bookableFrom: null,
                  bookableUntil: 2027-10-28,
                  canBook: false,
                  canCancelBooking: true,
                  history: [ { status: NOT_ACTIVATED, at: 2026-10-25T20:58:19.620Z, label: Earned with order - not yet booked } ]
                }
        "409":
          description: SLOT_UNAVAILABLE (window full), CHECKIN_NOT_BOOKABLE
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: INVALID_VISIT_SLOT
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/checkins/{checkinId}/cancel-booking:
    parameters:
      - name: checkinId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: cancelCheckinBooking
      tags:
        - Services
      summary: Cancel the booked visit (up to 1 business day before) - back to NOT_ACTIVATED
      responses:
        "200":
          description: Check-in
          content:
            application/json:
              schema:
                type: object
                properties:
                  checkinId:
                    type: string
                  number:
                    type: string
                  kind:
                    type: string
                  title:
                    type: string
                  description:
                    type: string
                  status:
                    type: string
                  projected:
                    type: boolean
                  recordedStatus:
                    type: string
                  valueCents:
                    type: integer
                  visitMinutes:
                    type: integer
                  parentOrderId:
                    type: string
                  parentOrderNumber:
                    type: string
                  deliveryDate:
                    type: string
                    format: date
                  address:
                    type: object
                  booking:
                    type: string
                    nullable: true
                  createdAt:
                    type: string
                    format: date-time
                  bookableFrom:
                    type: string
                    format: date
                  bookableUntil:
                    type: string
                    format: date
                  canBook:
                    type: boolean
                  canCancelBooking:
                    type: boolean
                  history:
                    type: array
                    items: {}
              example:
                {
                  checkinId: chk_a9ngr9g42trr594bv8xv,
                  number: VD-261025-KEHJJ-EC,
                  kind: SERVICE,
                  title: Expert check-in (in-home visit),
                  description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                    health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                  status: NOT_ACTIVATED,
                  projected: false,
                  recordedStatus: NOT_ACTIVATED,
                  valueCents: 30000,
                  visitMinutes: 90,
                  parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                  parentOrderNumber: VD-261025-KEHJJ,
                  deliveryDate: 2026-10-28,
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  booking: null,
                  createdAt: 2026-10-25T20:58:19.619Z,
                  bookableFrom: 2026-11-10,
                  bookableUntil: 2027-10-28,
                  canBook: true,
                  canCancelBooking: false,
                  history: [ { status: NOT_ACTIVATED, at: 2026-10-25T20:58:19.620Z, label: Earned with order - not yet booked } ]
                }
        "409":
          $ref: "#/components/responses/Problem"
  /v1/chat:
    get:
      operationId: getChatStatus
      tags:
        - Assistant
      summary: Assistant mode (openai | demo), model, notice, and the exact context it is given for this customer
      responses:
        "200":
          description: mode, model, notice, context
          content:
            application/json:
              schema:
                type: object
                properties:
                  mode:
                    type: string
                  model:
                    type: string
                    nullable: true
                  conversation:
                    type: string
                    nullable: true
                  testClock:
                    type: string
                    nullable: true
                  idleMinutes:
                    type: integer
                  notice:
                    type: string
                  context:
                    type: object
              example:
                {
                  mode: demo,
                  model: null,
                  conversation: null,
                  testClock: null,
                  idleMinutes: 30,
                  notice: "Demo mode: no AI model is configured, so answers are simple summaries of your account.",
                  context:
                    {
                      generatedAt: 2026-10-25T20:58:21.085Z,
                      today: 2026-10-26,
                      realToday: 2026-10-26,
                      testClock: null,
                      customer:
                        {
                          name: Ava Nguyen,
                          givenName: Ava,
                          email: ava.example@verdant-v1.testthis.now,
                          mobile: "+61412345678",
                          memberSince: 2026-10-25T20:56:38.293Z,
                          marketingEmails: true,
                          twoStepVerification: true
                        },
                      addresses: [ {} ],
                      cart: { items: [], subtotal: null, problems: [] },
                      orders: [ {} ],
                      cover: { policies: [], deliveredItemsWithoutCover: [], page: "#/cover" },
                      expertCheckins: [ {} ],
                      supportCases: [ {} ],
                      claims: [ {} ]
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
    post:
      operationId: chat
      tags:
        - Assistant
      summary: Ask the assistant. Send the conversation (last 12 turns used); the last message must be from the user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - messages
              properties:
                conversationId:
                  type: string
                  description: From the previous reply; omit to start a conversation. A different or ended id starts a new one (the old one is filed - see previous).
                messages:
                  type: array
                  items:
                    type: object
                    required:
                      - role
                      - content
                    properties:
                      role:
                        type: string
                        enum:
                          - user
                          - assistant
                      content:
                        type: string
                        description: user messages up to 1000 characters
            example: { messages: [ { role: user, content: When is my delivery? } ] }
      responses:
        "200":
          description: "reply, draft (null, or a support case the assistant prepared for the customer to review and send via POST /v1/cases: {type, orderId, orderNumber,
            subject, message} - nothing is sent), mode, model, usage, conversationId, previous ({conversationId, caseId} of a conversation filed on the way),
            testClock ({offsetDays, today, note} - dates ending in * are altered by the test clock)"
          content:
            application/json:
              schema:
                type: object
                properties:
                  reply:
                    type: string
                  draft:
                    type: string
                    nullable: true
                  mode:
                    type: string
                  model:
                    type: string
                    nullable: true
                  usage:
                    type: string
                    nullable: true
                  conversationId:
                    type: string
                  previous:
                    type: string
                    nullable: true
                  testClock:
                    type: string
                    nullable: true
              example:
                {
                  reply: "Your upcoming deliveries:

                    - VD-261025-KEHJJ: 2026-10-28, Morning (7:00am - 12:00pm) - confirmed #/orders/ord_xwykyn0axh01bp3k8xvr",
                  draft: null,
                  mode: demo,
                  model: null,
                  usage: null,
                  conversationId: chat_6mk2kn4najhqxh,
                  previous: null,
                  testClock: null
                }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
        "429":
          description: RATE_LIMITED (40/hour, 200/day per customer) or CHAT_DAILY_LIMIT (stack-wide ChatDailyLimit)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "503":
          description: ASSISTANT_UNAVAILABLE - the model API failed or timed out (detail.reason)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/chat/end:
    post:
      operationId: endChat
      tags:
        - Assistant
      summary: End the conversation - the transcript is filed as a CLOSED support case (type CHAT) and emailed to the customer. Idempotent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - conversationId
              properties:
                conversationId:
                  type: string
            example: { conversationId: chat_6mk2kn4najhqxh }
      responses:
        "200":
          description: conversationId, caseId (null when no question was asked), saved, page
          content:
            application/json:
              schema:
                type: object
                properties:
                  conversationId:
                    type: string
                  caseId:
                    type: string
                  saved:
                    type: boolean
                  page:
                    type: string
              example: { conversationId: chat_6mk2kn4najhqxh, caseId: C-261025-YY5AX, saved: true, page: "#/support/C-261025-YY5AX" }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/cases/{caseId}/reopen:
    parameters:
      - name: caseId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: reopenCase
      tags:
        - Support
      summary: Reopen a closed case - customers can for 7 days after it was closed (real time; see reopenableUntil / canReopen on the case)
      responses:
        "200":
          description: The case
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: object
                    nullable: true
                  assignedStaff:
                    type: object
                    nullable: true
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    nullable: true
                  reopenableUntil:
                    type: string
                    nullable: true
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-WYNPA,
                  type: ENQUIRY,
                  subject: Gift card?,
                  status: OPEN,
                  channel: WEB,
                  initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: null,
                  assignedStaff: null,
                  createdAt: 2026-10-25T20:58:20.913Z,
                  updatedAt: 2026-10-25T20:58:20.972Z,
                  closedAt: null,
                  reopenableUntil: null,
                  canReopen: false,
                  messageCount: 2,
                  lastMessageAt: 2026-10-25T20:58:20.928Z,
                  lastMessageBy: customer,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_9q8qdm86bd,
                          at: 2026-10-25T20:58:20.914Z,
                          kind: MESSAGE,
                          author: { type: customer, id: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen },
                          channel: WEB,
                          body: Do you sell gift cards?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.example@verdant-v1.testthis.now, messageId: local-15 }
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "409":
          description: CASE_NOT_CLOSED or CASE_REOPEN_EXPIRED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/test-clock:
    get:
      operationId: getTestClock
      tags:
        - Testing
      summary: Current test-clock state for this session
      responses:
        "200":
          description: Test clock
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TestClock"
              example: { enabled: true, offsetDays: 0, simulated: false, realDate: 2026-10-26, businessDate: 2026-10-26, maxOffsetDays: 3650, notice: null, resetsOn: login }
        4XX:
          $ref: "#/components/responses/Problem"
    put:
      operationId: setTestClock
      tags:
        - Testing
      summary: Move this session's business date n days ahead (resets at next login)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - offsetDays
              properties:
                offsetDays:
                  type: integer
                  minimum: 0
                  maximum: 3650
            example: { offsetDays: 12 }
      responses:
        "200":
          description: Test clock
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TestClock"
              example:
                {
                  enabled: true,
                  offsetDays: 12,
                  simulated: true,
                  realDate: 2026-10-26,
                  businessDate: 2026-11-07,
                  maxOffsetDays: 3650,
                  notice: "TEST MODE: business dates are simulated 12 day(s) ahead. Data is being manipulated for testing purposes.",
                  resetsOn: login
                }
        "403":
          description: TEST_CLOCK_DISABLED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          $ref: "#/components/responses/Problem"
    delete:
      operationId: resetTestClock
      tags:
        - Testing
      summary: Reset this session to the real date
      responses:
        "200":
          description: Test clock
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TestClock"
              example: { enabled: true, offsetDays: 0, simulated: false, realDate: 2026-10-26, businessDate: 2026-10-26, maxOffsetDays: 3650, notice: null, resetsOn: login }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/staff/enrol:
    post:
      operationId: staffEnrol
      tags:
        - Staff enrolment
      security: []
      summary: Staff enrolment; emails a 6-digit confirmation code (202)
      description: Same rules as customer sign-up (no mobile). Subject of the email is "Confirm your staff enrolment - code NNNNNN".
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                givenName:
                  type: string
                familyName:
                  type: string
                jobTitle:
                  type: string
                acceptTerms:
                  type: boolean
            example: { email: sam.staff@verdant-v1.testthis.now, password: Plant3r!Dock2026, givenName: Sam, familyName: Staff, jobTitle: Customer service, acceptTerms: true }
      responses:
        "200":
          description: OK
        "202":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  email:
                    type: string
                  message:
                    type: string
                  codeExpiresInSeconds:
                    type: integer
              example:
                {
                  status: VERIFICATION_SENT,
                  email: sam.staff@verdant-v1.testthis.now,
                  message: Check your email for a 6-digit verification code.,
                  codeExpiresInSeconds: 900
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/enrol/resend:
    post:
      operationId: staffResendVerification
      tags:
        - Staff enrolment
      security: []
      summary: Resend the enrolment code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
            example: { email: sam.staff@verdant-v1.testthis.now }
      responses:
        "200":
          description: OK
        "202":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  email:
                    type: string
                  message:
                    type: string
              example: { status: VERIFICATION_SENT, email: sam.staff@verdant-v1.testthis.now, message: "If that account is awaiting verification, a new code is on its way." }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/verify-email:
    post:
      operationId: staffVerifyEmail
      tags:
        - Staff enrolment
      security: []
      summary: Confirm the enrolment code; returns an enrolmentToken (scope staff_mfa_enrol)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                code:
                  type: string
            example: { email: sam.staff@verdant-v1.testthis.now, code: "500585" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  enrolmentToken:
                    type: string
                  expiresIn:
                    type: integer
                  message:
                    type: string
              example:
                {
                  status: MFA_ENROLMENT_REQUIRED,
                  enrolmentToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  message: Email verified. Set up your authenticator app to finish.
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/mfa/enrol:
    post:
      operationId: staffStartMfaEnrolment
      tags:
        - Staff enrolment
      security:
        - staffEnrolment: []
      summary: Start TOTP enrolment (secret, otpauth URI, QR SVG)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret:
                    type: string
                  otpauthUri:
                    type: string
                  qrSvg:
                    type: string
                  algorithm:
                    type: string
                  digits:
                    type: integer
                  period:
                    type: integer
                  message:
                    type: string
              example:
                {
                  secret: RYIR7QEPC5HIWNZLTYBP7HWNXBPUNVTC,
                  otpauthUri: otpauth://totp/Verdant%20Dock%20Staff%3Asam.staff%40verdant-v1.testthis.now?secret=RYIR7QEPC5HIWNZLTYBP7HWNXBPUNVTC&issuer=Verdant%20Dock%20Staff&algorithm=SHA1&digits=6&period=30,
                  qrSvg: <svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>,
                  algorithm: SHA1,
                  digits: 6,
                  period: 30,
                  message: "Scan the QR code (or type the secret) into an authenticator app, then confirm with a 6-digit code."
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/mfa/activate:
    post:
      operationId: staffActivateMfa
      tags:
        - Staff enrolment
      security:
        - staffEnrolment: []
      summary: Activate TOTP; returns staff tokens and recovery codes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code:
                  type: string
            example: { code: "613914" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  recoveryCodes:
                    type: array
                    items: {}
                  tokenType:
                    type: string
                  accessToken:
                    type: string
                  expiresIn:
                    type: integer
                  refreshToken:
                    type: string
                  refreshExpiresIn:
                    type: integer
                  user:
                    type: object
              example:
                {
                  status: ACTIVE,
                  recoveryCodes: [ GMP9-0KEG, Y2AQ-42GC, K82K-86VW, PZW5-5T3K, 3ADD-J1F8, 7H5R-PBC1, NV05-4T5R, WVB1-DYD9 ],
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: stf_gb5xdjbjr28f88htr0xw.ses_30c2nbdfmty...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: stf_gb5xdjbjr28f88htr0xw,
                      email: sam.staff@verdant-v1.testthis.now,
                      givenName: Sam,
                      familyName: Staff,
                      status: ACTIVE,
                      mfaEnabled: true,
                      role: STAFF,
                      jobTitle: Customer service,
                      apps: [ staff ],
                      marketingOptIn: false,
                      createdAt: 2026-10-25T20:58:21.229Z
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/login:
    post:
      operationId: staffLogin
      tags:
        - Staff enrolment
      security: []
      summary: Staff sign-in step 1 (password) -> MFA_REQUIRED with mfaToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                password:
                  type: string
            example: { email: sam.staff@verdant-v1.testthis.now, password: Plant3r!Dock2026 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  mfaToken:
                    type: string
                  expiresIn:
                    type: integer
                  methods:
                    type: array
                    items: {}
              example: { status: MFA_REQUIRED, mfaToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp..., expiresIn: 300, methods: [ totp, recovery_code ] }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/login/mfa:
    post:
      operationId: staffLoginMfa
      tags:
        - Staff enrolment
      security: []
      summary: Staff sign-in step 2 (TOTP or recovery code) -> tokens
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mfaToken:
                  type: string
                code:
                  type: string
                recoveryCode:
                  type: string
            example: { mfaToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp..., code: "399240" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  tokenType:
                    type: string
                  accessToken:
                    type: string
                  expiresIn:
                    type: integer
                  refreshToken:
                    type: string
                  refreshExpiresIn:
                    type: integer
                  user:
                    type: object
                  recoveryCodesRemaining:
                    type: integer
              example:
                {
                  status: AUTHENTICATED,
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: stf_gb5xdjbjr28f88htr0xw.ses_4c11qw6ffmb...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: stf_gb5xdjbjr28f88htr0xw,
                      email: sam.staff@verdant-v1.testthis.now,
                      givenName: Sam,
                      familyName: Staff,
                      status: ACTIVE,
                      mfaEnabled: true,
                      role: STAFF,
                      jobTitle: Customer service,
                      apps: [ staff ],
                      marketingOptIn: false,
                      createdAt: 2026-10-25T20:58:21.229Z
                    },
                  recoveryCodesRemaining: 8
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/token/refresh:
    post:
      operationId: staffRefreshToken
      tags:
        - Staff enrolment
      security: []
      summary: Rotate a staff refresh token (customer refresh tokens are rejected)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                refreshToken:
                  type: string
            example: { refreshToken: stf_gb5xdjbjr28f88htr0xw.ses_4c11qw6ffmb... }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokenType:
                    type: string
                  accessToken:
                    type: string
                  expiresIn:
                    type: integer
                  refreshToken:
                    type: string
                  refreshExpiresIn:
                    type: integer
                  user:
                    type: object
              example:
                {
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: stf_gb5xdjbjr28f88htr0xw.ses_4c11qw6ffmb...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: stf_gb5xdjbjr28f88htr0xw,
                      email: sam.staff@verdant-v1.testthis.now,
                      givenName: Sam,
                      familyName: Staff,
                      status: ACTIVE,
                      mfaEnabled: true,
                      role: STAFF,
                      jobTitle: Customer service,
                      apps: [ staff ],
                      marketingOptIn: false,
                      createdAt: 2026-10-25T20:58:21.229Z
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/password/forgot:
    post:
      operationId: staffForgotPassword
      tags:
        - Staff enrolment
      security: []
      summary: Email a staff password reset code (202)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
            example: { email: wes.warehouse@verdant-v1.testthis.now }
      responses:
        "200":
          description: OK
        "202":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string
                  codeExpiresInSeconds:
                    type: integer
              example: { status: RESET_CODE_SENT, message: "If an account exists for that email, a reset code is on its way.", codeExpiresInSeconds: 900 }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/password/reset:
    post:
      operationId: staffResetPassword
      tags:
        - Staff enrolment
      security: []
      summary: Reset a staff password with the emailed code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                code:
                  type: string
                newPassword:
                  type: string
            example: { email: wes.warehouse@verdant-v1.testthis.now, code: "333873", newPassword: N3w!Planter2026 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string
              example: { status: PASSWORD_RESET, message: Your password has been changed. Sign in with your new password and authenticator code. }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/logout:
    post:
      operationId: staffLogout
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Sign out (?all=true for every session)
      responses:
        "200":
          description: OK
        "204":
          description: OK
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/me:
    get:
      operationId: staffGetMe
      tags:
        - Staff portal
      security:
        - staff: []
      summary: The staff user, their IP history and networks
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  userId:
                    type: string
                  email:
                    type: string
                  givenName:
                    type: string
                  familyName:
                    type: string
                  status:
                    type: string
                  mfaEnabled:
                    type: boolean
                  role:
                    type: string
                  jobTitle:
                    type: string
                  apps:
                    type: array
                    items: {}
                  marketingOptIn:
                    type: boolean
                  createdAt:
                    type: string
                    format: date-time
                  lastLoginAt:
                    type: string
                    format: date-time
                  recoveryCodesRemaining:
                    type: integer
                  ipHistory:
                    type: array
                    items: {}
                  networks:
                    type: array
                    items: {}
                  visibility:
                    type: string
              example:
                {
                  userId: stf_gb5xdjbjr28f88htr0xw,
                  email: sam.staff@verdant-v1.testthis.now,
                  givenName: Sam,
                  familyName: Staff,
                  status: ACTIVE,
                  mfaEnabled: true,
                  role: STAFF,
                  jobTitle: Customer service,
                  apps: [ staff ],
                  marketingOptIn: false,
                  createdAt: 2026-10-25T20:58:21.229Z,
                  lastLoginAt: 2026-10-25T21:00:02.309Z,
                  recoveryCodesRemaining: 8,
                  ipHistory: [ { ip: 203.0.113.77, net: 203.0.113.77, firstSeen: 2026-10-25T20:58:21.239Z, lastSeen: 2026-10-25T21:00:02.310Z, hits: 6 } ],
                  networks: [ 203.0.113.77 ],
                  visibility: "You see customers who have used Verdant Dock from the same network (IP address) as you, and all activity from those networks."
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/overview:
    get:
      operationId: staffOverview
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Counts, upcoming deliveries and recent orders
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  today:
                    type: string
                    format: date
                  counts:
                    type: object
                  ordersByStatus:
                    type: object
                  upcomingDeliveries:
                    type: array
                    items: {}
                  recentOrders:
                    type: array
                    items: {}
                  networks:
                    type: array
                    items: {}
              example:
                {
                  today: 2026-10-26,
                  counts:
                    {
                      customers: 1,
                      orders: 4,
                      openOrders: 1,
                      deliveriesNext7Days: 1,
                      checkinsToBook: 1,
                      checkinsBooked: 0,
                      requests24h: 93,
                      failedLogins24h: 0,
                      openCases: 3
                    },
                  ordersByStatus: { CANCELLED: 2, PAYMENT_DECLINED: 1, CONFIRMED: 1 },
                  upcomingDeliveries:
                    [
                      {
                          orderId: ord_xwykyn0axh01bp3k8xvr,
                          orderNumber: VD-261025-KEHJJ,
                          status: CONFIRMED,
                          createdAt: 2026-10-25T20:58:19.594Z,
                          updatedAt: 2026-10-25T20:58:19.803Z,
                          customer: {},
                          totalCents: 504800,
                          itemCount: 2,
                          items: [ 1 × AutoPlanter Medium, 1 × Monstera 'Thai Constellation' - Medium ],
                          coveredLines: 2,
                          coverCents: 43800,
                          delivery: {},
                          payment: {},
                          failureReason: null,
                          checkin: {},
                          placedWithTestClockOffsetDays: 0
                        }
                    ],
                  recentOrders:
                    [
                      {
                          orderId: ord_zp5a2yggrx14j89xgtv0,
                          orderNumber: VD-261025-9RPDP,
                          status: CANCELLED,
                          createdAt: 2026-10-25T20:58:20.162Z,
                          updatedAt: 2026-10-25T20:58:20.252Z,
                          customer: {},
                          totalCents: 11400,
                          itemCount: 1,
                          items: [ 1 × AutoPlanter Coffee Table Book ],
                          coveredLines: 0,
                          coverCents: 0,
                          delivery: {},
                          payment: {},
                          failureReason: null,
                          checkin: null,
                          placedWithTestClockOffsetDays: 0
                        }
                    ],
                  networks: [ 203.0.113.77 ]
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/customers:
    get:
      operationId: staffListCustomers
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Customers in view
      parameters:
        - name: q
          in: query
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  customers:
                    type: array
                    items: {}
                  count:
                    type: integer
              example:
                {
                  customers:
                    [
                      {
                          userId: usr_btgygbap02w2g1cd3qcn,
                          name: Ava Nguyen,
                          givenName: Ava,
                          familyName: Nguyen,
                          email: ava.example@verdant-v1.testthis.now,
                          mobile: "+61412345678",
                          status: ACTIVE,
                          mfaEnabled: true,
                          locked: false,
                          createdAt: 2026-10-25T20:56:38.293Z,
                          lastLoginAt: 2026-10-25T20:58:19.423Z,
                          orderCount: 4,
                          openOrderCount: 1,
                          totalSpentCents: 504800,
                          matchedOn: [ { ip: 203.0.113.77, net: 203.0.113.77, lastSeen: 2026-10-25T20:58:21.212Z } ]
                        }
                    ],
                  count: 1
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/customers/{userId}:
    get:
      operationId: staffGetCustomer
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Customer detail - profile, IP history, addresses, orders, policies, check-ins, security events, activity
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  customer:
                    type: object
                  ipHistory:
                    type: array
                    items: {}
                  addresses:
                    type: array
                    items: {}
                  orders:
                    type: array
                    items: {}
                  checkins:
                    type: array
                    items: {}
                  policies:
                    type: array
                    items: {}
                  securityEvents:
                    type: array
                    items: {}
                  activity:
                    type: array
                    items: {}
                  cases:
                    type: array
                    items: {}
              example:
                {
                  customer:
                    {
                      userId: usr_btgygbap02w2g1cd3qcn,
                      name: Ava Nguyen,
                      givenName: Ava,
                      familyName: Nguyen,
                      email: ava.example@verdant-v1.testthis.now,
                      mobile: "+61412345678",
                      status: ACTIVE,
                      mfaEnabled: true,
                      locked: false,
                      createdAt: 2026-10-25T20:56:38.293Z,
                      lastLoginAt: 2026-10-25T20:58:19.423Z,
                      orderCount: 4,
                      openOrderCount: 1,
                      totalSpentCents: 504800,
                      matchedOn: [ {} ]
                    },
                  ipHistory: [ { ip: 203.0.113.77, net: 203.0.113.77, firstSeen: 2026-10-25T20:56:38.327Z } ],
                  addresses:
                    [
                      {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: "Side gate, code 4821. Dog is friendly.",
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        }
                    ],
                  orders:
                    [
                      {
                          orderId: ord_zp5a2yggrx14j89xgtv0,
                          orderNumber: VD-261025-9RPDP,
                          status: CANCELLED,
                          createdAt: 2026-10-25T20:58:20.162Z,
                          updatedAt: 2026-10-25T20:58:20.252Z,
                          customer: {},
                          totalCents: 11400,
                          itemCount: 1,
                          items: [ 1 × AutoPlanter Coffee Table Book ],
                          coveredLines: 0,
                          coverCents: 0,
                          delivery: {},
                          payment: {},
                          failureReason: null,
                          checkin: null,
                          placedWithTestClockOffsetDays: 0
                        }
                    ],
                  checkins:
                    [
                      {
                          checkinId: chk_a9ngr9g42trr594bv8xv,
                          number: VD-261025-KEHJJ-EC,
                          kind: SERVICE,
                          title: Expert check-in (in-home visit),
                          description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                            health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                          status: NOT_ACTIVATED,
                          projected: false,
                          recordedStatus: NOT_ACTIVATED,
                          valueCents: 30000,
                          visitMinutes: 90,
                          parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                          parentOrderNumber: VD-261025-KEHJJ,
                          deliveryDate: 2026-10-28,
                          address: {},
                          booking: null,
                          createdAt: 2026-10-25T20:58:19.619Z,
                          bookableFrom: 2026-11-04,
                          bookableUntil: 2027-10-28,
                          canBook: true,
                          canCancelBooking: false,
                          history: [ {} ]
                        }
                    ],
                  policies: [],
                  securityEvents: [ { type: TEST_CLOCK_RESET, at: 2026-10-25T20:58:20.851Z, ip: 203.0.113.77, userAgent: node-test, detail: {} } ],
                  activity:
                    [
                      {
                          at: 2026-10-25T21:00:02.397Z,
                          ip: 203.0.113.77,
                          net: 203.0.113.77,
                          method: GET,
                          path: /v1/me,
                          status: 200,
                          ms: 42,
                          actorType: customer,
                          actorId: usr_btgygbap02w2g1cd3qcn,
                          subjectId: null,
                          userAgent: node-test,
                          correlationId: req-0.680378018032652,
                          traceId: null
                        }
                    ],
                  cases:
                    [
                      {
                          caseId: C-261025-YY5AX,
                          type: CHAT,
                          subject: Chat with the assistant - 26 Oct 2026,
                          status: CLOSED,
                          channel: CHAT,
                          initiator: {},
                          userId: usr_btgygbap02w2g1cd3qcn,
                          customer: {},
                          order: null,
                          assignedStaff: null,
                          createdAt: 2026-10-25T20:58:21.193Z,
                          updatedAt: 2026-10-25T20:58:21.193Z,
                          closedAt: 2026-10-25T20:58:21.193Z,
                          reopenableUntil: 2026-11-01T20:58:21.193Z,
                          canReopen: true,
                          chat: {},
                          messageCount: 1,
                          lastMessageAt: 2026-10-25T20:58:21.193Z,
                          lastMessageBy: customer,
                          nets: [ 203.0.113.77 ]
                        }
                    ]
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/customers/{userId}/orders/{orderId}:
    get:
      operationId: staffGetOrder
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Order detail (recorded state; card token never shown)
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  orderId:
                    type: string
                  orderNumber:
                    type: string
                  status:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  customer:
                    type: object
                  totalCents:
                    type: integer
                  itemCount:
                    type: integer
                  items:
                    type: array
                    items: {}
                  coveredLines:
                    type: integer
                  coverCents:
                    type: integer
                  delivery:
                    type: object
                  payment:
                    type: object
                  failureReason:
                    type: string
                    nullable: true
                  checkin:
                    type: object
                  placedWithTestClockOffsetDays:
                    type: integer
                  lines:
                    type: array
                    items: {}
                  totals:
                    type: object
                  deliveryBreakdown:
                    type: array
                    items: {}
                  address:
                    type: object
                  refund:
                    type: string
                    nullable: true
                  deliveryDetailsHistory:
                    type: array
                    items: {}
                  statusHistory:
                    type: array
                    items: {}
                  policies:
                    type: array
                    items: {}
                  invoice:
                    type: object
              example:
                {
                  orderId: ord_xwykyn0axh01bp3k8xvr,
                  orderNumber: VD-261025-KEHJJ,
                  status: CONFIRMED,
                  createdAt: 2026-10-25T20:58:19.594Z,
                  updatedAt: 2026-10-25T20:58:19.803Z,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  totalCents: 504800,
                  itemCount: 2,
                  items: [ 1 × AutoPlanter Medium, 1 × Monstera 'Thai Constellation' - Medium ],
                  coveredLines: 2,
                  coverCents: 43800,
                  delivery:
                    {
                      method: TRUCK,
                      date: 2026-10-28,
                      window: AM,
                      windowLabel: Morning,
                      windowTimes: 7:00am - 12:00pm,
                      spaceUnits: 4,
                      jobMinutes: 210,
                      installMinutes: 110,
                      shippingKg: 89,
                      shippingM3: 2.73,
                      warehouse: DC-DAN,
                      status: BOOKED,
                      truck: T01,
                      booking: { truck: T01, window: AM, units: 4, reservations: [ {} ] }
                    },
                  payment:
                    {
                      brand: Visa,
                      last4: "1111",
                      expiry: 12/30,
                      amountCents: 504800,
                      approved: true,
                      responseCode: "00",
                      responseText: Approved,
                      authCode: Z9D96F,
                      transactionId: txn_pk491ngehh2yvm32,
                      processedAt: 2026-10-25T20:58:19.616Z
                    },
                  failureReason: null,
                  checkin:
                    {
                      checkinId: chk_a9ngr9g42trr594bv8xv,
                      number: VD-261025-KEHJJ-EC,
                      kind: SERVICE,
                      title: Expert check-in (in-home visit),
                      description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                        health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                      status: NOT_ACTIVATED,
                      projected: false,
                      recordedStatus: NOT_ACTIVATED,
                      valueCents: 30000,
                      visitMinutes: 90,
                      parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                      parentOrderNumber: VD-261025-KEHJJ,
                      deliveryDate: 2026-10-28,
                      address:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: "Side gate, code 4821. Dog is friendly.",
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        },
                      booking: null,
                      createdAt: 2026-10-25T20:58:19.619Z,
                      bookableFrom: 2026-11-04,
                      bookableUntil: 2027-10-28,
                      canBook: true,
                      canCancelBooking: false,
                      history: [ {} ]
                    },
                  placedWithTestClockOffsetDays: 0,
                  lines:
                    [
                      {
                          sku: VD-AP-M,
                          name: AutoPlanter Medium,
                          type: PLANTER,
                          size: M,
                          quantity: 1,
                          unitPriceCents: 239000,
                          itemTotalCents: 239000,
                          cover: {},
                          lineTotalCents: 260600
                        }
                    ],
                  totals: { merchandiseCents: 424000, coverCents: 43800, deliveryCents: 37000, totalCents: 504800, gstCents: 45891, deliveryIncluded: true },
                  deliveryBreakdown: [ { code: FREIGHT, label: Truck delivery (20 km from our Dandenong South DC), amountCents: 13000 } ],
                  address:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      line1: 12 Fern Street,
                      line2: "",
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      instructions: Ring the bell twice. Park in the driveway.,
                      isDefault: true,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                      createdAt: 2026-10-25T20:58:19.473Z
                    },
                  refund: null,
                  deliveryDetailsHistory: [ { at: 2026-10-25T20:58:19.803Z, changes: [ instructions ], from: {}, to: {}, simulated: false, businessDate: 2026-10-26 } ],
                  statusHistory: [ { status: PENDING_PAYMENT, at: 2026-10-25T20:58:19.594Z, simulated: false, businessDate: 2026-10-26, label: "Order received, awaiting payment" } ],
                  policies: [],
                  invoice:
                    {
                      invoiceNumber: INV-261025-KEHJJ,
                      filename: INV-261025-KEHJJ.pdf,
                      pdf: /v1/staff/customers/usr_btgygbap02w2g1cd3qcn/orders/ord_xwykyn0axh01bp3k8xvr/invoice.pdf
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/customers/{userId}/orders/{orderId}/invoice.pdf:
    get:
      operationId: staffGetInvoicePdf
      tags:
        - Staff portal
      security:
        - staff: []
      summary: The customer's invoice PDF (same bytes as the customer's copy)
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/orders:
    get:
      operationId: staffListOrders
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Orders in view
      parameters:
        - name: status
          in: query
          schema:
            type: string
        - name: q
          in: query
          schema:
            type: string
        - name: from
          in: query
          schema:
            type: string
            format: date
          description: delivery date from
        - name: to
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  orders:
                    type: array
                    items: {}
                  count:
                    type: integer
              example:
                {
                  orders:
                    [
                      {
                          orderId: ord_zp5a2yggrx14j89xgtv0,
                          orderNumber: VD-261025-9RPDP,
                          status: CANCELLED,
                          createdAt: 2026-10-25T20:58:20.162Z,
                          updatedAt: 2026-10-25T20:58:20.252Z,
                          customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                          totalCents: 11400,
                          itemCount: 1,
                          items: [ 1 × AutoPlanter Coffee Table Book ],
                          coveredLines: 0,
                          coverCents: 0,
                          delivery:
                            {
                              method: COURIER,
                              date: 2026-11-04,
                              window: COURIER,
                              windowLabel: Courier,
                              truck: null,
                              carrier: Swift Parcel Co. (fictional courier),
                              trackingNumber: SPCA5QHE00TD1,
                              dispatchDate: 2026-10-27,
                              status: RELEASED,
                              suburb: PERTH,
                              state: WA,
                              postcode: "6000"
                            },
                          payment: { brand: Visa, last4: "1111", approved: true, responseCode: "00" },
                          failureReason: null,
                          checkin: null,
                          placedWithTestClockOffsetDays: 0
                        }
                    ],
                  count: 4
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/deliveries:
    get:
      operationId: staffListDeliveries
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Dispatch view - deliveries and expert visits by date, window and truck
      parameters:
        - name: from
          in: query
          schema:
            type: string
            format: date
          example: 2026-10-28
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 60
          example: 1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  from:
                    type: string
                    format: date
                  to:
                    type: string
                    format: date
                  days:
                    type: array
                    items: {}
              example:
                {
                  from: 2026-10-28,
                  to: 2026-10-28,
                  days:
                    [
                      {
                          date: 2026-10-28,
                          deliveries:
                            [
                              {
                                  orderId: ord_xwykyn0axh01bp3k8xvr,
                                  orderNumber: VD-261025-KEHJJ,
                                  status: CONFIRMED,
                                  createdAt: 2026-10-25T20:58:19.594Z,
                                  updatedAt: 2026-10-25T20:58:19.803Z,
                                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                                  totalCents: 504800,
                                  itemCount: 2,
                                  items: [ 1 × AutoPlanter Medium, 1 × Monstera 'Thai Constellation' - Medium ],
                                  coveredLines: 2,
                                  coverCents: 43800,
                                  delivery:
                                    {
                                      method: TRUCK,
                                      date: 2026-10-28,
                                      window: AM,
                                      windowLabel: Morning,
                                      truck: T01,
                                      carrier: null,
                                      trackingNumber: null,
                                      dispatchDate: null,
                                      status: BOOKED,
                                      suburb: GLEN WAVERLEY,
                                      state: VIC,
                                      postcode: "3150"
                                    },
                                  payment: { brand: Visa, last4: "1111", approved: true, responseCode: "00" },
                                  failureReason: null,
                                  checkin: { checkinId: chk_a9ngr9g42trr594bv8xv, number: VD-261025-KEHJJ-EC, status: NOT_ACTIVATED, booking: null },
                                  placedWithTestClockOffsetDays: 0,
                                  jobMinutes: 210,
                                  installMinutes: 110,
                                  shippingKg: 89,
                                  address:
                                    {
                                      addressId: adr_2pf7jq915g9jfmzhma33,
                                      label: Home,
                                      recipientName: Ava Nguyen,
                                      phone: "+61412345678",
                                      line1: 12 Fern Street,
                                      line2: "",
                                      suburb: GLEN WAVERLEY,
                                      state: VIC,
                                      postcode: "3150",
                                      siteAccess: GROUND,
                                      instructions: Ring the bell twice. Park in the driveway.,
                                      isDefault: true,
                                      serviceArea: {},
                                      createdAt: 2026-10-25T20:58:19.473Z
                                    }
                                }
                            ],
                          visits: [],
                          closedReason: null
                        }
                    ]
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/checkins:
    get:
      operationId: staffListCheckins
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Expert check-ins in view
      parameters:
        - name: status
          in: query
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  checkins:
                    type: array
                    items: {}
              example:
                {
                  checkins:
                    [
                      {
                          checkinId: chk_a9ngr9g42trr594bv8xv,
                          number: VD-261025-KEHJJ-EC,
                          kind: SERVICE,
                          title: Expert check-in (in-home visit),
                          description: "A Verdant Dock plant expert visits your home to check the AutoPlanter's docking station, sensors and light-tracking map, inspect your plant's
                            health under the dome, and tune its care plan. About 90 minutes. Free when you...",
                          status: NOT_ACTIVATED,
                          projected: false,
                          recordedStatus: NOT_ACTIVATED,
                          valueCents: 30000,
                          visitMinutes: 90,
                          parentOrderId: ord_xwykyn0axh01bp3k8xvr,
                          parentOrderNumber: VD-261025-KEHJJ,
                          deliveryDate: 2026-10-28,
                          address:
                            {
                              addressId: adr_2pf7jq915g9jfmzhma33,
                              label: Home,
                              recipientName: Ava Nguyen,
                              phone: "+61412345678",
                              line1: 12 Fern Street,
                              line2: "",
                              suburb: GLEN WAVERLEY,
                              state: VIC,
                              postcode: "3150",
                              siteAccess: GROUND,
                              instructions: "Side gate, code 4821. Dog is friendly.",
                              isDefault: true,
                              serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO },
                              createdAt: 2026-10-25T20:58:19.473Z
                            },
                          booking: null,
                          createdAt: 2026-10-25T20:58:19.619Z,
                          bookableFrom: 2026-11-04,
                          bookableUntil: 2027-10-28,
                          canBook: true,
                          canCancelBooking: false,
                          history: [ { status: NOT_ACTIVATED, at: 2026-10-25T20:58:19.620Z, label: Earned with order - not yet booked } ],
                          customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now }
                        }
                    ]
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/activity:
    get:
      operationId: staffListActivity
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Request log for your networks (IP, time, route, status, who)
      parameters:
        - name: ip
          in: query
          schema:
            type: string
        - name: customer
          in: query
          schema:
            type: string
        - name: errors
          in: query
          schema:
            type: boolean
        - name: exclude
          in: query
          schema:
            type: string
            enum:
              - staff
        - name: limit
          in: query
          schema:
            type: integer
            maximum: 300
          example: 5
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items: {}
                  networks:
                    type: array
                    items: {}
              example:
                {
                  requests:
                    [
                      {
                          at: 2026-10-25T21:00:02.582Z,
                          ip: 203.0.113.77,
                          net: 203.0.113.77,
                          method: GET,
                          path: /v1/staff/checkins,
                          status: 200,
                          ms: 28,
                          actorType: staff,
                          actorId: stf_gb5xdjbjr28f88htr0xw,
                          subjectId: null,
                          userAgent: node-test,
                          correlationId: req-0.29038362029478937,
                          traceId: null,
                          actor: { id: stf_gb5xdjbjr28f88htr0xw, kind: staff, name: Sam Staff, email: sam.staff@verdant-v1.testthis.now },
                          subject: null
                        }
                    ],
                  networks: [ 203.0.113.77 ]
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/support:
    get:
      operationId: getSupportInfo
      tags:
        - Support
      security: []
      summary: Support address and case types
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  supportAddress:
                    type: string
                  types:
                    type: array
                    items: {}
              example: { supportAddress: support-verdant-v1@testthis.now, types: [ { code: ENQUIRY, label: General enquiry } ] }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/test-mailbox/send:
    post:
      operationId: sendTestMailToSupport
      tags:
        - Testing
      security: []
      summary: Email the support address as an address at the stack mail domain (202)
      description: In AWS the message goes out through SES and back in by the real inbound route (MX, receipt rule, parser). Locally it is handed straight to the
        parser. The caller IP travels with it so staff on the same network see the case.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                from:
                  type: string
                  format: email
                fromName:
                  type: string
                to:
                  type: string
                  description: defaults to the support address (the only allowed recipient)
                subject:
                  type: string
                text:
                  type: string
            example: { from: ava.example@verdant-v1.testthis.now, fromName: Ava Nguyen, subject: Where is my delivery?, text: "Hello, any news on my truck?" }
      responses:
        "202":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  to:
                    type: string
                  caseId:
                    type: string
                  created:
                    type: boolean
                  linked:
                    type: boolean
              example: { status: DELIVERED_LOCALLY, to: support-verdant-v1@testthis.now, caseId: C-261025-6DF7X, created: true, linked: true }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/test-tools/totp:
    get:
      operationId: getTestTotp
      tags:
        - Testing
      security: []
      summary: Current and next TOTP codes for a secret you hold (for HTTP-only clients such as Arazzo runners)
      description: "Computes RFC 6238 codes (SHA1, 6 digits, 30 s) for the base32 secret returned by startMfaEnrolment, so clients that cannot run code can still
        complete MFA. It reveals nothing new: anyone with the secret can compute the same codes. The API accepts the current code and nextCode (one step of
        drift); a code is single-use per account, so a sign-in straight after enrolment should use nextCode."
      parameters:
        - name: secret
          in: query
          required: true
          schema:
            type: string
            pattern: ^[A-Za-z2-7]{16,64}=*$
          description: Base32 TOTP secret
          example: H32RZNYZAXNQUJAEZ34RIGBCE4TUYH2L
      responses:
        "200":
          description: Codes
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - nextCode
                  - secondsRemaining
                properties:
                  code:
                    type: string
                    pattern: ^\d{6}$
                  nextCode:
                    type: string
                    pattern: ^\d{6}$
                  step:
                    type: integer
                  secondsRemaining:
                    type: integer
                  algorithm:
                    type: string
                  digits:
                    type: integer
                  period:
                    type: integer
              example: { code: "379172", nextCode: "406231", step: 59765395, secondsRemaining: 12, algorithm: SHA1, digits: 6, period: 30 }
        "422":
          $ref: "#/components/responses/Problem"
  /v1/observe:
    get:
      operationId: getObservability
      tags:
        - Observability
      security: []
      summary: What is observable and how (the headers, the pages, the links)
      description: "Every API response carries `X-Vd-Trace-Id` (the trace of that request, in X-Ray form), `X-Vd-Trace-Sampled` (1 = Lambda also recorded its own
        segments: Init on a cold start, Overhead), `X-Vd-Auth-Trace-Id` on protected routes (the JWT authorizer's own trace) and `Server-Timing: app;dur=<ms>`
        (time inside the handler); an error body carries `traceId` beside `correlationId`. Send `X-Vd-Run: <run id>` with every request of a test run (and
        `X-Vd-Action` / `X-Vd-Interaction` to group and label the requests of one step) and read the run back with getRun. 60 lookups a minute per IP address;
        everything is kept for 30 days."
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                  access:
                    type: string
                  trace:
                    type: string
                  run:
                    type: string
                  pages:
                    type: object
                  headers:
                    type: object
                  retention:
                    type: string
                  cleaned:
                    type: string
              example:
                {
                  name: Verdant Dock observability,
                  access: public - no sign-in; lookups by identifier only; 60 lookups a minute per IP address,
                  trace: "https://api.verdant-v1.testthis.now/v1/observe/traces/{traceId}  (X-Vd-Trace-Id on any API response, or the trace link on any page; several ids
                    comma-separated)",
                  run: "https://api.verdant-v1.testthis.now/v1/observe/runs/{runId}  (send X-Vd-Run: <your run id> with every request, or open a page with ?vdRun=<id>)",
                  pages:
                    {
                      trace: "https://shop.verdant-v1.testthis.now/observe.html#/trace/{traceId}",
                      run: "https://shop.verdant-v1.testthis.now/observe.html#/run/{runId}",
                      lookup: https://shop.verdant-v1.testthis.now/observe.html
                    },
                  headers:
                    {
                      request:
                        {
                          X-Vd-Run: "run id (3-64 letters, digits, . : - _)",
                          X-Vd-Action: groups the requests of one click on the run page,
                          X-Vd-Interaction: "a label for that click (ASCII, 60 characters)"
                        },
                      response:
                        {
                          X-Vd-Trace-Id: the trace of this request,
                          X-Vd-Trace-Sampled: "1 = Lambda recorded its own segments (Init, Overhead) too",
                          X-Vd-Auth-Trace-Id: the JWT authorizer's trace (protected routes),
                          Server-Timing: app;dur=<ms inside the handler>
                        }
                    },
                  retention: 30 days,
                  cleaned: "allow-list: route templates, operations, tables, durations, statuses"
                }
        "429":
          $ref: "#/components/responses/Problem"
  /v1/observe/traces/{traceIds}:
    get:
      operationId: getTraces
      tags:
        - Observability
      security: []
      summary: One trace - or, comma-separated, every trace of one click - cleaned, with a breakdown and clues
      description: "Read from AWS X-Ray and shown with an allow-list: route templates, operations, table and index names, durations, statuses and counts - never
        caller IPs, user agents, account ids, ARNs, request ids, URLs or query strings. Behind the HTTP API every request is its own Lambda invocation and so
        its own trace; give up to 10 ids to see one click's requests together (the trace pill in the apps does). Lambda records its own segments for one
        invocation a second plus 5% (`lambdaRecorded`); the app's spans are recorded for every invocation. A trace younger than five minutes that X-Ray does not
        have yet answers 202 `pending` (retry after 10 s); an older unknown id, or a trace from another application in the account, is 404. Finished traces are
        cached for 30 days."
      parameters:
        - name: traceIds
          in: path
          required: true
          schema:
            type: string
            pattern: ^1-[0-9a-f]{8}-[0-9a-f]{24}(,1-[0-9a-f]{8}-[0-9a-f]{24}){0,9}$
          description: One X-Ray trace id (`X-Vd-Trace-Id`), or up to 10 comma-separated
      responses:
        "200":
          description: The trace(s)
          content:
            application/json:
              schema:
                type: object
                properties:
                  traceIds:
                    type: array
                    items: {}
                  found:
                    type: array
                    items: {}
                  requests:
                    type: array
                    items: {}
                  clues:
                    type: array
                    items: {}
                  startedAt:
                    type: string
                    format: date-time
                  durationMs:
                    type: integer
                  spanCount:
                    type: integer
                  traces:
                    type: array
                    items: {}
                  cleaned:
                    type: string
                  status:
                    type: string
                  pending:
                    type: array
                    items: {}
                  missing:
                    type: array
                    items: {}
                  links:
                    type: object
              example:
                {
                  traceIds: [ 1-6ad00000-000000000000000000000001 ],
                  found: [ 1-6ad00000-000000000000000000000001 ],
                  requests:
                    [
                      {
                          index: 1,
                          traceId: 1-6ad00000-000000000000000000000001,
                          trigger: invocation,
                          function: verdant-v1-ApiFunction-1AB2CD3EF4GH,
                          app: api,
                          host: null,
                          method: POST,
                          route: /v1/orders,
                          status: 201,
                          startMs: 0,
                          totalMs: 1767,
                          lambdaRecorded: true,
                          coldStart: true,
                          initMs: 812,
                          afterResponseMs: 9,
                          breakdown: [ {} ],
                          awsCalls: [ {} ],
                          span: 0,
                          traceStartedAt: 2026-10-14T22:19:44.196Z,
                          startedAt: 2026-10-14T22:19:44.196Z
                        }
                    ],
                  clues:
                    [
                      Cold start — Init took 812 ms (46% of the request). The first request to a new Lambda instance loads the code (and the OpenTelemetry
                        layer) first.,
                      SESv2.SendEmail took 631 ms — 36% of the request.,
                      "About 9 ms passed between our handler finishing and the response leaving Lambda — mostly the OpenTelemetry layer exporting this trace:
                        the cost of observing, shown rather than hidden.",
                      "Lambda kept working about 9 ms after the response (Overhead): billed, but the caller does not wait for it."
                    ],
                  startedAt: 2026-10-14T22:19:44.196Z,
                  durationMs: 1773,
                  spanCount: 25,
                  traces:
                    [
                      {
                          traceId: 1-6ad00000-000000000000000000000001,
                          startedAt: 2026-10-14T22:19:44.196Z,
                          durationMs: 1773,
                          spans: [ {} ],
                          spanCount: 25,
                          truncated: false,
                          lambdaRecorded: true,
                          sampledByRule: null
                        }
                    ],
                  cleaned: "Shown with an allow-list: route templates, operations, tables, durations and statuses. Caller IPs, user agents, account ids, ARNs, request ids, URLs
                    and query strings are left out.",
                  status: complete,
                  pending: [],
                  missing: [],
                  links:
                    {
                      page: https://shop.verdant-v1.testthis.now/observe.html#/trace/1-6ad00000-000000000000000000000001,
                      json: https://api.verdant-v1.testthis.now/v1/observe/traces/1-6ad00000-000000000000000000000001,
                      awsConsole: [ https://console.aws.amazon.com/xray/home?region=ap-southeast-2#/traces/1-6ad00000-000000000000000000000001 ]
                    }
                }
        "202":
          description: Not in X-Ray yet (`status` pending, `retryAfterSeconds`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - pending
                  traceIds:
                    type: array
                    items:
                      type: string
                  pending:
                    type: array
                    items:
                      type: string
                  retryAfterSeconds:
                    type: integer
                  ageSeconds:
                    type: integer
                  message:
                    type: string
              example:
                {
                  status: pending,
                  traceIds: [ 1-6ad00000-000000000000000000000099 ],
                  pending: [ 1-6ad00000-000000000000000000000099 ],
                  missing: [],
                  retryAfterSeconds: 10,
                  ageSeconds: 12,
                  message: "Not in X-Ray yet: a trace usually appears within a minute of the request."
                }
        "400":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/observe/runs/{runId}:
    get:
      operationId: getRun
      tags:
        - Observability
      security: []
      summary: Every request of a test run tagged with X-Vd-Run - per-route p50/p95/max, the actions in order, the outliers
      description: "Tag a run by sending `X-Vd-Run: <run id>` with API requests (3-64 letters, digits, `.`, `:`, `-`, `_`), or by opening any page with `?vdRun=<run
        id>` (kept for that app in that browser tab). Requests that also carry `X-Vd-Action` are grouped into one action (one click) with every trace it
        produced; `X-Vd-Interaction` labels it. An outlier took at least twice as long as its route's median (and at least 100 ms). Server time is the time
        inside the handler (the `Server-Timing` header)."
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
            pattern: ^[A-Za-z0-9][A-Za-z0-9._:-]{2,63}$
      responses:
        "200":
          description: The run
          content:
            application/json:
              schema:
                type: object
                properties:
                  runId:
                    type: string
                  steps:
                    type: integer
                  interactions:
                    type: array
                    items: {}
                  firstAt:
                    type: string
                    format: date-time
                  lastAt:
                    type: string
                    format: date-time
                  errors:
                    type: integer
                  outliers:
                    type: integer
                  traced:
                    type: integer
                  lambdaSampled:
                    type: integer
                  serverMs:
                    type: object
                  routes:
                    type: array
                    items: {}
                  truncated:
                    type: boolean
                  links:
                    type: object
              example:
                {
                  runId: example-run-42,
                  steps: 2,
                  interactions:
                    [
                      {
                          actionId: 1-6ad00000-aaaaaaaaaaaaaaaaaaaaaaaa,
                          label: "open #/shop",
                          at: 2026-10-25T20:56:38.202Z,
                          app: customer,
                          traceIds: [],
                          steps:
                            [
                              {
                                  at: 2026-10-25T20:56:38.202Z,
                                  method: GET,
                                  route: /v1/catalogue,
                                  status: 200,
                                  serverMs: 1.4,
                                  traceId: null,
                                  sampled: null,
                                  authTraceId: null,
                                  action: 1-6ad00000-aaaaaaaaaaaaaaaaaaaaaaaa,
                                  interaction: "open #/shop",
                                  app: customer,
                                  outlier: false
                                }
                            ],
                          serverMs: 2,
                          errors: 0,
                          outlier: false,
                          link: null
                        }
                    ],
                  firstAt: 2026-10-25T20:56:38.202Z,
                  lastAt: 2026-10-25T20:56:38.205Z,
                  errors: 0,
                  outliers: 0,
                  traced: 0,
                  lambdaSampled: 0,
                  serverMs: { p50: 0.6, p95: 1.4, max: 1.4, total: 2 },
                  routes: [ { method: GET, route: /v1/catalogue, errors: 0, count: 1, p50: 1.4, p95: 1.4, max: 1.4, meanMs: 1.4 } ],
                  truncated: false,
                  links: { page: https://shop.verdant-v1.testthis.now/observe.html#/run/example-run-42, json: https://api.verdant-v1.testthis.now/v1/observe/runs/example-run-42 }
                }
        "400":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
  /v1/cases:
    get:
      operationId: listCases
      tags:
        - Support
      security:
        - bearer: []
      summary: Your cases
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - OPEN
              - AWAITING_CUSTOMER
              - CLOSED
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  cases:
                    type: array
                    items: {}
              example:
                {
                  cases:
                    [
                      {
                          caseId: C-261025-BRSRQ,
                          type: SUPPORT,
                          subject: Which way should the dock face?,
                          status: OPEN,
                          channel: WEB,
                          initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                          userId: usr_btgygbap02w2g1cd3qcn,
                          customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                          order: { orderId: ord_xwykyn0axh01bp3k8xvr, orderNumber: VD-261025-KEHJJ },
                          assignedStaff: null,
                          createdAt: 2026-10-25T20:58:20.861Z,
                          updatedAt: 2026-10-25T20:58:20.881Z,
                          closedAt: null,
                          reopenableUntil: null,
                          canReopen: false,
                          messageCount: 2,
                          lastMessageAt: 2026-10-25T20:58:20.881Z,
                          lastMessageBy: customer,
                          nets: [ 203.0.113.77 ]
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
    post:
      operationId: createCase
      tags:
        - Support
      security:
        - bearer: []
      summary: Raise a case (201); emails you a copy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  enum:
                    - ENQUIRY
                    - SUPPORT
                    - COMPLAINT
                subject:
                  type: string
                  maxLength: 150
                message:
                  type: string
                  maxLength: 5000
                orderId:
                  type: string
                conversationId:
                  type: string
                  description: "When sending a draft the assistant prepared: that chat (must be yours). Recorded on the case as origin."
                draftEdited:
                  type: boolean
            example: { type: SUPPORT, subject: Which way should the dock face?, message: Does the docking station need to face the window?, orderId: ord_xwykyn0axh01bp3k8xvr }
      responses:
        "201":
          description: OK
          links:
            GetCase:
              operationId: getCase
              parameters:
                caseId: $response.body#/caseId
              description: Read the case and its messages.
            ReplyToCase:
              operationId: replyToCase
              parameters:
                caseId: $response.body#/caseId
              description: Add a message to the case.
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: object
                    nullable: true
                  assignedStaff:
                    type: string
                    nullable: true
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    nullable: true
                  reopenableUntil:
                    type: string
                    nullable: true
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-BRSRQ,
                  type: SUPPORT,
                  subject: Which way should the dock face?,
                  status: OPEN,
                  channel: WEB,
                  initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: { orderId: ord_xwykyn0axh01bp3k8xvr, orderNumber: VD-261025-KEHJJ },
                  assignedStaff: null,
                  createdAt: 2026-10-25T20:58:20.861Z,
                  updatedAt: 2026-10-25T20:58:20.861Z,
                  closedAt: null,
                  reopenableUntil: null,
                  canReopen: false,
                  messageCount: 1,
                  lastMessageAt: 2026-10-25T20:58:20.861Z,
                  lastMessageBy: customer,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_c56hzvs9kh,
                          at: 2026-10-25T20:58:20.862Z,
                          kind: MESSAGE,
                          author: { type: customer, id: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen },
                          channel: WEB,
                          body: Does the docking station need to face the window?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.example@verdant-v1.testthis.now, messageId: local-13 }
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/cases/{caseId}:
    get:
      operationId: getCase
      tags:
        - Support
      security:
        - bearer: []
      summary: A case with its conversation
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            example: C-260927-7K3M9
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: object
                    nullable: true
                  assignedStaff:
                    type: string
                    nullable: true
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    nullable: true
                  reopenableUntil:
                    type: string
                    nullable: true
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-BRSRQ,
                  type: SUPPORT,
                  subject: Which way should the dock face?,
                  status: OPEN,
                  channel: WEB,
                  initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: { orderId: ord_xwykyn0axh01bp3k8xvr, orderNumber: VD-261025-KEHJJ },
                  assignedStaff: null,
                  createdAt: 2026-10-25T20:58:20.861Z,
                  updatedAt: 2026-10-25T20:58:20.881Z,
                  closedAt: null,
                  reopenableUntil: null,
                  canReopen: false,
                  messageCount: 2,
                  lastMessageAt: 2026-10-25T20:58:20.881Z,
                  lastMessageBy: customer,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_c56hzvs9kh,
                          at: 2026-10-25T20:58:20.862Z,
                          kind: MESSAGE,
                          author: { type: customer, id: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen },
                          channel: WEB,
                          body: Does the docking station need to face the window?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.example@verdant-v1.testthis.now, messageId: local-13 }
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/cases/{caseId}/messages:
    post:
      operationId: replyToCase
      tags:
        - Support
      security:
        - bearer: []
      summary: Add a message (reopens a closed case)
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            example: C-260927-7K3M9
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                message:
                  type: string
            example: { message: "Also, can it go on carpet?" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: object
                    nullable: true
                  assignedStaff:
                    type: string
                    nullable: true
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    nullable: true
                  reopenableUntil:
                    type: string
                    nullable: true
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-BRSRQ,
                  type: SUPPORT,
                  subject: Which way should the dock face?,
                  status: OPEN,
                  channel: WEB,
                  initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: { orderId: ord_xwykyn0axh01bp3k8xvr, orderNumber: VD-261025-KEHJJ },
                  assignedStaff: null,
                  createdAt: 2026-10-25T20:58:20.861Z,
                  updatedAt: 2026-10-25T20:58:20.881Z,
                  closedAt: null,
                  reopenableUntil: null,
                  canReopen: false,
                  messageCount: 2,
                  lastMessageAt: 2026-10-25T20:58:20.881Z,
                  lastMessageBy: customer,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_c56hzvs9kh,
                          at: 2026-10-25T20:58:20.862Z,
                          kind: MESSAGE,
                          author: { type: customer, id: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen },
                          channel: WEB,
                          body: Does the docking station need to face the window?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.example@verdant-v1.testthis.now, messageId: local-13 }
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/cases/{caseId}/close:
    post:
      operationId: closeCase
      tags:
        - Support
      security:
        - bearer: []
      summary: Close your case
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            example: C-260927-7K3M9
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: object
                    nullable: true
                  assignedStaff:
                    type: object
                    nullable: true
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    format: date-time
                  reopenableUntil:
                    type: string
                    format: date-time
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-WYNPA,
                  type: ENQUIRY,
                  subject: Gift card?,
                  status: CLOSED,
                  channel: WEB,
                  initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: null,
                  assignedStaff: null,
                  createdAt: 2026-10-25T20:58:20.913Z,
                  updatedAt: 2026-10-25T20:58:20.954Z,
                  closedAt: 2026-10-25T20:58:20.954Z,
                  reopenableUntil: 2026-11-01T20:58:20.954Z,
                  canReopen: true,
                  messageCount: 2,
                  lastMessageAt: 2026-10-25T20:58:20.928Z,
                  lastMessageBy: customer,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_9q8qdm86bd,
                          at: 2026-10-25T20:58:20.914Z,
                          kind: MESSAGE,
                          author: { type: customer, id: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen },
                          channel: WEB,
                          body: Do you sell gift cards?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.example@verdant-v1.testthis.now, messageId: local-15 }
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/staff/cases:
    get:
      operationId: staffListCases
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Cases in view (linked to your customers or raised from your networks)
      parameters:
        - name: status
          in: query
          schema:
            type: string
        - name: type
          in: query
          schema:
            type: string
        - name: q
          in: query
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  cases:
                    type: array
                    items: {}
                  count:
                    type: integer
                  totals:
                    type: object
              example:
                {
                  cases:
                    [
                      {
                          caseId: C-261025-YY5AX,
                          type: CHAT,
                          subject: Chat with the assistant - 26 Oct 2026,
                          status: CLOSED,
                          channel: CHAT,
                          initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                          userId: usr_btgygbap02w2g1cd3qcn,
                          customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                          order: null,
                          assignedStaff: null,
                          createdAt: 2026-10-25T20:58:21.193Z,
                          updatedAt: 2026-10-25T20:58:21.193Z,
                          closedAt: 2026-10-25T20:58:21.193Z,
                          reopenableUntil: 2026-11-01T20:58:21.193Z,
                          canReopen: true,
                          chat:
                            {
                              conversationId: chat_6mk2kn4najhqxh,
                              startedAt: 2026-10-25T20:58:21.114Z,
                              endedAt: 2026-10-25T20:58:21.188Z,
                              endReason: ENDED_BY_CUSTOMER,
                              messages: 2,
                              alteredDates: false,
                              alteredNote: null
                            },
                          messageCount: 1,
                          lastMessageAt: 2026-10-25T20:58:21.193Z,
                          lastMessageBy: customer,
                          nets: [ 203.0.113.77 ]
                        }
                    ],
                  count: 4,
                  totals: { OPEN: 3, AWAITING_CUSTOMER: 0, CLOSED: 1 }
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/staff/cases/{caseId}:
    get:
      operationId: staffGetCase
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Case detail, conversation (with email notification status), link candidates
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            example: C-260927-7K3M9
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: object
                  assignedStaff:
                    type: string
                    nullable: true
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    nullable: true
                  reopenableUntil:
                    type: string
                    nullable: true
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
                  linkCandidates:
                    type: array
                    items: {}
                  customerOrders:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-BRSRQ,
                  type: SUPPORT,
                  subject: Which way should the dock face?,
                  status: OPEN,
                  channel: WEB,
                  initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: { orderId: ord_xwykyn0axh01bp3k8xvr, orderNumber: VD-261025-KEHJJ },
                  assignedStaff: null,
                  createdAt: 2026-10-25T20:58:20.861Z,
                  updatedAt: 2026-10-25T20:58:20.881Z,
                  closedAt: null,
                  reopenableUntil: null,
                  canReopen: false,
                  messageCount: 2,
                  lastMessageAt: 2026-10-25T20:58:20.881Z,
                  lastMessageBy: customer,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_c56hzvs9kh,
                          at: 2026-10-25T20:58:20.862Z,
                          kind: MESSAGE,
                          author: { type: customer, id: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen },
                          channel: WEB,
                          body: Does the docking station need to face the window?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.example@verdant-v1.testthis.now, messageId: local-13 }
                        }
                    ],
                  linkCandidates: [],
                  customerOrders: [ { orderId: ord_vjnhd758df9hxe0d273p, orderNumber: VD-261025-0X19C, status: CANCELLED, createdAt: 2026-10-25T20:58:19.998Z } ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/staff/cases/{caseId}/messages:
    post:
      operationId: staffReplyToCase
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Reply to the customer, then set the status (default AWAITING_CUSTOMER)
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            example: C-260927-7K3M9
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                message:
                  type: string
                status:
                  type: string
                  enum:
                    - AWAITING_CUSTOMER
                    - OPEN
                    - CLOSED
            example: { message: Yes - face it into the room; it works on carpet too., status: AWAITING_CUSTOMER }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: object
                  assignedStaff:
                    type: object
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    nullable: true
                  reopenableUntil:
                    type: string
                    nullable: true
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-BRSRQ,
                  type: SUPPORT,
                  subject: Which way should the dock face?,
                  status: AWAITING_CUSTOMER,
                  channel: WEB,
                  initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: { orderId: ord_xwykyn0axh01bp3k8xvr, orderNumber: VD-261025-KEHJJ },
                  assignedStaff: { staffId: stf_gb5xdjbjr28f88htr0xw, name: Sam Staff },
                  createdAt: 2026-10-25T20:58:20.861Z,
                  updatedAt: 2026-10-25T21:00:02.594Z,
                  closedAt: null,
                  reopenableUntil: null,
                  canReopen: false,
                  messageCount: 3,
                  lastMessageAt: 2026-10-25T21:00:02.594Z,
                  lastMessageBy: staff,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_c56hzvs9kh,
                          at: 2026-10-25T20:58:20.862Z,
                          kind: MESSAGE,
                          author: { type: customer, id: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen },
                          channel: WEB,
                          body: Does the docking station need to face the window?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.example@verdant-v1.testthis.now, messageId: local-13 }
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/staff/cases/{caseId}/status:
    post:
      operationId: staffSetCaseStatus
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Close, reopen or mark a case
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            example: C-260927-7K3M9
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum:
                    - AWAITING_CUSTOMER
                    - OPEN
                    - CLOSED
            example: { status: CLOSED }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: object
                  assignedStaff:
                    type: object
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    format: date-time
                  reopenableUntil:
                    type: string
                    format: date-time
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-BRSRQ,
                  type: SUPPORT,
                  subject: Which way should the dock face?,
                  status: CLOSED,
                  channel: WEB,
                  initiator: { email: ava.example@verdant-v1.testthis.now, name: Ava Nguyen },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: { orderId: ord_xwykyn0axh01bp3k8xvr, orderNumber: VD-261025-KEHJJ },
                  assignedStaff: { staffId: stf_gb5xdjbjr28f88htr0xw, name: Sam Staff },
                  createdAt: 2026-10-25T20:58:20.861Z,
                  updatedAt: 2026-10-25T21:00:02.603Z,
                  closedAt: 2026-10-25T21:00:02.603Z,
                  reopenableUntil: 2026-11-01T21:00:02.603Z,
                  canReopen: true,
                  messageCount: 3,
                  lastMessageAt: 2026-10-25T21:00:02.594Z,
                  lastMessageBy: staff,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_c56hzvs9kh,
                          at: 2026-10-25T20:58:20.862Z,
                          kind: MESSAGE,
                          author: { type: customer, id: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen },
                          channel: WEB,
                          body: Does the docking station need to face the window?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.example@verdant-v1.testthis.now, messageId: local-13 }
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/claims:
    get:
      operationId: listClaims
      tags:
        - Claims
      summary: Policies with their claim state (canClaim, claim), your claims, and showTab (true when any policy is ACTIVE on the business date or a claim exists)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  showTab:
                    type: boolean
                  businessDate:
                    type: string
                    format: date
                  realDate:
                    type: string
                    format: date
                  simulated:
                    type: boolean
                  policies:
                    type: array
                    items: {}
                  claims:
                    type: array
                    items: {}
                  notShownPolicies:
                    type: array
                    items: {}
              example:
                {
                  showTab: true,
                  businessDate: 2026-11-07,
                  realDate: 2026-10-26,
                  simulated: true,
                  policies:
                    [
                      {
                          policyId: pol_c329a8e67492a63461db,
                          policyNumber: TC-C329A8E6,
                          status: ACTIVE,
                          planCode: TC-1Y,
                          planName: Thrive Cover 1 year,
                          termYears: 1,
                          summary: "If the plant fails for any reason - disease, pests, transplant shock or neglect - we replace it with one of the same size. Insure both an AutoPlanter
                            and a plant in the same order and you also get a free in-home expert check-in, worth $300.",
                          coveredSku: VD-PL-MT-M,
                          coveredItem: Monstera 'Thai Constellation' - Medium,
                          coveredType: PLANT,
                          unit: 1,
                          unitsOnLine: 1,
                          premiumCents: 22200,
                          orderId: ord_xwykyn0axh01bp3k8xvr,
                          orderNumber: VD-261025-KEHJJ,
                          startDate: 2026-10-28,
                          endDate: 2027-10-27,
                          daysRemaining: 355,
                          projected: true,
                          claims: { available: true, page: "#/claims" },
                          claim: null,
                          canClaim: true,
                          whyNot: null
                        }
                    ],
                  claims: [],
                  notShownPolicies: []
                }
        "401":
          $ref: "#/components/responses/Problem"
    post:
      operationId: createClaim
      tags:
        - Claims
      summary: Submit a claim
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - policyId
                - incidentType
                - incidentDate
                - description
                - stillUsable
                - contactPhone
                - addressId
                - declaration
                - uploadIds
              properties:
                policyId:
                  type: string
                  example: pol_4927a6c2e1d0b3f9a8c7
                incidentType:
                  type: string
                  description: Planters - STOPPED_WORKING, WONT_DOCK, DOME_DAMAGE, WATER_LEAK, SENSOR_FAULT, ACCIDENTAL_DAMAGE, OTHER; plants - PLANT_DIED, DISEASE_PESTS,
                    NOT_THRIVING, PLANT_DAMAGED, OTHER
                incidentDate:
                  type: string
                  format: date
                  description: From the policy start to the business date (test clock)
                description:
                  type: string
                  minLength: 20
                  maxLength: 2000
                stillUsable:
                  type: string
                  enum:
                    - NO
                    - PARTLY
                    - YES
                contactPhone:
                  type: string
                  example: 0412 345 678
                bestTime:
                  type: string
                  enum:
                    - ANY
                    - AM
                    - PM
                  default: ANY
                addressId:
                  type: string
                  description: Where a replacement goes (must be in the delivery area)
                declaration:
                  type: boolean
                  enum:
                    - true
                uploadIds:
                  type: array
                  minItems: 1
                  maxItems: 5
                  items:
                    type: string
            example:
              {
                policyId: pol_6196589215efebfa4974,
                incidentType: ACCIDENTAL_DAMAGE,
                incidentDate: 2026-10-31,
                description: The planter bumped the coffee table and the dome now has a 10 cm crack near the base.,
                stillUsable: PARTLY,
                contactPhone: 0412 345 678,
                bestTime: AM,
                addressId: adr_2pf7jq915g9jfmzhma33,
                declaration: true,
                uploadIds: [ upl_cnfn0m72cxnh2sn0stc0 ]
              }
      responses:
        "201":
          description: The claim (status NEW); emailed
          links:
            GetClaim:
              operationId: getClaim
              parameters:
                claimId: $response.body#/claimId
              description: Read the claim and its events.
          content:
            application/json:
              schema:
                type: object
                properties:
                  claimId:
                    type: string
                  status:
                    type: string
                  statusLabel:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  policy:
                    type: object
                  customer:
                    type: object
                  incident:
                    type: object
                  contact:
                    type: object
                  replacementAddress:
                    type: object
                  photos:
                    type: array
                    items: {}
                  testClock:
                    type: object
                  altered:
                    type: object
                  handledBy:
                    type: string
                    nullable: true
                  replacement:
                    type: string
                    nullable: true
                  lastComment:
                    type: string
                    nullable: true
                  allowedNext:
                    type: array
                    items: {}
                  canOrderReplacement:
                    type: boolean
                  events:
                    type: array
                    items: {}
              example:
                {
                  claimId: CL-261025-583RR,
                  status: NEW,
                  statusLabel: New,
                  createdAt: 2026-10-25T20:58:20.808Z,
                  updatedAt: 2026-10-25T20:58:20.808Z,
                  policy:
                    {
                      policyId: pol_6196589215efebfa4974,
                      policyNumber: VC-61965892,
                      planCode: VC-2Y,
                      planName: Verdant Care 2 years,
                      coveredSku: VD-AP-M,
                      coveredItem: AutoPlanter Medium,
                      coveredType: PLANTER,
                      orderId: ord_xwykyn0axh01bp3k8xvr,
                      orderNumber: VD-261025-KEHJJ,
                      startDate: 2026-10-28,
                      endDate: 2028-10-27,
                      projected: true,
                      originalAddress:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: Ring the bell twice. Park in the driveway.,
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        }
                    },
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, givenName: Ava, email: ava.example@verdant-v1.testthis.now },
                  incident:
                    {
                      type: ACCIDENTAL_DAMAGE,
                      date: 2026-10-31,
                      description: The planter bumped the coffee table and the dome now has a 10 cm crack near the base.,
                      stillUsable: PARTLY,
                      typeLabel: Accidental damage,
                      usableLabel: Partly
                    },
                  contact: { phone: "+61412345678", bestTime: AM, bestTimeLabel: Mornings },
                  replacementAddress:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      line1: 12 Fern Street,
                      line2: null,
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO }
                    },
                  photos:
                    [
                      {
                          photoId: ph1,
                          fileName: dome-crack.png,
                          contentType: image/png,
                          size: 308,
                          url: https://verdant-v1-uploadbucket-example.s3.ap-southeast-1.amazonaws.com/claims/CL-261025-583RR/1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=...
                        }
                    ],
                  testClock: { offsetDays: 12, businessDate: 2026-11-07 },
                  altered: { incidentDate: true, policyActive: true },
                  handledBy: null,
                  replacement: null,
                  lastComment: null,
                  allowedNext: [ INVESTIGATING ],
                  canOrderReplacement: false,
                  events: [ { at: 2026-10-25T20:58:20.823Z, type: SUBMITTED, from: null, to: NEW, comment: null, by: {}, notification: {}, replacement: null } ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "409":
          description: CLAIM_EXISTS - one claim per policy
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/claims/uploads:
    post:
      operationId: createClaimUpload
      tags:
        - Claims
      summary: 'A presigned upload for one photo: POST multipart/form-data to url with all fields, then the file (field "file", last)'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - contentType
                - size
              properties:
                fileName:
                  type: string
                contentType:
                  type: string
                  enum:
                    - image/jpeg
                    - image/png
                    - image/webp
                    - image/heic
                    - image/heif
                size:
                  type: integer
                  maximum: 10485760
            example: { fileName: dome-crack.png, contentType: image/png, size: 308 }
      responses:
        "201":
          description: uploadId, url, fields, maxBytes, expiresInSeconds (900)
          content:
            application/json:
              schema:
                type: object
                properties:
                  uploadId:
                    type: string
                  url:
                    type: string
                  fields:
                    type: object
                  maxBytes:
                    type: integer
                  expiresInSeconds:
                    type: integer
              example:
                {
                  uploadId: upl_cnfn0m72cxnh2sn0stc0,
                  url: https://verdant-v1-uploadbucket-example.s3.ap-southeast-1.amazonaws.com/,
                  fields:
                    {
                      key: uploads/tmp/usr_btgygbap02w2g1cd3qcn/upl_cnfn0m72cxnh2sn0stc0,
                      Content-Type: image/png,
                      policy: eyJrZXkiOiJ1cGxvYWRzL3RtcC91c3JfYnRneWdi...,
                      signature: 40fa717a020f65a9a686ba041cf4702b280f6d2c...
                    },
                  maxBytes: 10485760,
                  expiresInSeconds: 900
                }
        "422":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
  /v1/claims/{claimId}:
    parameters:
      - name: claimId
        in: path
        required: true
        schema:
          type: string
          example: CL-261001-4KQ7M
    get:
      operationId: getClaim
      tags:
        - Claims
      summary: One of your claims with its history and short-lived photo links
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  claimId:
                    type: string
                  status:
                    type: string
                  statusLabel:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  policy:
                    type: object
                  customer:
                    type: object
                  incident:
                    type: object
                  contact:
                    type: object
                  replacementAddress:
                    type: object
                  photos:
                    type: array
                    items: {}
                  testClock:
                    type: object
                  altered:
                    type: object
                  handledBy:
                    type: string
                    nullable: true
                  replacement:
                    type: string
                    nullable: true
                  lastComment:
                    type: string
                    nullable: true
                  allowedNext:
                    type: array
                    items: {}
                  canOrderReplacement:
                    type: boolean
                  events:
                    type: array
                    items: {}
              example:
                {
                  claimId: CL-261025-583RR,
                  status: NEW,
                  statusLabel: New,
                  createdAt: 2026-10-25T20:58:20.808Z,
                  updatedAt: 2026-10-25T20:58:20.808Z,
                  policy:
                    {
                      policyId: pol_6196589215efebfa4974,
                      policyNumber: VC-61965892,
                      planCode: VC-2Y,
                      planName: Verdant Care 2 years,
                      coveredSku: VD-AP-M,
                      coveredItem: AutoPlanter Medium,
                      coveredType: PLANTER,
                      orderId: ord_xwykyn0axh01bp3k8xvr,
                      orderNumber: VD-261025-KEHJJ,
                      startDate: 2026-10-28,
                      endDate: 2028-10-27,
                      projected: true,
                      originalAddress:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: Ring the bell twice. Park in the driveway.,
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        }
                    },
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, givenName: Ava, email: ava.example@verdant-v1.testthis.now },
                  incident:
                    {
                      type: ACCIDENTAL_DAMAGE,
                      date: 2026-10-31,
                      description: The planter bumped the coffee table and the dome now has a 10 cm crack near the base.,
                      stillUsable: PARTLY,
                      typeLabel: Accidental damage,
                      usableLabel: Partly
                    },
                  contact: { phone: "+61412345678", bestTime: AM, bestTimeLabel: Mornings },
                  replacementAddress:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      line1: 12 Fern Street,
                      line2: null,
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO }
                    },
                  photos:
                    [
                      {
                          photoId: ph1,
                          fileName: dome-crack.png,
                          contentType: image/png,
                          size: 308,
                          url: https://verdant-v1-uploadbucket-example.s3.ap-southeast-1.amazonaws.com/claims/CL-261025-583RR/1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=...
                        }
                    ],
                  testClock: { offsetDays: 12, businessDate: 2026-11-07 },
                  altered: { incidentDate: true, policyActive: true },
                  handledBy: null,
                  replacement: null,
                  lastComment: null,
                  allowedNext: [ INVESTIGATING ],
                  canOrderReplacement: false,
                  events: [ { at: 2026-10-25T20:58:20.823Z, type: SUBMITTED, from: null, to: NEW, comment: null, by: {}, notification: {}, replacement: null } ]
                }
        "404":
          $ref: "#/components/responses/Problem"
  /v1/policies/{policyId}/claim-form:
    parameters:
      - name: policyId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getClaimForm
      tags:
        - Claims
      summary: "Options for the claim form of one active policy: incident types, date range, addresses, photo limits"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  policy:
                    type: object
                  existingClaim:
                    type: string
                    nullable: true
                  incidentTypes:
                    type: array
                    items: {}
                  stillUsable:
                    type: array
                    items: {}
                  contactTimes:
                    type: array
                    items: {}
                  incidentDate:
                    type: object
                  addresses:
                    type: array
                    items: {}
                  defaultAddressId:
                    type: string
                  contactPhone:
                    type: string
                  photos:
                    type: object
                  descriptionLength:
                    type: object
              example:
                {
                  policy:
                    {
                      policyId: pol_6196589215efebfa4974,
                      policyNumber: VC-61965892,
                      status: ACTIVE,
                      planCode: VC-2Y,
                      planName: Verdant Care 2 years,
                      termYears: 2,
                      summary: "Covers failure of any kind - mechanical, electrical, sensor, pump or dock - plus accidental damage. Includes technician call-out. Insure both an
                        AutoPlanter and a plant in the same order and you also get a free in-home e...",
                      coveredSku: VD-AP-M,
                      coveredItem: AutoPlanter Medium,
                      coveredType: PLANTER,
                      unit: 1,
                      unitsOnLine: 1,
                      premiumCents: 21600,
                      orderId: ord_xwykyn0axh01bp3k8xvr,
                      orderNumber: VD-261025-KEHJJ,
                      startDate: 2026-10-28,
                      endDate: 2028-10-27,
                      daysRemaining: 721,
                      projected: true,
                      claims: { available: true, page: "#/claims" }
                    },
                  existingClaim: null,
                  incidentTypes: [ { code: STOPPED_WORKING, label: Stopped working or won't power on } ],
                  stillUsable: [ { code: NO, label: No - it can't be used } ],
                  contactTimes: [ { code: ANY, label: Any time } ],
                  incidentDate: { min: 2026-10-28, max: 2026-11-07, realToday: 2026-10-26 },
                  addresses: [ { addressId: adr_2pf7jq915g9jfmzhma33, label: Home, text: "12 Fern Street, GLEN WAVERLEY VIC 3150", serviceable: true, isDefault: true } ],
                  defaultAddressId: adr_2pf7jq915g9jfmzhma33,
                  contactPhone: "+61412345678",
                  photos: { min: 1, max: 5, maxBytes: 10485760, types: [ image/jpeg, image/png, image/webp, image/heic ] },
                  descriptionLength: { min: 20, max: 2000 }
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/staff/claims:
    get:
      operationId: staffListClaims
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Claims on policies of customers in your view, with totals per status
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - NEW
              - INVESTIGATING
              - ACCEPTED
              - REJECTED
              - RESOLVED
        - name: q
          in: query
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  claims:
                    type: array
                    items: {}
                  count:
                    type: integer
                  totals:
                    type: object
                  statuses:
                    type: array
                    items: {}
              example:
                {
                  claims:
                    [
                      {
                          claimId: CL-261025-583RR,
                          status: NEW,
                          statusLabel: New,
                          createdAt: 2026-10-25T20:58:20.808Z,
                          updatedAt: 2026-10-25T20:58:20.808Z,
                          policy: {},
                          customer: {},
                          incident: {},
                          contact: {},
                          replacementAddress: {},
                          photos: [ {} ],
                          testClock: {},
                          altered: {},
                          handledBy: null,
                          replacement: null,
                          lastComment: null,
                          allowedNext: [ INVESTIGATING ],
                          canOrderReplacement: false
                        }
                    ],
                  count: 1,
                  totals: { NEW: 1, INVESTIGATING: 0, ACCEPTED: 0, REJECTED: 0, RESOLVED: 0 },
                  statuses: [ { code: NEW, label: New } ]
                }
        4XX:
          $ref: "#/components/responses/Problem"
  /v1/staff/claims/{claimId}:
    parameters:
      - name: claimId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: staffGetClaim
      tags:
        - Staff portal
      security:
        - staff: []
      summary: "A claim: details, photos, history, allowedNext, and (ACCEPTED) replacementOptions with free delivery slots"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  claimId:
                    type: string
                  status:
                    type: string
                  statusLabel:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  policy:
                    type: object
                  customer:
                    type: object
                  incident:
                    type: object
                  contact:
                    type: object
                  replacementAddress:
                    type: object
                  photos:
                    type: array
                    items: {}
                  testClock:
                    type: object
                  altered:
                    type: object
                  handledBy:
                    type: string
                    nullable: true
                  replacement:
                    type: string
                    nullable: true
                  lastComment:
                    type: string
                    nullable: true
                  allowedNext:
                    type: array
                    items: {}
                  canOrderReplacement:
                    type: boolean
                  events:
                    type: array
                    items: {}
                  replacementOptions:
                    type: string
                    nullable: true
              example:
                {
                  claimId: CL-261025-583RR,
                  status: NEW,
                  statusLabel: New,
                  createdAt: 2026-10-25T20:58:20.808Z,
                  updatedAt: 2026-10-25T20:58:20.808Z,
                  policy:
                    {
                      policyId: pol_6196589215efebfa4974,
                      policyNumber: VC-61965892,
                      planCode: VC-2Y,
                      planName: Verdant Care 2 years,
                      coveredSku: VD-AP-M,
                      coveredItem: AutoPlanter Medium,
                      coveredType: PLANTER,
                      orderId: ord_xwykyn0axh01bp3k8xvr,
                      orderNumber: VD-261025-KEHJJ,
                      startDate: 2026-10-28,
                      endDate: 2028-10-27,
                      projected: true,
                      originalAddress:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: Ring the bell twice. Park in the driveway.,
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        }
                    },
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, givenName: Ava, email: ava.example@verdant-v1.testthis.now },
                  incident:
                    {
                      type: ACCIDENTAL_DAMAGE,
                      date: 2026-10-31,
                      description: The planter bumped the coffee table and the dome now has a 10 cm crack near the base.,
                      stillUsable: PARTLY,
                      typeLabel: Accidental damage,
                      usableLabel: Partly
                    },
                  contact: { phone: "+61412345678", bestTime: AM, bestTimeLabel: Mornings },
                  replacementAddress:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      line1: 12 Fern Street,
                      line2: null,
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO }
                    },
                  photos:
                    [
                      {
                          photoId: ph1,
                          fileName: dome-crack.png,
                          contentType: image/png,
                          size: 308,
                          url: https://verdant-v1-uploadbucket-example.s3.ap-southeast-1.amazonaws.com/claims/CL-261025-583RR/1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=...
                        }
                    ],
                  testClock: { offsetDays: 12, businessDate: 2026-11-07 },
                  altered: { incidentDate: true, policyActive: true },
                  handledBy: null,
                  replacement: null,
                  lastComment: null,
                  allowedNext: [ INVESTIGATING ],
                  canOrderReplacement: false,
                  events: [ { at: 2026-10-25T20:58:20.823Z, type: SUBMITTED, from: null, to: NEW, comment: null, by: {}, notification: {}, replacement: null } ],
                  replacementOptions: null
                }
        "404":
          $ref: "#/components/responses/Problem"
  /v1/staff/claims/{claimId}/status:
    parameters:
      - name: claimId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: staffSetClaimStatus
      tags:
        - Staff portal
      security:
        - staff: []
      summary: "Move a claim on: NEW -> INVESTIGATING; INVESTIGATING -> ACCEPTED or REJECTED (comment required). The comment is emailed."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - status
              properties:
                status:
                  type: string
                  enum:
                    - INVESTIGATING
                    - ACCEPTED
                    - REJECTED
                comment:
                  type: string
                  maxLength: 2000
            example: { status: INVESTIGATING, comment: A technician is reviewing your photos. }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  claimId:
                    type: string
                  status:
                    type: string
                  statusLabel:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  policy:
                    type: object
                  customer:
                    type: object
                  incident:
                    type: object
                  contact:
                    type: object
                  replacementAddress:
                    type: object
                  photos:
                    type: array
                    items: {}
                  testClock:
                    type: object
                  altered:
                    type: object
                  handledBy:
                    type: object
                  replacement:
                    type: string
                    nullable: true
                  lastComment:
                    type: string
                  allowedNext:
                    type: array
                    items: {}
                  canOrderReplacement:
                    type: boolean
                  events:
                    type: array
                    items: {}
                  replacementOptions:
                    type: string
                    nullable: true
              example:
                {
                  claimId: CL-261025-583RR,
                  status: INVESTIGATING,
                  statusLabel: Investigating,
                  createdAt: 2026-10-25T20:58:20.808Z,
                  updatedAt: 2026-10-25T21:00:02.707Z,
                  policy:
                    {
                      policyId: pol_6196589215efebfa4974,
                      policyNumber: VC-61965892,
                      planCode: VC-2Y,
                      planName: Verdant Care 2 years,
                      coveredSku: VD-AP-M,
                      coveredItem: AutoPlanter Medium,
                      coveredType: PLANTER,
                      orderId: ord_xwykyn0axh01bp3k8xvr,
                      orderNumber: VD-261025-KEHJJ,
                      startDate: 2026-10-28,
                      endDate: 2028-10-27,
                      projected: true,
                      originalAddress:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: Ring the bell twice. Park in the driveway.,
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        }
                    },
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, givenName: Ava, email: ava.example@verdant-v1.testthis.now },
                  incident:
                    {
                      type: ACCIDENTAL_DAMAGE,
                      date: 2026-10-31,
                      description: The planter bumped the coffee table and the dome now has a 10 cm crack near the base.,
                      stillUsable: PARTLY,
                      typeLabel: Accidental damage,
                      usableLabel: Partly
                    },
                  contact: { phone: "+61412345678", bestTime: AM, bestTimeLabel: Mornings },
                  replacementAddress:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      line1: 12 Fern Street,
                      line2: null,
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO }
                    },
                  photos:
                    [
                      {
                          photoId: ph1,
                          fileName: dome-crack.png,
                          contentType: image/png,
                          size: 308,
                          url: https://verdant-v1-uploadbucket-example.s3.ap-southeast-1.amazonaws.com/claims/CL-261025-583RR/1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=...
                        }
                    ],
                  testClock: { offsetDays: 12, businessDate: 2026-11-07 },
                  altered: { incidentDate: true, policyActive: true },
                  handledBy: { staffId: stf_gb5xdjbjr28f88htr0xw, name: Sam Staff },
                  replacement: null,
                  lastComment: A technician is reviewing your photos.,
                  allowedNext: [ ACCEPTED, REJECTED ],
                  canOrderReplacement: false,
                  events: [ { at: 2026-10-25T20:58:20.823Z, type: SUBMITTED, from: null, to: NEW, comment: null, by: {}, notification: {}, replacement: null } ],
                  replacementOptions: null
                }
        "404":
          $ref: "#/components/responses/Problem"
        "409":
          description: INVALID_TRANSITION or CLAIM_CHANGED
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/staff/claims/{claimId}/replacement:
    parameters:
      - name: claimId
        in: path
        required: true
        schema:
          type: string
    post:
      operationId: staffOrderReplacement
      tags:
        - Staff portal
      security:
        - staff: []
      summary: "ACCEPTED claims only: book a $0 replacement order on a truck (real capacity) to the claim address; the claim becomes RESOLVED and the customer is
        emailed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - date
                - window
              properties:
                date:
                  type: string
                  format: date
                window:
                  type: string
                  enum:
                    - AM
                    - PM
                    - FULL
                comment:
                  type: string
            example: { date: 2026-11-02, window: AM, comment: Replacement dome and base. }
      responses:
        "200":
          description: The resolved claim
          content:
            application/json:
              schema:
                type: object
                properties:
                  claimId:
                    type: string
                  status:
                    type: string
                  statusLabel:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  policy:
                    type: object
                  customer:
                    type: object
                  incident:
                    type: object
                  contact:
                    type: object
                  replacementAddress:
                    type: object
                  photos:
                    type: array
                    items: {}
                  testClock:
                    type: object
                  altered:
                    type: object
                  handledBy:
                    type: object
                  replacement:
                    type: object
                  lastComment:
                    type: string
                  allowedNext:
                    type: array
                    items: {}
                  canOrderReplacement:
                    type: boolean
                  events:
                    type: array
                    items: {}
                  replacementOptions:
                    type: string
                    nullable: true
              example:
                {
                  claimId: CL-261025-583RR,
                  status: RESOLVED,
                  statusLabel: Resolved,
                  createdAt: 2026-10-25T20:58:20.808Z,
                  updatedAt: 2026-10-25T21:00:02.771Z,
                  policy:
                    {
                      policyId: pol_6196589215efebfa4974,
                      policyNumber: VC-61965892,
                      planCode: VC-2Y,
                      planName: Verdant Care 2 years,
                      coveredSku: VD-AP-M,
                      coveredItem: AutoPlanter Medium,
                      coveredType: PLANTER,
                      orderId: ord_xwykyn0axh01bp3k8xvr,
                      orderNumber: VD-261025-KEHJJ,
                      startDate: 2026-10-28,
                      endDate: 2028-10-27,
                      projected: true,
                      originalAddress:
                        {
                          addressId: adr_2pf7jq915g9jfmzhma33,
                          label: Home,
                          recipientName: Ava Nguyen,
                          phone: "+61412345678",
                          line1: 12 Fern Street,
                          line2: "",
                          suburb: GLEN WAVERLEY,
                          state: VIC,
                          postcode: "3150",
                          siteAccess: GROUND,
                          instructions: Ring the bell twice. Park in the driveway.,
                          isDefault: true,
                          serviceArea: {},
                          createdAt: 2026-10-25T20:58:19.473Z
                        }
                    },
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, givenName: Ava, email: ava.example@verdant-v1.testthis.now },
                  incident:
                    {
                      type: ACCIDENTAL_DAMAGE,
                      date: 2026-10-31,
                      description: The planter bumped the coffee table and the dome now has a 10 cm crack near the base.,
                      stillUsable: PARTLY,
                      typeLabel: Accidental damage,
                      usableLabel: Partly
                    },
                  contact: { phone: "+61412345678", bestTime: AM, bestTimeLabel: Mornings },
                  replacementAddress:
                    {
                      addressId: adr_2pf7jq915g9jfmzhma33,
                      label: Home,
                      recipientName: Ava Nguyen,
                      phone: "+61412345678",
                      instructions: "Side gate, code 4821. Dog is friendly.",
                      line1: 12 Fern Street,
                      line2: null,
                      suburb: GLEN WAVERLEY,
                      state: VIC,
                      postcode: "3150",
                      siteAccess: GROUND,
                      serviceArea: { serviceable: true, reason: null, distanceFromMelbourneKm: 19.1, roadKmFromWarehouse: 20, zone: METRO }
                    },
                  photos:
                    [
                      {
                          photoId: ph1,
                          fileName: dome-crack.png,
                          contentType: image/png,
                          size: 308,
                          url: https://verdant-v1-uploadbucket-example.s3.ap-southeast-1.amazonaws.com/claims/CL-261025-583RR/1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=...
                        }
                    ],
                  testClock: { offsetDays: 12, businessDate: 2026-11-07 },
                  altered: { incidentDate: true, policyActive: true },
                  handledBy: { staffId: stf_gb5xdjbjr28f88htr0xw, name: Sam Staff },
                  replacement:
                    {
                      status: ORDERED,
                      orderId: ord_z9gh0cb736ewxdp57vmw,
                      orderNumber: VD-261025-WETX9,
                      date: 2026-11-02,
                      window: AM,
                      windowLabel: Morning,
                      windowTimes: 7:00am - 12:00pm,
                      truck: T01,
                      orderedAt: 2026-10-25T21:00:02.771Z,
                      orderedBy: { type: staff, id: stf_gb5xdjbjr28f88htr0xw, name: Sam Staff }
                    },
                  lastComment: Replacement dome and base.,
                  allowedNext: [],
                  canOrderReplacement: false,
                  events: [ { at: 2026-10-25T20:58:20.823Z, type: SUBMITTED, from: null, to: NEW, comment: null, by: {}, notification: {}, replacement: null } ],
                  replacementOptions: null
                }
        "404":
          $ref: "#/components/responses/Problem"
        "409":
          description: INVALID_TRANSITION, CLAIM_CHANGED or SLOT_UNAVAILABLE
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/staff/cases/{caseId}/link:
    post:
      operationId: staffLinkCase
      tags:
        - Staff portal
      security:
        - staff: []
      summary: Link an unlinked case to one of your customers
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            example: C-260927-7K3M9
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                userId:
                  type: string
                email:
                  type: string
            example: { userId: usr_btgygbap02w2g1cd3qcn }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  caseId:
                    type: string
                  type:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  channel:
                    type: string
                  initiator:
                    type: object
                  userId:
                    type: string
                  customer:
                    type: object
                  order:
                    type: string
                    nullable: true
                  assignedStaff:
                    type: string
                    nullable: true
                  senderVerified:
                    type: boolean
                  createdAt:
                    type: string
                    format: date-time
                  updatedAt:
                    type: string
                    format: date-time
                  closedAt:
                    type: string
                    nullable: true
                  reopenableUntil:
                    type: string
                    nullable: true
                  canReopen:
                    type: boolean
                  messageCount:
                    type: integer
                  lastMessageAt:
                    type: string
                    format: date-time
                  lastMessageBy:
                    type: string
                  nets:
                    type: array
                    items: {}
                  messages:
                    type: array
                    items: {}
              example:
                {
                  caseId: C-261025-ERD7B,
                  type: ENQUIRY,
                  subject: Gift wrapping?,
                  status: OPEN,
                  channel: EMAIL,
                  initiator: { email: ava.personal@verdant-v1.testthis.now, name: Ava (personal) },
                  userId: usr_btgygbap02w2g1cd3qcn,
                  customer: { userId: usr_btgygbap02w2g1cd3qcn, name: Ava Nguyen, email: ava.example@verdant-v1.testthis.now },
                  order: null,
                  assignedStaff: null,
                  senderVerified: true,
                  createdAt: 2026-10-25T21:00:02.675Z,
                  updatedAt: 2026-10-25T21:00:02.689Z,
                  closedAt: null,
                  reopenableUntil: null,
                  canReopen: false,
                  messageCount: 1,
                  lastMessageAt: 2026-10-25T21:00:02.675Z,
                  lastMessageBy: customer,
                  nets: [ 203.0.113.77 ],
                  messages:
                    [
                      {
                          messageId: m_yynv2fhzhh,
                          at: 2026-10-25T21:00:02.675Z,
                          kind: MESSAGE,
                          author: { type: email, name: Ava (personal), email: ava.personal@verdant-v1.testthis.now, id: null },
                          channel: EMAIL,
                          body: Can the coffee table book be gift wrapped?,
                          event: null,
                          ip: 203.0.113.77,
                          notification: { status: SENT, to: ava.personal@verdant-v1.testthis.now, messageId: local-28 }
                        }
                    ]
                }
        "404":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/enrol:
    post:
      operationId: warehouseEnrol
      tags:
        - Warehouse
      security: []
      summary: Warehouse enrolment (202); an existing staff account is emailed how to add access instead
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                password:
                  type: string
                givenName:
                  type: string
                familyName:
                  type: string
                jobTitle:
                  type: string
                acceptTerms:
                  type: boolean
            example: { email: wes.warehouse@verdant-v1.testthis.now, password: Plant3r!Dock2026, givenName: Wes, familyName: Warehouse, jobTitle: Dispatch, acceptTerms: true }
      responses:
        "202":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  email:
                    type: string
                  message:
                    type: string
                  codeExpiresInSeconds:
                    type: integer
              example:
                {
                  status: VERIFICATION_SENT,
                  email: wes.warehouse@verdant-v1.testthis.now,
                  message: Check your email for a 6-digit verification code.,
                  codeExpiresInSeconds: 900
                }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/login:
    post:
      operationId: warehouseLogin
      tags:
        - Warehouse
      security: []
      summary: Sign-in step 1 (password)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                password:
                  type: string
            example: { email: sam.staff@verdant-v1.testthis.now, password: Plant3r!Dock2026 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  mfaToken:
                    type: string
                  expiresIn:
                    type: integer
                  methods:
                    type: array
                    items: {}
              example: { status: MFA_REQUIRED, mfaToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp..., expiresIn: 300, methods: [ totp, recovery_code ] }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/login/mfa:
    post:
      operationId: warehouseLoginMfa
      tags:
        - Warehouse
      security: []
      summary: Sign-in step 2 (TOTP) - tokens, or APP_ACCESS_AVAILABLE with a grantToken if the account lacks warehouse access
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mfaToken:
                  type: string
                code:
                  type: string
                recoveryCode:
                  type: string
            example: { mfaToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp..., code: "775038" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  app:
                    type: string
                  appLabel:
                    type: string
                  currentApps:
                    type: array
                    items: {}
                  grantToken:
                    type: string
                  expiresIn:
                    type: integer
                  message:
                    type: string
              example:
                {
                  status: APP_ACCESS_AVAILABLE,
                  app: warehouse,
                  appLabel: Warehouse,
                  currentApps: [ staff ],
                  grantToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 300,
                  message: Your account doesn't have Warehouse access yet. Add it to use the same sign-in here.
                }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/access:
    post:
      operationId: warehouseGrantAccess
      tags:
        - Warehouse
      security: []
      summary: Add warehouse access to the signed-in account (grant token from login/mfa) and get warehouse tokens
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                grantToken:
                  type: string
                confirm:
                  type: boolean
                  enum:
                    - true
            example: { grantToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp..., confirm: true }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  app:
                    type: string
                  tokenType:
                    type: string
                  accessToken:
                    type: string
                  expiresIn:
                    type: integer
                  refreshToken:
                    type: string
                  refreshExpiresIn:
                    type: integer
                  user:
                    type: object
              example:
                {
                  status: AUTHENTICATED,
                  app: warehouse,
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: stf_gb5xdjbjr28f88htr0xw.ses_56qtqwsefcp...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: stf_gb5xdjbjr28f88htr0xw,
                      email: sam.staff@verdant-v1.testthis.now,
                      givenName: Sam,
                      familyName: Staff,
                      status: ACTIVE,
                      mfaEnabled: true,
                      role: STAFF,
                      jobTitle: Customer service,
                      apps: [ staff, warehouse ],
                      marketingOptIn: false,
                      createdAt: 2026-10-25T20:58:21.229Z
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/schedule:
    get:
      operationId: warehouseGetSchedule
      tags:
        - Warehouse
      security:
        - warehouse: []
      summary: Orders per truck per day (counts by window and order numbers) across all customers
      parameters:
        - name: from
          in: query
          schema:
            type: string
            format: date
          example: 2026-10-28
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 28
          example: 1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  today:
                    type: string
                    format: date
                  from:
                    type: string
                    format: date
                  to:
                    type: string
                    format: date
                  days:
                    type: array
                    items: {}
              example:
                {
                  today: 2026-10-26,
                  from: 2026-10-28,
                  to: 2026-10-28,
                  days:
                    [
                      {
                          date: 2026-10-28,
                          dayOfWeek: Wednesday,
                          closedReason: null,
                          trucks: 3,
                          defaultTrucks: 3,
                          maxTrucks: 99,
                          unitsPerTruck: 20,
                          unitsUsed: 4,
                          unitsCapacity: 60,
                          fleetChangedBy: null,
                          fleetChangedAt: null,
                          orderCount: 1,
                          trucksInUse: 1,
                          schedule: [ { truck: T01, orderCount: 1, byWindow: { AM: 1, PM: 0, FULL: 0 }, unitsUsed: 4, unitsFree: 16, spaceUnits: 20, orderIds: [ VD-261025-KEHJJ ] } ]
                        }
                    ]
                }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/fleet:
    post:
      operationId: warehouseSetTrucks
      tags:
        - Warehouse
      security:
        - warehouse: []
      summary: Add trucks to a delivery day (increase only, max 99)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                date:
                  type: string
                  format: date
                trucks:
                  type: integer
                  minimum: 1
                  maximum: 99
            example: { date: 2026-11-10, trucks: 4 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  date:
                    type: string
                    format: date
                  dayOfWeek:
                    type: string
                  closedReason:
                    type: string
                    nullable: true
                  trucks:
                    type: integer
                  defaultTrucks:
                    type: integer
                  maxTrucks:
                    type: integer
                  unitsPerTruck:
                    type: integer
                  unitsUsed:
                    type: integer
                  unitsCapacity:
                    type: integer
                  fleetChangedBy:
                    type: object
                  fleetChangedAt:
                    type: string
                    format: date-time
                  orderCount:
                    type: integer
                  trucksInUse:
                    type: integer
                  schedule:
                    type: array
                    items: {}
              example:
                {
                  date: 2026-11-10,
                  dayOfWeek: Tuesday,
                  closedReason: null,
                  trucks: 4,
                  defaultTrucks: 3,
                  maxTrucks: 99,
                  unitsPerTruck: 20,
                  unitsUsed: 0,
                  unitsCapacity: 80,
                  fleetChangedBy: { staffId: stf_gb5xdjbjr28f88htr0xw, name: Sam Staff },
                  fleetChangedAt: 2026-10-25T21:00:33.813Z,
                  orderCount: 0,
                  trucksInUse: 0,
                  schedule: [ { truck: T01, orderCount: 0, byWindow: { AM: 0, PM: 0, FULL: 0 }, unitsUsed: 0, unitsFree: 20, spaceUnits: 20, orderIds: [] } ]
                }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/me:
    get:
      operationId: warehouseGetMe
      tags:
        - Warehouse
      security:
        - warehouse: []
      summary: The signed-in warehouse user
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  userId:
                    type: string
                  email:
                    type: string
                  givenName:
                    type: string
                  familyName:
                    type: string
                  status:
                    type: string
                  mfaEnabled:
                    type: boolean
                  role:
                    type: string
                  jobTitle:
                    type: string
                  apps:
                    type: array
                    items: {}
                  marketingOptIn:
                    type: boolean
                  createdAt:
                    type: string
                    format: date-time
                  lastLoginAt:
                    type: string
                    format: date-time
                  recoveryCodesRemaining:
                    type: integer
              example:
                {
                  userId: stf_gb5xdjbjr28f88htr0xw,
                  email: sam.staff@verdant-v1.testthis.now,
                  givenName: Sam,
                  familyName: Staff,
                  status: ACTIVE,
                  mfaEnabled: true,
                  role: STAFF,
                  jobTitle: Customer service,
                  apps: [ staff, warehouse ],
                  marketingOptIn: false,
                  createdAt: 2026-10-25T20:58:21.229Z,
                  lastLoginAt: 2026-10-25T21:00:33.789Z,
                  recoveryCodesRemaining: 8
                }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/staff/access:
    post:
      operationId: staffGrantAccess
      tags:
        - Warehouse
      security: []
      summary: Add staff-portal access to an account (grant token from /v1/staff/login/mfa)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                grantToken:
                  type: string
                confirm:
                  type: boolean
                  enum:
                    - true
            example: { grantToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp..., confirm: true }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  app:
                    type: string
                  tokenType:
                    type: string
                  accessToken:
                    type: string
                  expiresIn:
                    type: integer
                  refreshToken:
                    type: string
                  refreshExpiresIn:
                    type: integer
                  user:
                    type: object
              example:
                {
                  status: AUTHENTICATED,
                  app: staff,
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: stf_6gnjq8qnadqgd6gdxgmc.ses_7p3b8j9n8kk...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: stf_6gnjq8qnadqgd6gdxgmc,
                      email: wes.warehouse@verdant-v1.testthis.now,
                      givenName: Wes,
                      familyName: Warehouse,
                      status: ACTIVE,
                      mfaEnabled: true,
                      role: STAFF,
                      jobTitle: null,
                      apps: [ warehouse, staff ],
                      marketingOptIn: false,
                      createdAt: 2026-10-25T21:00:33.829Z
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/enrol/resend:
    post:
      operationId: warehouseResendVerification
      tags:
        - Warehouse
      security: []
      summary: Resend the warehouse enrolment code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
            example: { email: wes.warehouse@verdant-v1.testthis.now }
      responses:
        "200":
          description: OK
        "202":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  email:
                    type: string
                  message:
                    type: string
              example: { status: VERIFICATION_SENT, email: wes.warehouse@verdant-v1.testthis.now, message: "If that account is awaiting verification, a new code is on its way." }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/verify-email:
    post:
      operationId: warehouseVerifyEmail
      tags:
        - Warehouse
      security: []
      summary: Confirm the warehouse enrolment code; returns an enrolmentToken (scope warehouse_mfa_enrol)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                code:
                  type: string
            example: { email: wes.warehouse@verdant-v1.testthis.now, code: "236577" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  enrolmentToken:
                    type: string
                  expiresIn:
                    type: integer
                  message:
                    type: string
              example:
                {
                  status: MFA_ENROLMENT_REQUIRED,
                  enrolmentToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  message: Email verified. Set up your authenticator app to finish.
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/mfa/enrol:
    post:
      operationId: warehouseStartMfaEnrolment
      tags:
        - Warehouse
      security:
        - warehouseEnrolment: []
      summary: Start TOTP enrolment (secret, otpauth URI, QR SVG)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret:
                    type: string
                  otpauthUri:
                    type: string
                  qrSvg:
                    type: string
                  algorithm:
                    type: string
                  digits:
                    type: integer
                  period:
                    type: integer
                  message:
                    type: string
              example:
                {
                  secret: OS65XSMBFTKMCPPKM3VRSTCFT74SP3BI,
                  otpauthUri: otpauth://totp/Verdant%20Dock%3Awes.warehouse%40verdant-v1.testthis.now?secret=OS65XSMBFTKMCPPKM3VRSTCFT74SP3BI&issuer=Verdant%20Dock&algorithm=SHA1&digits=6&period=30,
                  qrSvg: <svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>,
                  algorithm: SHA1,
                  digits: 6,
                  period: 30,
                  message: "Scan the QR code (or type the secret) into an authenticator app, then confirm with a 6-digit code."
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/mfa/activate:
    post:
      operationId: warehouseActivateMfa
      tags:
        - Warehouse
      security:
        - warehouseEnrolment: []
      summary: Activate TOTP; returns warehouse tokens and recovery codes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code:
                  type: string
            example: { code: "215799" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  recoveryCodes:
                    type: array
                    items: {}
                  tokenType:
                    type: string
                  accessToken:
                    type: string
                  expiresIn:
                    type: integer
                  refreshToken:
                    type: string
                  refreshExpiresIn:
                    type: integer
                  user:
                    type: object
              example:
                {
                  status: ACTIVE,
                  recoveryCodes: [ 8VP3-W46E, 6TDC-2WGS, XHVS-E0VG, 1BPH-SAA4, CJC7-SVD4, 6ANW-NN6X, AT2E-S53A, R278-RBYN ],
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: stf_6gnjq8qnadqgd6gdxgmc.ses_h7pxnq1pwck...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: stf_6gnjq8qnadqgd6gdxgmc,
                      email: wes.warehouse@verdant-v1.testthis.now,
                      givenName: Wes,
                      familyName: Warehouse,
                      status: ACTIVE,
                      mfaEnabled: true,
                      role: STAFF,
                      jobTitle: null,
                      apps: [ warehouse ],
                      marketingOptIn: false,
                      createdAt: 2026-10-25T21:00:33.829Z
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/token/refresh:
    post:
      operationId: warehouseRefreshToken
      tags:
        - Warehouse
      security: []
      summary: Rotate a warehouse refresh token (customer and staff-portal refresh tokens are rejected)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                refreshToken:
                  type: string
            example: { refreshToken: stf_gb5xdjbjr28f88htr0xw.ses_56qtqwsefcp... }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokenType:
                    type: string
                  accessToken:
                    type: string
                  expiresIn:
                    type: integer
                  refreshToken:
                    type: string
                  refreshExpiresIn:
                    type: integer
                  user:
                    type: object
              example:
                {
                  tokenType: Bearer,
                  accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtp...,
                  expiresIn: 900,
                  refreshToken: stf_gb5xdjbjr28f88htr0xw.ses_56qtqwsefcp...,
                  refreshExpiresIn: 43200,
                  user:
                    {
                      userId: stf_gb5xdjbjr28f88htr0xw,
                      email: sam.staff@verdant-v1.testthis.now,
                      givenName: Sam,
                      familyName: Staff,
                      status: ACTIVE,
                      mfaEnabled: true,
                      role: STAFF,
                      jobTitle: Customer service,
                      apps: [ staff, warehouse ],
                      marketingOptIn: false,
                      createdAt: 2026-10-25T20:58:21.229Z
                    }
                }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/password/forgot:
    post:
      operationId: warehouseForgotPassword
      tags:
        - Warehouse
      security: []
      summary: Email a warehouse password reset code (202)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
            example: { email: wes.warehouse@verdant-v1.testthis.now }
      responses:
        "200":
          description: OK
        "202":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string
                  codeExpiresInSeconds:
                    type: integer
              example: { status: RESET_CODE_SENT, message: "If an account exists for that email, a reset code is on its way.", codeExpiresInSeconds: 900 }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/password/reset:
    post:
      operationId: warehouseResetPassword
      tags:
        - Warehouse
      security: []
      summary: Reset a warehouse password with the emailed code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                code:
                  type: string
                newPassword:
                  type: string
            example: { email: wes.warehouse@verdant-v1.testthis.now, code: "979460", newPassword: N3w!Planter2027 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string
              example: { status: PASSWORD_RESET, message: Your password has been changed. Sign in with your new password and authenticator code. }
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/warehouse/logout:
    post:
      operationId: warehouseLogout
      tags:
        - Warehouse
      security:
        - warehouse: []
      summary: Sign out (?all=true for every session)
      responses:
        "200":
          description: OK
        "204":
          description: OK
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Access token (scope customer)
    staff:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Staff access token (scope staff)
    warehouse:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Warehouse access token (scope warehouse)
    staffEnrolment:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Staff enrolment token (scope staff_mfa_enrol) from staff verify-email or login
    enrolment:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Enrolment token (scope mfa_enrol) from verify-email or login
    mailboxKey:
      type: apiKey
      in: header
      name: X-Mailbox-Key
      description: "Only when deployed with MailboxAccess=key: test-mailbox key generated by the stack (deploy.ps1 prints it)"
    warehouseEnrolment:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Warehouse enrolment token (scope warehouse_mfa_enrol) from warehouse verify-email or login
  responses:
    Problem:
      description: RFC 7807 problem
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
  schemas:
    Problem:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        code:
          type: string
          example: VALIDATION_FAILED
        detail:
          type: string
        instance:
          type: string
        correlationId:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
        orderId:
          type: string
        orderNumber:
          type: string
        responseCode:
          type: string
    Tokens:
      type: object
      properties:
        tokenType:
          type: string
          example: Bearer
        accessToken:
          type: string
        expiresIn:
          type: integer
          example: 900
        refreshToken:
          type: string
        refreshExpiresIn:
          type: integer
          example: 43200
        user:
          type: object
        recoveryCodes:
          type: array
          items:
            type: string
          description: Only on MFA activation
        status:
          type: string
        recoveryCodesRemaining:
          type: integer
    Product:
      type: object
      properties:
        sku:
          type: string
          example: VD-AP-M
        type:
          type: string
          enum:
            - PLANTER
            - PLANT
        size:
          type: string
          enum:
            - S
            - M
            - L
        name:
          type: string
        priceCents:
          type: integer
        capacityKg:
          type: integer
          description: Planters only - rated plant+soil load
        massKg:
          type: number
          description: Plants only - plant+soil weight
        dimensionsCm:
          type: object
          properties:
            w:
              type: number
            d:
              type: number
            h:
              type: number
        package:
          type: object
          properties:
            w:
              type: number
            d:
              type: number
            h:
              type: number
            kg:
              type: number
        installMinutes:
          type: integer
        coverOptions:
          type: array
          items:
            type: object
        shortDescription:
          type: string
        description:
          type: string
        growthNote:
          type: string
        maxPlantKg:
          type: integer
        dockCm:
          type: object
        requiresTechnician:
          type: boolean
        features:
          type: array
          items: {}
        coverPlans:
          type: array
          items: {}
        image:
          type: string
        bundleOffer:
          type: object
    ServiceArea:
      type: object
      properties:
        serviceable:
          type: boolean
        reason:
          type: string
          nullable: true
        distanceFromMelbourneKm:
          type: number
        roadKmFromWarehouse:
          type: integer
        zone:
          type: string
          enum:
            - METRO
            - OUTER
            - REGIONAL
            - OUT_OF_AREA
        locality:
          type: object
        indicativeFreightCents:
          type: integer
        warehouse:
          type: object
    AddressInput:
      type: object
      required:
        - line1
        - suburb
        - state
        - postcode
      properties:
        label:
          type: string
          example: Home
        recipientName:
          type: string
        phone:
          type: string
        line1:
          type: string
          example: 12 Fern Street
        line2:
          type: string
        suburb:
          type: string
          example: Glen Waverley
        state:
          type: string
          enum:
            - VIC
            - NSW
            - ACT
            - QLD
            - SA
            - WA
            - TAS
            - NT
        postcode:
          type: string
          example: "3150"
        siteAccess:
          type: string
          enum:
            - GROUND
            - LIFT
            - STAIRS
        instructions:
          type: string
        isDefault:
          type: boolean
        addressId:
          type: string
        serviceArea:
          type: object
        createdAt:
          type: string
          format: date-time
    Address:
      allOf:
        - $ref: "#/components/schemas/AddressInput"
        - type: object
          properties:
            addressId:
              type: string
            serviceArea:
              $ref: "#/components/schemas/ServiceArea"
            label:
              type: string
            recipientName:
              type: string
            phone:
              type: string
            line1:
              type: string
            line2:
              type: string
            suburb:
              type: string
            state:
              type: string
            postcode:
              type: string
            siteAccess:
              type: string
            instructions:
              type: string
            isDefault:
              type: boolean
            createdAt:
              type: string
              format: date-time
    Cart:
      type: object
      properties:
        items:
          type: array
          items:
            type: object
        version:
          type: integer
        lines:
          type: array
          items:
            type: object
        totals:
          type: object
        issues:
          type: array
          items:
            type: object
            properties:
              code:
                type: string
                example: PLANT_REQUIRES_PLANTER
              message:
                type: string
        currency:
          type: string
        deliveryBreakdown:
          type: array
          items: {}
        deliveryMethod:
          type: string
        benefits:
          type: array
          items: {}
    Order:
      type: object
      properties:
        orderId:
          type: string
        orderNumber:
          type: string
          example: VD-260926-4F7K2
        status:
          type: string
          enum:
            - PENDING_PAYMENT
            - CONFIRMED
            - DISPATCH_SCHEDULED
            - OUT_FOR_DELIVERY
            - DELIVERED
            - PAYMENT_DECLINED
            - FAILED
            - CANCELLED
        recordedStatus:
          type: string
        projected:
          type: boolean
        statusHistory:
          type: array
          items:
            type: object
            properties:
              status:
                type: string
              label:
                type: string
              at:
                type: string
                format: date-time
              simulated:
                type: boolean
              businessDate:
                type: string
                format: date
        policies:
          type: array
          items:
            $ref: "#/components/schemas/Policy"
        checkin:
          allOf:
            - $ref: "#/components/schemas/Checkin"
          nullable: true
          type: object
        lines:
          type: array
          items:
            type: object
        deliveryBreakdown:
          type: array
          items:
            type: object
        totals:
          type: object
        address:
          $ref: "#/components/schemas/Address"
        delivery:
          type: object
          properties:
            method:
              type: string
              enum:
                - TRUCK
                - COURIER
            date:
              type: string
              description: "Truck: booked date. Courier: estimated delivery date"
            window:
              type: string
              enum:
                - AM
                - PM
                - FULL
                - COURIER
            truck:
              type: string
            jobMinutes:
              type: integer
            status:
              type: string
            dispatchDate:
              type: string
              description: Courier only
            carrier:
              type: string
              description: Courier only
            trackingNumber:
              type: string
              description: Courier only
            windowLabel:
              type: string
            windowTimes:
              type: string
            spaceUnits:
              type: integer
            installMinutes:
              type: integer
            shippingKg:
              type: number
            shippingM3:
              type: number
            warehouse:
              type: string
            booking:
              type: object
            transitBusinessDays:
              type: integer
            deliveredAt:
              type: string
              format: date-time
        payment:
          type: object
          properties:
            brand:
              type: string
            last4:
              type: string
            authCode:
              type: string
            responseCode:
              type: string
            transactionId:
              type: string
            expiry:
              type: string
            token:
              type: string
            amountCents:
              type: integer
            approved:
              type: boolean
            responseText:
              type: string
            processedAt:
              type: string
              format: date-time
            gateway:
              type: string
        refund:
          type: object
          nullable: true
        cancellable:
          type: boolean
        deliveryDetails:
          type: object
          properties:
            recipientName:
              type: string
            phone:
              type: string
              nullable: true
            instructions:
              type: string
            editable:
              type: boolean
            editableUntil:
              type: string
              format: date
              description: Last day changes are accepted online (the day before delivery)
            reason:
              type: string
              nullable: true
              enum:
                - DELIVERY_DAY
                - DELIVERED
                - CANCELLED
                - NOT_ACTIVE
                - null
            message:
              type: string
              nullable: true
              description: "On delivery day: raise a support case to change the instructions"
            history:
              type: array
              items:
                type: object
                properties:
                  at:
                    type: string
                    format: date-time
                  changes:
                    type: array
                    items:
                      type: string
                  simulated:
                    type: boolean
        invoice:
          type: object
          nullable: true
          properties:
            invoiceNumber:
              type: string
              example: INV-260926-4F7K2
            filename:
              type: string
            notARealTaxInvoice:
              type: boolean
            pdf:
              type: string
              example: /v1/orders/ord_123/invoice.pdf
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        kind:
          type: string
        replacementFor:
          type: string
          nullable: true
        placedWithTestClockOffsetDays:
          type: integer
    Policy:
      type: object
      properties:
        policyId:
          type: string
        policyNumber:
          type: string
          example: VC-7K2M9QXD
        status:
          type: string
          enum:
            - ACTIVE
            - EXPIRED
        planCode:
          type: string
          enum:
            - VC-2Y
            - VC-3Y
            - VC-5Y
            - TC-1Y
            - TC-2Y
        planName:
          type: string
        termYears:
          type: integer
        coveredSku:
          type: string
        coveredItem:
          type: string
        unit:
          type: integer
        premiumCents:
          type: integer
        orderId:
          type: string
        orderNumber:
          type: string
        startDate:
          type: string
          format: date
          nullable: true
        endDate:
          type: string
          format: date
          nullable: true
        daysRemaining:
          type: integer
          nullable: true
        projected:
          type: boolean
          description: True when shown only because the test clock is past the delivery date
        summary:
          type: string
        coveredType:
          type: string
        unitsOnLine:
          type: integer
        claims:
          type: object
    TestClock:
      type: object
      properties:
        enabled:
          type: boolean
        offsetDays:
          type: integer
        simulated:
          type: boolean
        realDate:
          type: string
          format: date
        businessDate:
          type: string
          format: date
        maxOffsetDays:
          type: integer
        notice:
          type: string
          nullable: true
        resetsOn:
          type: string
          example: login
    Checkin:
      type: object
      properties:
        checkinId:
          type: string
        number:
          type: string
          example: VD-260926-4F7K2-EC
        kind:
          type: string
          enum:
            - SERVICE
        title:
          type: string
        status:
          type: string
          enum:
            - NOT_ACTIVATED
            - BOOKED
            - COMPLETED
            - EXPIRED
            - VOID
        recordedStatus:
          type: string
        projected:
          type: boolean
        valueCents:
          type: integer
          example: 30000
        parentOrderId:
          type: string
        parentOrderNumber:
          type: string
        deliveryDate:
          type: string
          format: date
        booking:
          type: object
          nullable: true
          properties:
            date:
              type: string
              format: date
            window:
              type: string
              enum:
                - AM
                - PM
            windowTimes:
              type: string
            windowLabel:
              type: string
            bookedAt:
              type: string
              format: date-time
            bookedUnderTestClock:
              type: boolean
        bookableFrom:
          type: string
          format: date
          nullable: true
        bookableUntil:
          type: string
          format: date
        canBook:
          type: boolean
        canCancelBooking:
          type: boolean
        description:
          type: string
        visitMinutes:
          type: integer
        address:
          type: object
        createdAt:
          type: string
          format: date-time
        history:
          type: array
          items: {}
    Mail:
      type: object
      properties:
        messageId:
          type: string
        to:
          type: string
        from:
          type: string
        subject:
          type: string
        receivedAt:
          type: string
          format: date-time
        code:
          type: string
          nullable: true
          example: "482913"
        links:
          type: array
          items:
            type: string
        text:
          type: string
        html:
          type: string
        attachments:
          type: array
          items:
            type: object
            properties:
              index:
                type: integer
              filename:
                type: string
                example: INV-260926-4F7K2.pdf
              contentType:
                type: string
                example: application/pdf
              size:
                type: integer
              sha256:
                type: string
              stored:
                type: boolean
              downloadUrl:
                type: string
              textUrl:
                type: string
                description: PDFs only
