openapi: 3.0.3
info:
  title: PharmaOne API
  version: 2.0.0
  description: |
    Partner-facing HTTP API for POS systems, ERP connectors, n8n, WooCommerce/JTL plugins, and custom integrations.

    **Two authentication modes:**

    1. **v2 (recommended)** — Exchange an org integration API key for a short-lived JWT, then call scoped endpoints with `Authorization: Bearer`.
    2. **v1 order-requests** — Submit order requests with API key only (no JWT).

    Configure API keys and webhooks in Manager → **Org Settings → Integrations**.

    Full JSON schemas: see **components/schemas** in this file and the [Data models guide](../external-api/schemas.md).
  contact:
    name: PharmaOne Integrations
  license:
    name: Proprietary

servers:
  - url: https://manager.prod.pharmaone.shop
    description: Production

tags:
  - name: Auth
    description: Exchange integration API key for JWT
  - name: Shops
    description: List organization shops
  - name: Products
    description: Shop catalog and product management
  - name: Inventory
    description: Org-level stock and adjustments
  - name: Shop assignment
    description: Attach, detach, and override shop products
  - name: Orders
    description: Read and write pharmacy orders
  - name: Order requests
    description: Submit requests for staff review
  - name: Prescriptions
    description: Prescription request ingest and listing
  - name: Reports
    description: Read-only analytics and statistics
  - name: Webhooks
    description: Outbound events PharmaOne POSTs to your HTTPS endpoint
  - name: External Telemedicine
    description: Shop-scoped external order endpoint (not on Manager host)

paths:
  /api/v2/public/orgs/{orgId}/auth/token:
    post:
      tags: [Auth]
      summary: Exchange API key for integration JWT
      description: |
        Returns a Bearer token valid for **1 hour**.
      operationId: exchangeToken
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: apikey
          in: query
          schema:
            type: string
          description: Alternative to header — API key as query parameter
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Token issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              example:
                access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                token_type: Bearer
                expires_in: 3600
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v2/public/orgs/{orgId}/shops:
    get:
      tags: [Shops]
      summary: List active shops
      operationId: listShops
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [products.read]
      responses:
        '200':
          description: Shop list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Shop'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v2/public/orgs/{orgId}/products:
    get:
      tags: [Products]
      summary: List shop product catalog
      description: Requires `shop_id`. Shop must belong to your organization.
      operationId: listProducts
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: shop_id
          in: query
          required: true
          schema:
            type: string
          example: shop1
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      security:
        - BearerAuth: [products.read]
      responses:
        '200':
          description: Paginated product catalog
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v2/public/orgs/{orgId}/products/{p1Id}:
    get:
      tags: [Products]
      summary: Get single shop product
      operationId: getProduct
      parameters:
        - $ref: '#/components/parameters/orgId'
        - $ref: '#/components/parameters/p1Id'
        - name: shop_id
          in: query
          required: true
          schema:
            type: string
      security:
        - BearerAuth: [products.read]
      responses:
        '200':
          description: Product detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShopProduct'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Products]
      summary: Update product fields
      description: Product must exist in org inventory. Supports name, slug, description, category, price, THC/CBD, and availability fields.
      operationId: updateProduct
      parameters:
        - $ref: '#/components/parameters/orgId'
        - $ref: '#/components/parameters/p1Id'
      security:
        - BearerAuth: [products.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductUpdate'
      responses:
        '200':
          description: Updated
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v2/public/orgs/{orgId}/products/add-to-inventory:
    post:
      tags: [Products]
      summary: Add catalog product to org inventory
      operationId: addToInventory
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [products.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddToInventoryRequest'
      responses:
        '200':
          description: Product added to org inventory
        '404':
          description: Product not found in global catalog

  /api/v2/public/orgs/{orgId}/inventory:
    get:
      tags: [Inventory]
      summary: List org inventory overview
      operationId: listInventory
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [inventory.read]
      responses:
        '200':
          description: Inventory rows
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/InventoryRow'

  /api/v2/public/orgs/{orgId}/inventory/adjust:
    post:
      tags: [Inventory]
      summary: Adjust stock on hand or incoming
      operationId: adjustInventory
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [inventory.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InventoryAdjustRequest'
      responses:
        '200':
          description: Stock adjusted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /api/v2/public/orgs/{orgId}/inventory/{p1Id}/log:
    get:
      tags: [Inventory]
      summary: Stock change log for product
      operationId: inventoryLog
      parameters:
        - $ref: '#/components/parameters/orgId'
        - $ref: '#/components/parameters/p1Id'
      security:
        - BearerAuth: [inventory.read]
      responses:
        '200':
          description: Up to 200 log entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/InventoryLogEntry'

  /api/v2/public/orgs/{orgId}/inventory/{p1Id}/history:
    get:
      tags: [Inventory]
      summary: Stock quantity history
      operationId: inventoryHistory
      parameters:
        - $ref: '#/components/parameters/orgId'
        - $ref: '#/components/parameters/p1Id'
        - name: days
          in: query
          schema:
            type: integer
            default: 30
            maximum: 365
      security:
        - BearerAuth: [inventory.read]
      responses:
        '200':
          description: Historical stock series

  /api/v2/public/orgs/{orgId}/shops/attach-product:
    post:
      tags: [Shop assignment]
      summary: Assign product to shop
      description: Product must be in org inventory with stock > 0. `shop_id` must belong to org.
      operationId: attachProduct
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [products.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShopProductAttach'
      responses:
        '200':
          description: Attached
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v2/public/orgs/{orgId}/shops/detach-product:
    post:
      tags: [Shop assignment]
      summary: Remove product from shop
      operationId: detachProduct
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [products.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShopProductDetach'
      responses:
        '200':
          description: Detached

  /api/v2/public/orgs/{orgId}/shops/product-overrides:
    post:
      tags: [Shop assignment]
      summary: Set per-shop product overrides
      operationId: shopProductOverrides
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [products.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShopProductOverrides'
      responses:
        '200':
          description: Overrides applied

  /api/v2/public/orgs/{orgId}/orders:
    get:
      tags: [Orders]
      summary: List orders
      operationId: listOrders
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: status
          in: query
          schema:
            type: string
        - name: shop_id
          in: query
          schema:
            type: string
      security:
        - BearerAuth: [orders.read]
      responses:
        '200':
          description: Paginated orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderListResponse'
    post:
      tags: [Orders]
      summary: Create or upsert order
      description: Upserts by `external_id` + `shop_id`. Line items use **`qty`** not `quantity`.
      operationId: createOrder
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [orders.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderUpsert'
      responses:
        '200':
          description: Order created or updated
        '500':
          $ref: '#/components/responses/ServerError'

  /api/v2/public/orgs/{orgId}/orders/{orderId}:
    get:
      tags: [Orders]
      summary: Get order with items
      operationId: getOrder
      parameters:
        - $ref: '#/components/parameters/orgId'
        - $ref: '#/components/parameters/orderId'
      security:
        - BearerAuth: [orders.read]
      responses:
        '200':
          description: Order detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderDetail'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Orders]
      summary: Partial order update
      operationId: updateOrder
      parameters:
        - $ref: '#/components/parameters/orgId'
        - $ref: '#/components/parameters/orderId'
      security:
        - BearerAuth: [orders.write]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Updated

  /api/v2/public/orgs/{orgId}/orders/{orderId}/status:
    patch:
      tags: [Orders]
      summary: Update order status and flags
      description: Triggers `order_status_updated` webhook when configured.
      operationId: patchOrderStatus
      parameters:
        - $ref: '#/components/parameters/orgId'
        - $ref: '#/components/parameters/orderId'
      security:
        - BearerAuth: [orders.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderStatusPatch'
      responses:
        '200':
          description: Status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  order_id:
                    type: string
                  status:
                    type: string
                  updated_at:
                    type: string
                    format: date-time

  /api/v2/public/orgs/{orgId}/order-requests:
    post:
      tags: [Order requests]
      summary: Submit order request (JWT)
      description: Same JSON envelope as v1. Org comes from URL path.
      operationId: submitOrderRequestV2
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [order_requests.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderRequestEnvelope'
      responses:
        '200':
          description: Request accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderRequestResponse'

  /api/v1/public/orgs/{orgId}/order-requests:
    post:
      tags: [Order requests]
      summary: Submit order request (API key only)
      description: API key authentication. No JWT required.
      operationId: submitOrderRequestV1
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderRequestEnvelope'
      responses:
        '200':
          description: Request accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderRequestResponse'
        '401':
          description: Missing or invalid API key

  /api/v2/public/orgs/{orgId}/prescriptions:
    get:
      tags: [Prescriptions]
      summary: List prescription requests
      operationId: listPrescriptions
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: status
          in: query
          schema:
            type: string
      security:
        - BearerAuth: [prescriptions.read]
      responses:
        '200':
          description: Prescription list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrescriptionListResponse'
    post:
      tags: [Prescriptions]
      summary: Ingest prescription request
      operationId: ingestPrescription
      parameters:
        - $ref: '#/components/parameters/orgId'
      security:
        - BearerAuth: [prescriptions.write]
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrescriptionIngestRequest'
      responses:
        '200':
          description: Ingested
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrescriptionIngestResponse'

  /api/v2/public/orgs/{orgId}/prescriptions/{prescriptionId}:
    get:
      tags: [Prescriptions]
      summary: Get prescription request
      operationId: getPrescription
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: prescriptionId
          in: path
          required: true
          schema:
            type: string
      security:
        - BearerAuth: [prescriptions.read]
      responses:
        '200':
          description: Prescription detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'

  /api/v2/public/orgs/{orgId}/reports/stats:
    get:
      tags: [Reports]
      summary: Dashboard statistics
      operationId: reportStats
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: from
          in: query
          schema:
            type: string
            format: date
          description: YYYY-MM-DD
        - name: to
          in: query
          schema:
            type: string
            format: date
      security:
        - BearerAuth: [reports.read]
      responses:
        '200':
          description: Aggregated stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportStatsResponse'

  /api/v2/public/orgs/{orgId}/reports/product-performance:
    get:
      tags: [Reports]
      summary: Product performance analytics
      operationId: reportProductPerformance
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
      security:
        - BearerAuth: [reports.read]
      responses:
        '200':
          description: Performance data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportProductPerformanceResponse'

  /api/v2/public/orgs/{orgId}/reports/product-sales:
    get:
      tags: [Reports]
      summary: Product sales by order status
      operationId: reportProductSales
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: from_date
          in: query
          schema:
            type: string
            format: date
        - name: to_date
          in: query
          schema:
            type: string
            format: date
      security:
        - BearerAuth: [reports.read]
      responses:
        '200':
          description: Sales breakdown
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportProductSalesResponse'

  /api/v2/public/orgs/{orgId}/reports/pharmaone:
    get:
      tags: [Reports]
      summary: PharmaOne service fee report
      operationId: reportPharmaone
      parameters:
        - $ref: '#/components/parameters/orgId'
        - name: month
          in: query
          schema:
            type: string
          description: YYYY-MM
        - name: month_from
          in: query
          schema:
            type: string
        - name: month_to
          in: query
          schema:
            type: string
      security:
        - BearerAuth: [reports.read]
      responses:
        '200':
          description: Fee report rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportPharmaoneResponse'

  /webhooks/pharmaone:
    post:
      tags: [Webhooks]
      summary: Inbound webhook delivery (partner endpoint)
      description: |
        **Your server** receives this POST — it is not a Manager route.
        Configure the target URL in Manager → Org Settings → Integrations → Webhook subscriptions.
        Verify `X-PharmaOne-Signature` on every delivery.
      servers:
        - url: https://your-server.example
          description: Partner webhook receiver
      parameters:
        - name: X-PharmaOne-Event
          in: header
          required: true
          schema:
            type: string
        - name: X-PharmaOne-Delivery-Id
          in: header
          required: true
          schema:
            type: string
        - name: X-PharmaOne-Signature
          in: header
          required: true
          schema:
            type: string
            description: sha256= HMAC-SHA256 hex of raw body
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
      responses:
        '200':
          description: Delivery acknowledged (return any 2xx quickly)
        '401':
          description: Partner rejected invalid signature

  /api/v1/external-order:
    post:
      tags: [External Telemedicine]
      summary: Submit order via shop URL
      description: |
        **ExternalTelemedicineConnector** — call your assigned `{shop-domain}`, not Manager.
        `org_id` and `shop_id` are resolved from the shop URL; omit them from the payload.
      servers:
        - url: https://{shopDomain}
          description: Assigned shop base URL
          variables:
            shopDomain:
              default: your-shop.pharmaone.app
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExternalTelemedicineOrderRequest'
      responses:
        '200':
          description: Order request accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderRequestResponse'
        '401':
          description: Invalid API key


components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: apikey
      description: Also accepts `X-API-Key` header or `?apikey=` query parameter
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Integration JWT from `/auth/token`

  parameters:
    orgId:
      name: orgId
      in: path
      required: true
      schema:
        type: string
      example: org1
    p1Id:
      name: p1Id
      in: path
      required: true
      schema:
        type: string
      description: PharmaOne product id (numeric string)
      example: "12345"
    orderId:
      name: orderId
      in: path
      required: true
      schema:
        type: string
      example: ORD_20260605_abc123

  responses:
    BadRequest:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Org mismatch or insufficient scope
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            org_mismatch:
              value:
                error: org_mismatch
                message: Token org_id does not match URL orgId
            insufficient_scope:
              value:
                error: insufficient_scope
                message: "Missing scope: orders.read"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: Server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
        message:
          type: string

    TokenResponse:
      type: object
      required: [access_token, token_type, expires_in]
      properties:
        access_token:
          type: string
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          example: 3600

    Shop:
      type: object
      properties:
        shop_id:
          type: string
        name:
          type: string
        is_active:
          type: boolean

    ShopProduct:
      type: object
      properties:
        p1_id:
          type: string
        name:
          type: string
        slug:
          type: string
        category:
          type: string
        price:
          type: number
        stock_quantity:
          type: number
        shop_id:
          type: string

    ProductListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ShopProduct'
        pagination:
          $ref: '#/components/schemas/PaginationPage'

    ProductUpdate:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
        description:
          type: string
        category:
          type: string
        price:
          type: number
        available:
          type: boolean

    AddToInventoryRequest:
      type: object
      required: [p1_id]
      properties:
        p1_id:
          type: string
        initial_stock:
          type: integer
        price:
          type: number

    InventoryRow:
      type: object
      properties:
        p1_id:
          type: integer
        name:
          type: string
        stockquantity:
          type: integer
        price:
          type: number
        category:
          type: string
        log_entries:
          type: integer
        last_activity:
          type: string
          format: date-time

    InventoryAdjustRequest:
      type: object
      required: [p1_id, reason]
      properties:
        p1_id:
          type: string
        shop_id:
          type: string
          nullable: true
        delta_on_hand:
          type: integer
        delta_incoming:
          type: integer
        reason:
          type: string
        metadata:
          type: object
          additionalProperties: true

    InventoryLogEntry:
      type: object
      properties:
        id:
          type: string
        p1_id:
          type: integer
        shop_id:
          type: string
          nullable: true
        delta_on_hand:
          type: integer
        delta_incoming:
          type: integer
        reason:
          type: string
        created_at:
          type: string
          format: date-time

    ShopProductAttach:
      type: object
      required: [shop_id, p1_id]
      properties:
        shop_id:
          type: string
        p1_id:
          type: string
        overrides:
          type: object
          additionalProperties: true
        mode:
          type: string
          enum: [merge, replace]

    ShopProductDetach:
      type: object
      required: [shop_id, p1_id]
      properties:
        shop_id:
          type: string
        p1_id:
          type: string

    ShopProductOverrides:
      type: object
      required: [shop_id, p1_id, overrides]
      properties:
        shop_id:
          type: string
        p1_id:
          type: string
        overrides:
          type: object
          additionalProperties: true
        mode:
          type: string
          enum: [merge, replace]

    OrderListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/OrderSummary'
        pagination:
          type: object
          properties:
            limit:
              type: integer
            offset:
              type: integer
            total:
              type: integer

    OrderSummary:
      type: object
      properties:
        id:
          type: string
        external_id:
          type: string
        status:
          type: string
        shop_id:
          type: string
        customer_name:
          type: string
        total_amount:
          type: number
        payment_status:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    OrderDetail:
      allOf:
        - $ref: '#/components/schemas/OrderSummary'
        - type: object
          properties:
            customer_email:
              type: string
            payment_reference:
              type: string
            shipped:
              type: boolean
            picked_up:
              type: boolean
            cash_on_pickup:
              type: boolean
            shipping_address:
              type: object
              properties:
                street:
                  type: string
                city:
                  type: string
                postal_code:
                  type: string
                phone:
                  type: string
            items:
              type: array
              items:
                $ref: '#/components/schemas/OrderItem'

    OrderItem:
      type: object
      properties:
        p1_id:
          type: string
        name:
          type: string
        quantity:
          type: integer
        price:
          type: number

    OrderUpsert:
      type: object
      required: [shop_id, external_id]
      properties:
        shop_id:
          type: string
        external_id:
          type: string
        order_number:
          type: string
        order_datetime:
          type: string
          format: date-time
        customer_name:
          type: string
        customer_email:
          type: string
        address_street:
          type: string
        address_city:
          type: string
        address_postal_code:
          type: string
        address_phone:
          type: string
        total_amount:
          type: number
        payment_status:
          type: string
        payment_reference:
          type: string
        shipping_option:
          type: string
        shipped:
          type: boolean
        picked_up:
          type: boolean
        cash_on_pickup:
          type: boolean
        status_text:
          type: string
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderUpsertItem'

    OrderStatusPatch:
      type: object
      properties:
        status:
          type: string
        payment_status:
          type: string
        shipped:
          type: boolean
        picked_up:
          type: boolean

    OrderRequestEnvelope:
      type: object
      required: [external_reference, source, payload]
      properties:
        external_reference:
          type: string
        source:
          type: string
        payload:
          $ref: '#/components/schemas/OrderRequestPayload'

    OrderRequestPayload:
      type: object
      required: [shop_id]
      properties:
        shop_id:
          type: string
        customer_name:
          type: string
        customer_email:
          type: string
        total_amount:
          type: number
        external_id:
          type: string
        payment_status:
          type: string
        cash_on_pickup:
          type: boolean
        prescription_pdf_base64:
          type: string
        prescription_pdf_url:
          type: string
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderRequestLineItem'

    OrderRequestResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_reference:
          type: string
        message:
          type: string
        status:
          type: string

    PrescriptionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Prescription'
        pagination:
          type: object
          properties:
            limit:
              type: integer
            offset:
              type: integer

    Prescription:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
        shop_id:
          type: string
        order_id:
          type: string
        customer_email:
          type: string
        created_at:
          type: string
          format: date-time

    PaginationOffset:
      type: object
      properties:
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer

    PaginationPage:
      type: object
      properties:
        page:
          type: integer
        pageSize:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer

    InventoryListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/InventoryRow'

    InventoryAdjustResponse:
      type: object
      properties:
        status:
          type: string
          example: ok
        id:
          type: string

    OrderUpsertItem:
      type: object
      properties:
        name:
          type: string
        qty:
          type: integer
        price:
          type: number
        p1_id:
          type: string

    OrderRequestLineItem:
      type: object
      properties:
        name:
          type: string
        quantity:
          type: integer
        price:
          type: number
        p1_id:
          type: string
        product_id:
          type: string

    OrderStatusPatchResponse:
      type: object
      properties:
        order_id:
          type: string
        status:
          type: string
        updated_at:
          type: string
          format: date-time

    PrescriptionIngestResponse:
      type: object
      properties:
        id:
          type: string
        upsert:
          type: string
          example: ok

    ExternalOrderRequestPayload:
      type: object
      description: Same as OrderRequestPayload but shop_id and org_id are optional (resolved from shop URL).
      properties:
        customer_name:
          type: string
        customer_email:
          type: string
        customer_id:
          type: string
        address_street:
          type: string
        address_city:
          type: string
        address_postal_code:
          type: string
        address_phone:
          type: string
        shipping_option:
          type: string
        cash_on_pickup:
          type: boolean
        payment_status:
          type: string
        payment_reference:
          type: string
        total_amount:
          type: number
        prescription_pdf_base64:
          type: string
        prescription_pdf_url:
          type: string
          format: uri
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderRequestLineItem'

    ExternalTelemedicineOrderRequest:
      type: object
      required: [payload]
      properties:
        external_reference:
          type: string
        source:
          type: string
        payload:
          $ref: '#/components/schemas/ExternalOrderRequestPayload'

    WebhookEvent:
      type: object
      required: [id, org_id, event_type, created_at]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
        event_type:
          type: string
          description: e.g. order_status_updated, order_request_submitted, low_stock
        title:
          type: string
        description:
          type: string
        metadata:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time

    ReportPrescriptionStats:
      type: object
      properties:
        total:
          type: integer
        approved:
          type: integer
        declined:
          type: integer
        pending:
          type: integer
        completed:
          type: integer

    ReportTopCustomer:
      type: object
      properties:
        customer_id:
          type: string
        name:
          type: string
        email:
          type: string
        order_count:
          type: integer
        total_spent:
          type: number

    ReportStatsResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            prescriptions:
              $ref: '#/components/schemas/ReportPrescriptionStats'
            orders:
              type: object
              additionalProperties: true
            customers:
              type: object
              additionalProperties: true
            top_customers:
              type: array
              items:
                $ref: '#/components/schemas/ReportTopCustomer'

    ReportProductPerformanceRow:
      type: object
      properties:
        name:
          type: string
        p1_id:
          type: string
        units:
          type: integer
        revenue:
          type: number
        previous_units:
          type: integer
        previous_revenue:
          type: number
        growth_rate:
          type: number

    ReportProductPerformanceResponse:
      type: object
      properties:
        products:
          type: array
          items:
            $ref: '#/components/schemas/ReportProductPerformanceRow'
        top_by_revenue:
          $ref: '#/components/schemas/ReportProductPerformanceRow'
        top_by_growth:
          $ref: '#/components/schemas/ReportProductPerformanceRow'
        top_by_margin:
          $ref: '#/components/schemas/ReportProductPerformanceRow'

    ReportProductSalesStatusRow:
      type: object
      properties:
        volume_gr:
          type: number
        revenue:
          type: number

    ReportProductSalesRow:
      type: object
      properties:
        p1_id:
          type: integer
        product_name:
          type: string
        by_status:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/ReportProductSalesStatusRow'
        total_volume:
          type: number
        total_revenue:
          type: number

    ReportProductSalesResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ReportProductSalesRow'
        from_date:
          type: string
          format: date
        to_date:
          type: string
          format: date

    ReportPharmaoneRow:
      type: object
      properties:
        org_id:
          type: string
        org_name:
          type: string
        owner_email:
          type: string
        period:
          type: string
        total_grams:
          type: integer
        total_service_fee:
          type: number
        already_paid:
          type: number
        to_pay:
          type: number
        orders_count:
          type: integer
        shop_names:
          type: array
          items:
            type: string

    ReportPharmaoneResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ReportPharmaoneRow'
        period:
          type: string
        month:
          type: string

    PrescriptionIngestRequest:
      type: object
      description: |
        Prescription ingest body. Uses **Title Case** JSON keys (shop questionnaire format).
        Unknown keys are stored in `raw_json`. Provide `shop_id` as query param when required.
      properties:
        Order Id:
          type: string
        Timestamp:
          type: string
        Customer First Name:
          type: string
        Customer Last Name:
          type: string
        Customer Email:
          type: string
        Customer Phone:
          type: string
        Customer Birth Date:
          type: string
        Customer Address Street:
          type: string
        Customer Address City:
          type: string
        Customer Address Postal Code:
          type: string
        Customer Notes:
          type: string
        Nutzungshinweis:
          type: string
        Order Subtotal:
          type: number
        Pickup Branch:
          type: string
        Order Shipping Method:
          type: string
        Order Shipping Cost:
          type: number
        Order Prescription Fee:
          type: number
        Order Total Amount:
          type: number
        Prescription Requested:
          type: boolean
        Prescription Fee:
          type: number
        Questionnaire Selected Symptoms:
          type: string
        Questionnaire Positive Effects:
          type: string
        Questionnaire Negative Effects:
          type: string
        Questionnaire Symptom Change:
          type: string
        Questionnaire Exclusion Criteria:
          type: boolean
        Questionnaire Medication Check:
          type: boolean
        Questionnaire Addiction Signs:
          description: string or structured value
        Questionnaire Awareness Confirmed:
          type: boolean
        Questionnaire Indication:
          type: string
        Questionnaire Indication Other:
          type: string
        Questionnaire Expected Outcomes:
          type: string
        Questionnaire Expected Outcomes Other:
          type: string
        Questionnaire Prior Cannabis Therapy:
          type: string
        Questionnaire Prior Cannabis Help:
          type: string
        Questionnaire Prior Cannabis Dosage:
          type: string
        Questionnaire Prior Cannabis Strain:
          type: string
        Questionnaire Prior Cannabis Side Effects:
          type: string
        Questionnaire Prior Cannabis Side Effects Details:
          type: string
        Questionnaire Medical History Preexisting:
          type: string
        Questionnaire Medical History Preexisting Details:
          type: string
        Questionnaire Medical History Psychiatric:
          type: string
        Questionnaire Medical History Psychiatric Details:
          type: string
        Questionnaire Medical History Allergies:
          type: string
        Questionnaire Medical History Allergies Details:
          type: string
        Questionnaire Medical History Medications:
          type: string
        Questionnaire Medical History Medications Details:
          type: string
        Questionnaire Medical History Pregnancy:
          type: string
        Questionnaire Medical History Other:
          type: string
        Id Card Encoded:
          type: string
          description: Legacy ID card URL or front|back pipe-separated URLs
        Id Card Front Url:
          type: string
          format: uri
        Id Card Back Url:
          type: string
          format: uri
        Receipt Pdf Base64:
          type: string
          description: HTTPS URL to receipt PDF (despite field name)
        Prescription Fee Receipt Pdf Url:
          type: string
          format: uri
        Shop Id:
          type: string
        Product 1 Product Name:
          type: string
        Product 1 Pharma One Product Id:
          type: string
        Product 1 Quantity:
          type: string
        Product 1 Price:
          type: string
        Product 2 Product Name:
          type: string
        Product 2 Pharma One Product Id:
          type: string
        Product 3 Product Name:
          type: string
        Product 3 Pharma One Product Id:
          type: string
        Cash On Pickup:
          type: string
        Payment Method:
          type: string
      additionalProperties: true
