openapi: 3.1.0
info:
  title: FractalPack Packing API
  version: 1.0.0
  description: |
    The FractalPack Packing API provides 3D bin-packing, order management,
    shipment optimization, consolidation, wave planning, and fulfillment
    planning capabilities.

    All endpoints are served under `https://api.fractalpack.com` and require
    authentication via either an API key or a JWT bearer token.

    ## Pagination

    List endpoints use **cursor-based pagination**. Pass `limit` (page size)
    and `cursor` (opaque token from a previous response's `nextCursor`).
    The response includes an `items` array and a `nextCursor` field (null
    when there are no more results).

    ## Rate Limiting

    Certain endpoint categories are rate-limited: `pack`, `batch`, `ai`,
    and `rates`. Every rate-limited response includes these headers:

    | Header | Description |
    |---|---|
    | `X-RateLimit-Limit` | Max requests in the current window |
    | `X-RateLimit-Remaining` | Requests remaining |
    | `X-RateLimit-Reset` | Seconds until the window resets |
    | `Retry-After` | Seconds to wait (only on 429) |

    ## Errors

    All errors use [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457)
    format with a `doc_url` extension linking to the relevant error documentation.
  contact:
    name: FractalPack Engineering
    url: https://fractalpack.com
    email: support@fractalpack.com
  license:
    name: Proprietary

servers:
  - url: https://api.fractalpack.com
    description: Production

security:
  - ApiKeyAuth: []
  - BearerAuth: []

tags:
  - name: Packing
    description: Core 3D bin-packing engine
  - name: Batch
    description: Asynchronous batch packing
  - name: Items
    description: Item Master CRUD and bulk operations
  - name: Containers
    description: Container Master CRUD and bulk operations
  - name: Orders
    description: Order lifecycle management
  - name: Shipments
    description: Shipment optimization, rating, and lifecycle
  - name: Rules
    description: Packing rules engine
  - name: Consolidation
    description: Multi-order consolidation
  - name: Consolidation Profiles
    description: Consolidation profile templates
  - name: Wave Planning
    description: Warehouse wave planning and pick optimization
  - name: Fulfillment Plans
    description: Multi-modal fulfillment strategy evaluation
  - name: Integrations
    description: ERP/host system connectors and webhooks
  - name: Identity
    description: Current user and algorithm information
  - name: API Keys
    description: API key management
  - name: Users
    description: Organization user management. Requires the admin role within your organization.
  - name: Usage
    description: Usage statistics
  - name: Entitlements
    description: Organization entitlements and limits

paths:
  # ── Packing ──────────────────────────────────────────────────────────
  /api/v1/pack:
    post:
      operationId: pack
      tags: [Packing]
      summary: Pack items into containers
      description: |
        Run the 3D bin-packing algorithm to optimally place items into
        containers, pallets, and/or equipment. If no containers are specified,
        the system falls back to the organization's Container Master.

        Rules are automatically evaluated and applied before packing.

        **Rate limit category:** `pack`
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PackRequest'
            example:
              items:
                - id: "WIDGET-001"
                  length: 10
                  width: 8
                  height: 6
                  weight: 2.5
                  quantity: 3
                - id: "GADGET-002"
                  length: 12
                  width: 10
                  height: 4
                  weight: 1.8
                  quantity: 2
              containers:
                - id: "BOX-MEDIUM"
                  length: 20
                  width: 15
                  height: 12
                  maxWeight: 50
              allowMultipleBoxes: true
              algorithm: "ebAfit"
      responses:
        '200':
          description: Packing result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PackResponse'
              example:
                algorithm: "ebAfit"
                results:
                  - containerId: "BOX-MEDIUM"
                    boxIndex: 0
                    containerDimensions:
                      length: 20
                      width: 15
                      height: 12
                    packedItems:
                      - id: "WIDGET-001"
                        coordinate: { x: 0, y: 0, z: 0 }
                        packedDimensions: { length: 10, width: 8, height: 6 }
                        weight: 2.5
                        tags: []
                    unpackedItems: []
                    volumeUtilizationPercent: 68.5
                    totalWeight: 12.1
                firedRules: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ── Batch ────────────────────────────────────────────────────────────
  /api/v1/batch:
    post:
      operationId: submitBatch
      tags: [Batch]
      summary: Submit a batch of packing jobs
      description: |
        Submit up to 10,000 orders for asynchronous packing. Returns
        immediately with a batch ID and status URL.

        Optionally provide a `webhookUrl` (HTTPS only) to receive a
        callback when the batch completes.

        **Rate limit category:** `batch`
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchRequest'
            example:
              orders:
                - orderId: "ORD-001"
                  packRequest:
                    items:
                      - id: "SKU-A"
                        length: 10
                        width: 8
                        height: 6
                        weight: 2
                        quantity: 1
                    containers:
                      - id: "SM-BOX"
                        length: 12
                        width: 10
                        height: 8
                        maxWeight: 25
              webhookUrl: "https://example.com/webhook/batch-complete"
      responses:
        '202':
          description: Batch accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchSubmitResponse'
              example:
                batchId: "bat_abc123"
                status: "submitted"
                totalOrders: 1
                statusUrl: "/api/v1/batch/bat_abc123"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

    get:
      operationId: listBatches
      tags: [Batch]
      summary: List batches
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: after
          in: query
          schema:
            type: string
          description: Pagination cursor from `nextPageToken`
      responses:
        '200':
          description: Batch list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/batch/{batchId}:
    get:
      operationId: getBatch
      tags: [Batch]
      summary: Get batch status and results
      parameters:
        - name: batchId
          in: path
          required: true
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, completed, failed]
          description: Filter orders by status
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
        - name: after
          in: query
          schema:
            type: string
          description: Pagination cursor
      responses:
        '200':
          description: Batch details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Items ────────────────────────────────────────────────────────────
  /api/v1/items:
    post:
      operationId: createItem
      tags: [Items]
      summary: Create an item
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateItemRequest'
            example:
              id: "WIDGET-001"
              name: "Blue Widget"
              length: 10
              width: 8
              height: 6
              weight: 2.5
              category: "electronics"
              tags: ["fragile"]
      responses:
        '201':
          description: Item created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ItemMaster'
          headers:
            Location:
              schema:
                type: string
              description: URL of the created item
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'

    get:
      operationId: listItems
      tags: [Items]
      summary: List items
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: search
          in: query
          schema:
            type: string
          description: Full-text search across ID, name, and description
        - name: category
          in: query
          schema:
            type: string
          description: Filter by category
        - name: tags
          in: query
          schema:
            type: string
          description: Comma-separated list of tags to filter by
      responses:
        '200':
          description: Paginated item list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ItemListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/items/{itemId}:
    get:
      operationId: getItem
      tags: [Items]
      summary: Get an item by ID
      parameters:
        - name: itemId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Item details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ItemMaster'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      operationId: updateItem
      tags: [Items]
      summary: Partially update an item
      description: |
        Update one or more fields on an item. Requires the current `version`
        for optimistic concurrency control.
      parameters:
        - name: itemId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchItemRequest'
            example:
              version: 1
              weight: 3.0
              tags: ["fragile", "electronic"]
      responses:
        '200':
          description: Updated item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ItemMaster'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

    delete:
      operationId: deleteItem
      tags: [Items]
      summary: Delete an item
      description: Requires admin role.
      parameters:
        - name: itemId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Item deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/items/bulk:
    post:
      operationId: bulkImportItems
      tags: [Items]
      summary: Bulk import items
      description: |
        Import up to 1,000 items at once. Existing items (by ID) are
        updated; new items are created. Requires admin role.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkImportRequest'
      responses:
        '200':
          description: Bulk import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkImportResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/items/import/csv:
    post:
      operationId: csvImportItems
      tags: [Items]
      summary: Import items from CSV
      description: |
        Upload a CSV file to bulk import items. Max file size: 5 MB.
        Requires admin role.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
              required: [file]
      responses:
        '200':
          description: CSV import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkImportResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/items/lookup:
    get:
      operationId: lookupItemByExternalId
      tags: [Items]
      summary: Look up an item by external ID
      parameters:
        - name: type
          in: query
          required: true
          schema:
            type: string
          description: External ID type (e.g. "sku", "upc", "ean")
          example: "sku"
        - name: value
          in: query
          required: true
          schema:
            type: string
          description: External ID value
          example: "WIDGET-001"
      responses:
        '200':
          description: Item found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ItemMaster'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/items/batch:
    post:
      operationId: batchGetItems
      tags: [Items]
      summary: Get multiple items by ID
      description: Retrieve up to 100 items in a single request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchGetRequest'
            example:
              itemIds: ["WIDGET-001", "GADGET-002"]
      responses:
        '200':
          description: Items found
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ItemMaster'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ── Containers ───────────────────────────────────────────────────────
  /api/v1/containers:
    post:
      operationId: createContainer
      tags: [Containers]
      summary: Create a container
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContainerRequest'
            example:
              id: "BOX-MEDIUM"
              name: "Medium Box"
              containerType: "Box"
              innerLength: 20
              innerWidth: 15
              innerHeight: 12
              weightMaxGross: 50
      responses:
        '201':
          description: Container created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContainerMaster'
          headers:
            Location:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'

    get:
      operationId: listContainers
      tags: [Containers]
      summary: List containers
      parameters:
        - name: containerType
          in: query
          schema:
            type: string
            enum: [Box, Pallet, Equipment]
          description: Filter by container type
        - name: search
          in: query
          schema:
            type: string
          description: Search by ID or name
        - name: tags
          in: query
          schema:
            type: string
          description: Comma-separated tags
        - name: isActive
          in: query
          schema:
            type: boolean
          description: Filter by active status
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Paginated container list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContainerListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/containers/{containerId}:
    get:
      operationId: getContainer
      tags: [Containers]
      summary: Get a container by ID
      parameters:
        - name: containerId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Container details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContainerMaster'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      operationId: updateContainer
      tags: [Containers]
      summary: Partially update a container
      description: Requires the current `version` for optimistic concurrency.
      parameters:
        - name: containerId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchContainerRequest'
            example:
              version: 1
              weightMaxGross: 55
      responses:
        '200':
          description: Updated container
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContainerMaster'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

    delete:
      operationId: deleteContainer
      tags: [Containers]
      summary: Delete a container
      description: Requires admin role.
      parameters:
        - name: containerId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Container deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/containers/bulk:
    post:
      operationId: bulkImportContainers
      tags: [Containers]
      summary: Bulk import containers
      description: |
        Import up to 1,000 containers. Existing containers (by ID) are
        updated; new ones are created. Requires admin role.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkContainerImportRequest'
      responses:
        '200':
          description: Bulk import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkContainerImportResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/containers/import/csv:
    post:
      operationId: csvImportContainers
      tags: [Containers]
      summary: Import containers from CSV
      description: "Max file size: 5 MB. Requires admin role."
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
              required: [file]
      responses:
        '200':
          description: CSV import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkContainerImportResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ── Orders ───────────────────────────────────────────────────────────
  /api/v1/orders:
    post:
      operationId: createOrder
      tags: [Orders]
      summary: Create an order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
            example:
              sourceOrderId: "SO-2026-001"
              orderType: "Outbound"
              priority: "Standard"
              shipTo:
                name: "Acme Corp"
                addressLine1: "123 Main St"
                city: "Springfield"
                state: "IL"
                postalCode: "62701"
                country: "US"
              lines:
                - sku: "WIDGET-001"
                  quantityOrdered: 5
                  length: 10
                  width: 8
                  height: 6
                  weight: 2.5
      responses:
        '201':
          description: Order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      operationId: listOrders
      tags: [Orders]
      summary: List orders
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [Draft, Received, Planned, Cancelled]
          description: Filter by order status
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Paginated order list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Order'
                  nextCursor:
                    type: ["string", "null"]
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/orders/{orderId}:
    get:
      operationId: getOrder
      tags: [Orders]
      summary: Get an order by ID
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Order details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      operationId: updateOrder
      tags: [Orders]
      summary: Update an order
      description: Requires the current `version` for optimistic concurrency.
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrderRequest'
      responses:
        '200':
          description: Updated order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

    delete:
      operationId: cancelOrder
      tags: [Orders]
      summary: Cancel an order
      description: Sets the order status to Cancelled.
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Cancelled order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/orders/{orderId}/revert:
    post:
      operationId: revertOrder
      tags: [Orders]
      summary: Revert an order to its previous status
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Reverted order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/orders/import/csv:
    post:
      operationId: csvImportOrders
      tags: [Orders]
      summary: Import orders from CSV
      description: |
        Upload a CSV file to import orders. Existing orders (by sourceOrderId)
        are updated; new orders are created. Max file size: 5 MB.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
              required: [file]
      responses:
        '200':
          description: CSV import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderImportResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ── Shipments ────────────────────────────────────────────────────────
  /api/v1/shipments:
    post:
      operationId: createShipment
      tags: [Shipments]
      summary: Create a shipment from orders
      description: |
        Creates a shipment from one or more orders. Items are resolved
        from the Item Master when orders reference itemIds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateShipmentRequest'
            example:
              orderIds: ["ord_abc123"]
              mode: "Parcel"
              serviceLevel: "Standard"
      responses:
        '201':
          description: Shipment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Item resolution failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

    get:
      operationId: listShipments
      tags: [Shipments]
      summary: List shipments
      parameters:
        - name: status
          in: query
          schema:
            type: string
          description: Filter by shipment status
        - name: orderId
          in: query
          schema:
            type: string
          description: Filter by order ID
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Paginated shipment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Shipment'
                  nextCursor:
                    type: ["string", "null"]
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/shipments/{shipmentId}:
    get:
      operationId: getShipment
      tags: [Shipments]
      summary: Get a shipment by ID
      parameters:
        - name: shipmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Shipment details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      operationId: updateShipment
      tags: [Shipments]
      summary: Update a shipment
      parameters:
        - name: shipmentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateShipmentRequest'
      responses:
        '200':
          description: Updated shipment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

    delete:
      operationId: cancelShipment
      tags: [Shipments]
      summary: Cancel a shipment
      parameters:
        - name: shipmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Cancelled shipment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/shipments/{shipmentId}/optimize:
    post:
      operationId: optimizeShipment
      tags: [Shipments]
      summary: Run packing optimization on a shipment
      description: |
        Runs the 3D bin-packing algorithm on the shipment's items and
        updates the shipment with the pack result.
      parameters:
        - name: shipmentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OptimizeShipmentRequest'
            example:
              algorithm: "ebAfit"
      responses:
        '200':
          description: Optimized shipment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/shipments/{shipmentId}/rate:
    post:
      operationId: rateShipment
      tags: [Shipments]
      summary: Get shipping rates for a shipment
      description: |
        Fetches live shipping rates from connected carriers for the
        optimized shipment parcels.
      parameters:
        - name: shipmentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RateShipmentRequest'
            example:
              carriers: ["ups", "fedex"]
      responses:
        '200':
          description: Rated shipment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/shipments/{shipmentId}/ready:
    post:
      operationId: readyShipment
      tags: [Shipments]
      summary: Mark a shipment as ready
      description: Transitions the shipment to the Ready state.
      parameters:
        - name: shipmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Shipment marked as ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Shipment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Rules ────────────────────────────────────────────────────────────
  /api/v1/rules:
    get:
      operationId: listRules
      tags: [Rules]
      summary: List all packing rules
      responses:
        '200':
          description: Rules list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Rule'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      operationId: createRule
      tags: [Rules]
      summary: Create a packing rule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Rule'
            example:
              name: "Exclude small boxes for heavy items"
              ruleType: "BoxSetFilter"
              enabled: true
              priority: 10
              conditions:
                operator: "and"
                conditions:
                  - field: "totalWeight"
                    operator: "greaterThan"
                    value: 50
              action:
                type: "BoxSetFilter"
                payload:
                  exclude: ["SM-BOX"]
      responses:
        '201':
          description: Rule created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Rule'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/rules/{id}:
    get:
      operationId: getRule
      tags: [Rules]
      summary: Get a rule by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Rule details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Rule'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      operationId: updateRule
      tags: [Rules]
      summary: Update a rule
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Rule'
      responses:
        '200':
          description: Updated rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Rule'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: deleteRule
      tags: [Rules]
      summary: Delete a rule
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Rule deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Consolidation ────────────────────────────────────────────────────
  /api/v1/consolidation/evaluate:
    post:
      operationId: evaluateConsolidation
      tags: [Consolidation]
      summary: Evaluate orders for consolidation
      description: |
        Analyzes a set of orders and suggests consolidation groups based
        on destination, shipping mode, and profile constraints.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConsolidationEvaluateRequest'
            example:
              orderIds: ["ord_001", "ord_002", "ord_003"]
              profileId: "prof_default"
      responses:
        '200':
          description: Consolidation evaluation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsolidationEvaluationResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/consolidation/groups:
    post:
      operationId: createConsolidationGroup
      tags: [Consolidation]
      summary: Create a consolidation group
      description: |
        Groups orders for consolidated packing. Requires at least 2 orders.
        Set `forceOverride: true` to bypass validation warnings.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConsolidationGroupRequest'
            example:
              sourceOrderIds: ["ord_001", "ord_002"]
              profileId: "prof_default"
      responses:
        '201':
          description: Group created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsolidationGroupResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: Validation warnings (override with `forceOverride`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  rejected:
                    type: array
                    items:
                      type: object
                      properties:
                        orderIds:
                          type: array
                          items:
                            type: string
                        warnings:
                          type: array
                          items:
                            type: string

    get:
      operationId: listConsolidationGroups
      tags: [Consolidation]
      summary: List consolidation groups
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          schema:
            type: string
            enum: [Pending, Packed, Dissolved]
      responses:
        '200':
          description: Paginated group list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConsolidationGroupResponse'
                  nextCursor:
                    type: string
                    
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/consolidation/groups/{groupId}:
    get:
      operationId: getConsolidationGroup
      tags: [Consolidation]
      summary: Get a consolidation group
      parameters:
        - name: groupId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Group details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsolidationGroupResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: dissolveConsolidationGroup
      tags: [Consolidation]
      summary: Dissolve a consolidation group
      description: Returns member orders to their ungrouped state.
      parameters:
        - name: groupId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Dissolved group
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsolidationGroupResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/consolidation/groups/{groupId}/pack:
    post:
      operationId: packConsolidationGroup
      tags: [Consolidation]
      summary: Pack a consolidation group
      description: |
        Runs packing optimization on all orders in the group. Items from
        different orders can be mixed within containers unless the profile
        disallows it.
      parameters:
        - name: groupId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConsolidationPackRequest'
      responses:
        '200':
          description: Consolidation pack result
          content:
            application/json:
              schema:
                type: object
                properties:
                  packResult:
                    $ref: '#/components/schemas/PackResponse'
                  orderMapping:
                    type: object
                    additionalProperties:
                      type: array
                      items:
                        type: string
                    description: Maps container IDs to source order IDs
                  groupId:
                    type: string
                  groupStatus:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Consolidation Profiles ───────────────────────────────────────────
  /api/v1/consolidation/profiles:
    get:
      operationId: listConsolidationProfiles
      tags: [Consolidation Profiles]
      summary: List consolidation profiles
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Paginated profile list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConsolidationProfileResponse'
                  nextCursor:
                    type: string
                    
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      operationId: createConsolidationProfile
      tags: [Consolidation Profiles]
      summary: Create a consolidation profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConsolidationProfileRequest'
            example:
              name: "Same-destination consolidation"
              groupingKeys: ["shipTo.postalCode", "serviceLevel"]
              consolidationLevel: "Box"
              allowMixedOrdersInCarton: true
      responses:
        '201':
          description: Profile created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsolidationProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/consolidation/profiles/{id}:
    get:
      operationId: getConsolidationProfile
      tags: [Consolidation Profiles]
      summary: Get a consolidation profile
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Profile details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsolidationProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      operationId: updateConsolidationProfile
      tags: [Consolidation Profiles]
      summary: Update a consolidation profile
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConsolidationProfileRequest'
      responses:
        '200':
          description: Updated profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsolidationProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

    delete:
      operationId: deleteConsolidationProfile
      tags: [Consolidation Profiles]
      summary: Delete a consolidation profile
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Profile deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Wave Planning ────────────────────────────────────────────────────
  /api/v1/wave-plan:
    post:
      operationId: planWaves
      tags: [Wave Planning]
      summary: Generate an optimized wave plan
      description: |
        Analyzes orders and warehouse layout to generate optimized pick
        waves that minimize travel distance using TSP-based routing.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WavePlanRequest'
            example:
              orders:
                - orderId: "ORD-001"
                  priority: 1
                  items:
                    - sku: "WIDGET-001"
                      quantity: 3
                    - sku: "GADGET-002"
                      quantity: 1
              config:
                maxPicksPerWave: 200
                maxOrdersPerWave: 50
      responses:
        '200':
          description: Wave plan result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WavePlanResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/wave-plan/algorithms:
    get:
      operationId: getWavePlanAlgorithms
      tags: [Wave Planning]
      summary: List available wave planning algorithms
      responses:
        '200':
          description: Available algorithms
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
              example: ["nearest-neighbor-2opt", "nearest-neighbor"]
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ── Fulfillment Plans ────────────────────────────────────────────────
  /api/v1/fulfillment-plans:
    post:
      operationId: createFulfillmentPlan
      tags: [Fulfillment Plans]
      summary: Create a fulfillment plan
      description: |
        Evaluates parcel vs LTL shipping strategies for the given orders
        and returns options with cost breakdowns.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateFulfillmentPlanRequest'
            example:
              orderIds: ["ord_abc123"]
              parcelCarriers: ["ups", "fedex"]
      responses:
        '201':
          description: Fulfillment plan created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FulfillmentPlan'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Item resolution failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

    get:
      operationId: listFulfillmentPlans
      tags: [Fulfillment Plans]
      summary: List fulfillment plans
      parameters:
        - name: status
          in: query
          schema:
            type: string
          description: Filter by plan status
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Paginated plan list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/FulfillmentPlan'
                  nextCursor:
                    type: string
                    
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/fulfillment-plans/{planId}:
    get:
      operationId: getFulfillmentPlan
      tags: [Fulfillment Plans]
      summary: Get a fulfillment plan
      parameters:
        - name: planId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Plan details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FulfillmentPlan'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: cancelFulfillmentPlan
      tags: [Fulfillment Plans]
      summary: Cancel a fulfillment plan
      parameters:
        - name: planId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Cancelled plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FulfillmentPlan'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/fulfillment-plans/{planId}/select:
    post:
      operationId: selectFulfillmentOption
      tags: [Fulfillment Plans]
      summary: Select a fulfillment option
      description: |
        Select an option (e.g. "parcel" or "ltl") from a ready plan.
        This creates the corresponding shipment(s).
      parameters:
        - name: planId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelectFulfillmentOptionRequest'
            example:
              optionKey: "parcel"
      responses:
        '200':
          description: Plan with selected option and created shipments
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FulfillmentPlan'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Integrations ─────────────────────────────────────────────────────
  /api/v1/integrations:
    get:
      operationId: listIntegrations
      tags: [Integrations]
      summary: List configured integrations
      responses:
        '200':
          description: Integration list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/IntegrationConfig'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/integrations/connectors:
    get:
      operationId: listConnectors
      tags: [Integrations]
      summary: List available connector types
      responses:
        '200':
          description: Available connectors
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ConnectorInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/integrations/{connectorType}:
    get:
      operationId: getIntegration
      tags: [Integrations]
      summary: Get integration config for a connector type
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
          example: "sap-s4hana"
      responses:
        '200':
          description: Integration config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationConfig'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      operationId: createOrUpdateIntegration
      tags: [Integrations]
      summary: Create or update an integration
      description: Requires admin role.
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrUpdateIntegrationRequest'
            example:
              tenantUrl: "https://my-sap.example.com"
              enabled: true
              credentials:
                clientId: "sap-client-id"
                clientSecret: "sap-client-secret"
              settings:
                defaultAlgorithm: "ebAfit"
      responses:
        '200':
          description: Integration saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationConfig'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    delete:
      operationId: deleteIntegration
      tags: [Integrations]
      summary: Delete an integration
      description: Requires admin role.
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Integration deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/integrations/{connectorType}/test:
    post:
      operationId: testIntegrationConnection
      tags: [Integrations]
      summary: Test integration connectivity
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Connection test result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionTestResult'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/integrations/{connectorType}/sync:
    post:
      operationId: triggerSync
      tags: [Integrations]
      summary: Trigger a master data sync
      description: Pulls items and/or orders from the connected host system.
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Sync result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/integrations/{connectorType}/sync/status:
    get:
      operationId: getSyncStatus
      tags: [Integrations]
      summary: Get last sync status
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Sync status
          content:
            application/json:
              schema:
                type: object
                properties:
                  lastSyncAt:
                    type: string
                    format: date-time
                    
                  lastSyncStatus:
                    type: string
                    
                  lastSyncItemCount:
                    type: integer
                    
                  lastError:
                    type: string
                    
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/integrations/{connectorType}/mappings:
    get:
      operationId: getIntegrationMappings
      tags: [Integrations]
      summary: Get field mappings for an integration
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Field mappings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationFieldMappings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      operationId: updateIntegrationMappings
      tags: [Integrations]
      summary: Update field mappings
      description: Requires admin role.
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntegrationFieldMappings'
      responses:
        '200':
          description: Updated mappings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationFieldMappings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/integrations/{connectorType}/mappings/reset:
    post:
      operationId: resetIntegrationMappings
      tags: [Integrations]
      summary: Reset field mappings to defaults
      description: Requires admin role.
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Default mappings restored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationFieldMappings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/integrations/{connectorType}/mappings/defaults:
    get:
      operationId: getDefaultMappings
      tags: [Integrations]
      summary: Get default field mapping template
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Default mapping template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationFieldMappings'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/integrations/webhook/{connectorType}/{orgId}:
    post:
      operationId: handleWebhook
      tags: [Integrations]
      summary: Receive a webhook from a host system
      description: |
        Unauthenticated endpoint for receiving push notifications from
        connected host systems. Validated via HMAC signature in the
        `X-Webhook-Signature` header.
      security: []
      parameters:
        - name: connectorType
          in: path
          required: true
          schema:
            type: string
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: X-Webhook-Signature
          in: header
          schema:
            type: string
          description: HMAC-SHA256 signature of the request body
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Webhook processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Identity ─────────────────────────────────────────────────────────
  /api/v1/me:
    get:
      operationId: getCurrentUser
      tags: [Identity]
      summary: Get the current authenticated user
      responses:
        '200':
          description: Current user info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrentUserResponse'
              example:
                email: "user@example.com"
                orgId: "org_abc123"
                isAdmin: false
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/algorithms:
    get:
      operationId: listAlgorithms
      tags: [Identity]
      summary: List available packing algorithms
      description: Public endpoint (no authentication required).
      security: []
      responses:
        '200':
          description: Available algorithms
          content:
            application/json:
              schema:
                type: object
                properties:
                  algorithms:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        displayName:
                          type: string
                  defaultAlgorithm:
                    type: string
              example:
                algorithms:
                  - name: "ebAfit"
                    displayName: "EB-AFIT"
                  - name: "liquidFill"
                    displayName: "Liquid Fill"
                  - name: "fpFast"
                    displayName: "FP Fast"
                defaultAlgorithm: "ebAfit"

  # ── API Keys ─────────────────────────────────────────────────────────
  /api/v1/keys:
    post:
      operationId: createApiKey
      tags: [API Keys]
      summary: Create an API key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
            example:
              label: "Production integration"
              expiresInDays: 365
      responses:
        '200':
          description: |
            API key created. The full key is only returned once in the
            `apiKey` field. Store it securely.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyResponse'
              example:
                keyId: "key_abc123"
                label: "Production integration"
                apiKey: "fpk_live_abc123def456..."
                keyPrefix: "fpk_live_abc"
                isActive: true
                createdAt: "2026-04-01T00:00:00Z"
                expiresAt: "2027-04-01T00:00:00Z"
                mode: "live"
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      operationId: listApiKeys
      tags: [API Keys]
      summary: List API keys
      description: |
        Returns all API keys for the organization. The full key value
        is never returned after creation, only the prefix.
      responses:
        '200':
          description: API keys list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/keys/{keyId}:
    delete:
      operationId: revokeApiKey
      tags: [API Keys]
      summary: Revoke an API key
      parameters:
        - name: keyId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Key revoked
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ── Users ────────────────────────────────────────────────────────────
  /api/v1/users:
    get:
      operationId: listUsers
      tags: [Users]
      summary: List users
      description: Requires admin role.
      responses:
        '200':
          description: User list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      operationId: createUser
      tags: [Users]
      summary: Create a user
      description: Requires admin role.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
            example:
              email: "newuser@example.com"
              temporaryPassword: "TempPass123!"
              isAdmin: false
      responses:
        '200':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'

  /api/v1/users/{username}:
    get:
      operationId: getUser
      tags: [Users]
      summary: Get a user
      description: Requires admin role.
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      operationId: updateUser
      tags: [Users]
      summary: Update a user
      description: Requires admin role.
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
            example:
              nickname: "John"
              isAdmin: true
      responses:
        '200':
          description: Updated user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: deleteUser
      tags: [Users]
      summary: Delete a user
      description: Requires admin role.
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/users/{username}/disable:
    post:
      operationId: disableUser
      tags: [Users]
      summary: Disable a user
      description: Requires admin role.
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User disabled
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/users/{username}/enable:
    post:
      operationId: enableUser
      tags: [Users]
      summary: Enable a user
      description: Requires admin role.
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User enabled
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/users/{username}/reset-password:
    post:
      operationId: resetPassword
      tags: [Users]
      summary: Reset a user's password
      description: Sends a password reset email. Requires admin role.
      parameters:
        - name: username
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Password reset initiated
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Usage ────────────────────────────────────────────────────────────
  /api/v1/usage:
    get:
      operationId: getUsage
      tags: [Usage]
      summary: Get usage statistics
      parameters:
        - name: days
          in: query
          schema:
            type: integer
            default: 30
          description: Number of days to include
      responses:
        '200':
          description: Usage statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
              example:
                totalRequests: 12450
                totalItemsPacked: 89300
                totalContainersUsed: 4200
                dailyBreakdown:
                  - date: "2026-04-02"
                    requests: 450
                    itemsPacked: 3200
                    containersUsed: 150
                    byCategory:
                      pack: 400
                      batch: 50
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ── Entitlements ─────────────────────────────────────────────────────
  /api/v1/entitlements:
    get:
      operationId: getEntitlements
      tags: [Entitlements]
      summary: Get organization entitlements
      responses:
        '200':
          description: Organization entitlements
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntitlementsResponse'
              example:
                enabledProducts: ["packing", "shipping", "consolidation"]
                maxUsers: 25
                rateLimits:
                  pack: 100
                  batch: 10
                monthlyRequestQuota: 100000
        '401':
          $ref: '#/components/responses/Unauthorized'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: |
        API key authentication. Keys are prefixed with `fpk_test_` (testing)
        or `fpk_live_` (production).
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token from AWS Cognito.

  parameters:
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 20
        minimum: 1
        maximum: 100
      description: Maximum number of items to return
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
      description: Opaque pagination cursor from a previous response's `nextCursor`

  responses:
    BadRequest:
      description: Bad request
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: "https://developers.fractalpack.com/errors/bad_request"
            title: "Bad Request"
            status: 400
            detail: "At least one item is required."
            instance: "0HN9A1B2C3D4:00000001"
            doc_url: "https://developers.fractalpack.com/errors/bad_request"
    Unauthorized:
      description: Authentication required
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: "https://developers.fractalpack.com/errors/unauthorized"
            title: "Unauthorized"
            status: 401
            detail: "Invalid or missing authentication credentials."
            instance: "0HN9A1B2C3D4:00000002"
            doc_url: "https://developers.fractalpack.com/errors/unauthorized"
    Forbidden:
      description: Insufficient permissions
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: "https://developers.fractalpack.com/errors/forbidden"
            title: "Forbidden"
            status: 403
            detail: "Admin role required."
            instance: "0HN9A1B2C3D4:00000003"
            doc_url: "https://developers.fractalpack.com/errors/forbidden"
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: "https://developers.fractalpack.com/errors/not_found"
            title: "Not Found"
            status: 404
            detail: "Item 'WIDGET-999' not found."
            instance: "0HN9A1B2C3D4:00000004"
            doc_url: "https://developers.fractalpack.com/errors/not_found"
    Conflict:
      description: Resource conflict (duplicate or version mismatch)
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: "https://developers.fractalpack.com/errors/conflict"
            title: "Conflict"
            status: 409
            detail: "Item 'WIDGET-001' already exists."
            instance: "0HN9A1B2C3D4:00000005"
            doc_url: "https://developers.fractalpack.com/errors/conflict"
    TooManyRequests:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: "https://tools.ietf.org/html/rfc6585#section-4"
            title: "Too Many Requests"
            status: 429
            detail: "Rate limit exceeded for this organization. Try again in 60 seconds."

  schemas:
    # ── Error ────────────────────────────────────────────────────────
    ProblemDetails:
      type: object
      description: RFC 9457 Problem Details
      properties:
        type:
          type: string
          format: uri
          description: Error type URI
        title:
          type: string
          description: Human-readable error title
        status:
          type: integer
          description: HTTP status code
        detail:
          type: string
          description: Human-readable explanation
        instance:
          type: string
          description: Request trace identifier
        doc_url:
          type: string
          format: uri
          description: Link to error documentation
        field:
          type: string
          description: Field that caused the error (for validation errors)

    # ── Packing ──────────────────────────────────────────────────────
    PackRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ItemDto'
          minItems: 1
          maxItems: 10000
          description: Items to pack
        containers:
          type: array
          items:
            $ref: '#/components/schemas/ContainerDto'
          maxItems: 1000
          description: Fixed-size containers. If omitted, falls back to Container Master.
        dynamicContainers:
          type: array
          items:
            $ref: '#/components/schemas/DynamicContainerDto'
          maxItems: 1000
          description: Variable-size containers
        pallets:
          type: array
          items:
            $ref: '#/components/schemas/PalletDto'
          maxItems: 100
          description: Pallet definitions
        equipment:
          type: array
          items:
            $ref: '#/components/schemas/EquipmentDto'
          maxItems: 100
          description: Equipment (trailers, etc.)
        allowMultipleBoxes:
          type: boolean
          default: false
          description: Allow items to be packed into multiple boxes
        loadingMode:
          type: string
          default: "palletized"
          description: 'Loading mode: "standard", "palletized"'
        algorithm:
          type: string
          description: 'Packing algorithm: "ebAfit", "liquidFill", "fpFast"'
          enum: ["ebAfit", "liquidFill", "fpFast"]
        useCasePacks:
          type: boolean
          default: false
          description: Use case pack configurations from items
        allowPalletStacking:
          type: boolean
          default: false
          description: Allow stacking pallets in trailers
        respectTiHi:
          type: boolean
          default: true
          description: Use vendor-prescribed Ti/Hi pallet patterns
        separationGroups:
          type: array
          items:
            type: array
            items:
              type: string
          description: |
            Groups of item tags that cannot share a container.
            Items from different groups are packed separately.
        affinityGroups:
          type: array
          items:
            $ref: '#/components/schemas/AffinityGroupDto'
          description: Pin specific items to specific containers
        optimizeFor:
          type: string
          enum: ["volume", "cost"]
          description: Optimization objective (default "volume")
        originZip:
          type: string
          maxLength: 10
          description: Origin ZIP code (required for cost optimization)
        destinationZip:
          type: string
          maxLength: 10
          description: Destination ZIP code (required for cost optimization)

    ItemDto:
      type: object
      properties:
        id:
          type: string
          maxLength: 128
        length:
          type: number
          format: double
          description: Length in inches
        width:
          type: number
          format: double
          description: Width in inches
        height:
          type: number
          format: double
          description: Height in inches
        weight:
          type: number
          format: double
          minimum: 0
          maximum: 500000
          description: Weight in lbs
        quantity:
          type: integer
          minimum: 1
          maximum: 100000
          default: 1
        voidSpace:
          $ref: '#/components/schemas/VoidSpaceDto'
        unitOfMeasure:
          type: string
          default: "each"
          description: '"each" for standard items, "linear" for rollable items'
        linearQuantity:
          type: number
          format: double
          description: Linear quantity for rollable items
        rollProperties:
          $ref: '#/components/schemas/RollPropertiesDto'
        shipAlone:
          type: boolean
          default: false
        tags:
          type: array
          items:
            type: string
        sourceOrderId:
          type: string
          maxLength: 128
        sourceLineNumber:
          type: integer
        casePackQuantity:
          type: integer
          minimum: 1
        caseLength:
          type: number
          format: double
        caseWidth:
          type: number
          format: double
        caseHeight:
          type: number
          format: double
        caseWeight:
          type: number
          format: double
        tiHiTi:
          type: integer
          description: Cases per pallet layer (Ti)
        tiHiHi:
          type: integer
          description: Layers per pallet (Hi)

    ContainerDto:
      type: object
      properties:
        id:
          type: string
          maxLength: 128
        length:
          type: number
          format: double
          description: Internal length in inches
        width:
          type: number
          format: double
        height:
          type: number
          format: double
        maxWeight:
          type: number
          format: double
          maximum: 200000
        cost:
          type: number
          format: double
          description: Box cost for cost optimization
        carrierService:
          type: string
          description: Carrier service code (e.g. "ups_ground")
        flatRateCost:
          type: number
          format: double
          description: Flat-rate shipping cost

    DynamicContainerDto:
      type: object
      properties:
        id:
          type: string
          maxLength: 128
        minLength:
          type: number
          format: double
        minWidth:
          type: number
          format: double
        minHeight:
          type: number
          format: double
        maxLength:
          type: number
          format: double
        maxWidth:
          type: number
          format: double
        maxHeight:
          type: number
          format: double
        maxWeight:
          type: number
          format: double
          maximum: 200000

    PalletDto:
      type: object
      properties:
        id:
          type: string
          maxLength: 128
        length:
          type: number
          format: double
        width:
          type: number
          format: double
        maxHeight:
          type: number
          format: double
        palletWeight:
          type: number
          format: double
          description: Weight of the empty pallet
        maxWeight:
          type: number
          format: double
          maximum: 200000
        platformHeight:
          type: number
          format: double
        maxStackWeight:
          type: number
          format: double
          maximum: 200000

    EquipmentDto:
      type: object
      properties:
        id:
          type: string
          maxLength: 128
        length:
          type: number
          format: double
        width:
          type: number
          format: double
        height:
          type: number
          format: double
        tareWeight:
          type: number
          format: double
        maxWeight:
          type: number
          format: double
          maximum: 200000
        equipmentType:
          type: string
          default: "DryVan"

    AffinityGroupDto:
      type: object
      properties:
        itemPatterns:
          type: array
          items:
            type: string
          description: Glob patterns matching item IDs or tags
        containerIds:
          type: array
          items:
            type: string
          description: Container IDs these items must be packed into
        exclusive:
          type: boolean
          default: false
          description: If true, only matched items go in these containers

    VoidSpaceDto:
      type: object
      properties:
        shape:
          type: string
          enum: ["rectangular", "cylindrical", "spherical"]
        length:
          type: number
          format: double
        width:
          type: number
          format: double
        height:
          type: number
          format: double

    RollPropertiesDto:
      type: object
      properties:
        materialThickness:
          type: number
          format: double
        materialWidth:
          type: number
          format: double
        coreInnerDiameter:
          type: number
          format: double
        coreWallThickness:
          type: number
          format: double
        maxRollOuterDiameter:
          type: number
          format: double
        maxLinearQuantityPerRoll:
          type: number
          format: double
        weightPerUnitLength:
          type: number
          format: double

    PackResponse:
      type: object
      properties:
        algorithm:
          type: string
        results:
          type: array
          items:
            $ref: '#/components/schemas/ContainerPackResult'
        palletResults:
          $ref: '#/components/schemas/PalletPackResponse'
        trailerResults:
          $ref: '#/components/schemas/TrailerPackResponse'
        firedRules:
          type: array
          items:
            $ref: '#/components/schemas/FiredRuleDto'

    ContainerPackResult:
      type: object
      properties:
        containerId:
          type: string
        boxIndex:
          type: integer
        containerDimensions:
          $ref: '#/components/schemas/Dimensions'
        packedItems:
          type: array
          items:
            $ref: '#/components/schemas/PackedItemDto'
        unpackedItems:
          type: array
          items:
            $ref: '#/components/schemas/UnpackedItemDto'
        volumeUtilizationPercent:
          type: number
          format: double
        totalWeight:
          type: number
          format: double
        sourceOrderIds:
          type: array
          items:
            type: string
        costBreakdown:
          $ref: '#/components/schemas/CostBreakdownDto'

    PackedItemDto:
      type: object
      properties:
        id:
          type: string
        coordinate:
          $ref: '#/components/schemas/Coordinate'
        packedDimensions:
          $ref: '#/components/schemas/Dimensions'
        weight:
          type: number
          format: double
        tags:
          type: array
          items:
            type: string
        nestedItems:
          type: array
          items:
            $ref: '#/components/schemas/PackedItemDto'
        rollInfo:
          $ref: '#/components/schemas/RollResultDto'
        sourceOrderId:
          type: string
        sourceLineNumber:
          type: integer

    UnpackedItemDto:
      type: object
      properties:
        id:
          type: string
        dimensions:
          $ref: '#/components/schemas/Dimensions'
        weight:
          type: number
          format: double
        rollInfo:
          $ref: '#/components/schemas/RollResultDto'
        sourceOrderId:
          type: string
        sourceLineNumber:
          type: integer

    RollResultDto:
      type: object
      properties:
        sourceItemId:
          type: string
        rollIndex:
          type: integer
        totalRolls:
          type: integer
        linearQuantityOnRoll:
          type: number
          format: double
        outerDiameter:
          type: number
          format: double

    CostBreakdownDto:
      type: object
      properties:
        shippingCost:
          type: number
          format: decimal
        boxCost:
          type: number
          format: decimal
        totalCost:
          type: number
          format: decimal
        billableWeight:
          type: number
          format: double
        dimWeight:
          type: number
          format: double
        actualWeight:
          type: number
          format: double
        carrier:
          type: string
        service:
          type: string
        zone:
          type: integer

    PalletPackResponse:
      type: object
      properties:
        pallets:
          type: array
          items:
            $ref: '#/components/schemas/PalletResult'
        unpalletizedCartons:
          type: array
          items:
            type: object
            properties:
              cartonId:
                type: string
              dimensions:
                $ref: '#/components/schemas/Dimensions'
              weight:
                type: number
                format: double

    PalletResult:
      type: object
      properties:
        palletId:
          type: string
        palletIndex:
          type: integer
        palletDimensions:
          $ref: '#/components/schemas/Dimensions'
        cartons:
          type: array
          items:
            type: object
            properties:
              cartonId:
                type: string
              coordinate:
                $ref: '#/components/schemas/Coordinate'
              packedDimensions:
                $ref: '#/components/schemas/Dimensions'
              weight:
                type: number
                format: double
        volumeUtilizationPercent:
          type: number
          format: double
        totalWeight:
          type: number
          format: double
        palletWeight:
          type: number
          format: double
        platformHeight:
          type: number
          format: double

    TrailerPackResponse:
      type: object
      properties:
        trailers:
          type: array
          items:
            $ref: '#/components/schemas/TrailerResult'
        unloadedPallets:
          type: array
          items:
            type: object
            properties:
              palletId:
                type: string
              dimensions:
                $ref: '#/components/schemas/Dimensions'
              weight:
                type: number
                format: double
        unloadedCartons:
          type: array
          items:
            type: object
            properties:
              cartonId:
                type: string
              dimensions:
                $ref: '#/components/schemas/Dimensions'
              weight:
                type: number
                format: double

    TrailerResult:
      type: object
      properties:
        trailerId:
          type: string
        trailerIndex:
          type: integer
        equipmentType:
          type: string
        trailerDimensions:
          $ref: '#/components/schemas/Dimensions'
        pallets:
          type: array
          items:
            type: object
            properties:
              palletId:
                type: string
              coordinate:
                $ref: '#/components/schemas/Coordinate'
              palletDimensions:
                $ref: '#/components/schemas/Dimensions'
              weight:
                type: number
                format: double
              stackInfo:
                type: object
                properties:
                  bottomPalletId:
                    type: string
                  topPalletId:
                    type: string
                  bottomHeight:
                    type: number
                    format: double
                  topHeight:
                    type: number
                    format: double
        floorLoadedCartons:
          type: array
          items:
            type: object
            properties:
              cartonId:
                type: string
              coordinate:
                $ref: '#/components/schemas/Coordinate'
              packedDimensions:
                $ref: '#/components/schemas/Dimensions'
              weight:
                type: number
                format: double
        volumeUtilizationPercent:
          type: number
          format: double
        totalWeight:
          type: number
          format: double
        tareWeight:
          type: number
          format: double

    FiredRuleDto:
      type: object
      properties:
        ruleId:
          type: string
        ruleName:
          type: string
        ruleType:
          type: string
        summary:
          type: string

    Coordinate:
      type: object
      properties:
        x:
          type: number
          format: double
        y:
          type: number
          format: double
        z:
          type: number
          format: double

    Dimensions:
      type: object
      properties:
        length:
          type: number
          format: double
        width:
          type: number
          format: double
        height:
          type: number
          format: double

    # ── Batch ────────────────────────────────────────────────────────
    BatchRequest:
      type: object
      required: [orders]
      properties:
        orders:
          type: array
          items:
            $ref: '#/components/schemas/BatchOrderRequest'
          minItems: 1
          maxItems: 10000
        webhookUrl:
          type: string
          format: uri
          maxLength: 2048
          description: HTTPS URL to receive completion callback

    BatchOrderRequest:
      type: object
      required: [orderId, packRequest]
      properties:
        orderId:
          type: string
          minLength: 1
          maxLength: 256
        packRequest:
          $ref: '#/components/schemas/PackRequest'

    BatchSubmitResponse:
      type: object
      properties:
        batchId:
          type: string
        status:
          type: string
        totalOrders:
          type: integer
        statusUrl:
          type: string

    BatchStatusResponse:
      type: object
      properties:
        batchId:
          type: string
        status:
          type: string
          enum: [submitted, processing, completed, failed]
        totalOrders:
          type: integer
        completedOrders:
          type: integer
        failedOrders:
          type: integer
        pendingOrders:
          type: integer
        createdAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
          
        orders:
          type: array
          items:
            $ref: '#/components/schemas/BatchOrderResponse'
        nextPageToken:
          type: string
          
        hasMore:
          type: boolean

    BatchOrderResponse:
      type: object
      properties:
        orderId:
          type: string
        status:
          type: string
          enum: [pending, completed, failed]
        result:
          $ref: '#/components/schemas/PackResponse'
        error:
          type: string

    BatchListResponse:
      type: object
      properties:
        batches:
          type: array
          items:
            $ref: '#/components/schemas/BatchSummary'
        nextPageToken:
          type: string
          
        hasMore:
          type: boolean

    BatchSummary:
      type: object
      properties:
        batchId:
          type: string
        status:
          type: string
        totalOrders:
          type: integer
        completedOrders:
          type: integer
        failedOrders:
          type: integer
        createdAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
          

    # ── Items ────────────────────────────────────────────────────────
    CreateItemRequest:
      type: object
      required: [id, length, width, height, weight]
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 128
        name:
          type: string
          maxLength: 256
        description:
          type: string
          maxLength: 2000
        externalIds:
          type: object
          properties:
            gtin:
              type: string
            upc:
              type: string
            ean:
              type: string
            vendorPartNumber:
              type: string
            customerPartNumber:
              type: string
          additionalProperties:
            type: string
        category:
          type: string
          maxLength: 64
        commodityCode:
          type: string
          maxLength: 64
        hsCode:
          type: string
          maxLength: 64
        countryOfOrigin:
          type: string
          maxLength: 64
        tags:
          type: array
          items:
            type: string
        customAttributes:
          type: object
          additionalProperties: true
        length:
          type: number
          format: double
          description: Length in inches
        width:
          type: number
          format: double
        height:
          type: number
          format: double
        dimensionUnit:
          type: string
          maxLength: 64
        weight:
          type: number
          format: double
          description: Weight in lbs
        netWeight:
          type: number
          format: double
        weightUnit:
          type: string
          maxLength: 64
        volume:
          type: number
          format: double
        volumeUnit:
          type: string
          maxLength: 64
        unitOfMeasure:
          type: string
          maxLength: 64
        rollProperties:
          $ref: '#/components/schemas/RollPropertiesDto'
        voidSpace:
          $ref: '#/components/schemas/VoidSpaceDto'
        shape:
          type: string
          enum: [rectangular, cylindrical, spherical, irregular, lShape, tShape, custom]
        orientationConstraints:
          type: object
          properties:
            allowedRotations:
              type: array
              items:
                type: string
              description: List of allowed rotation codes
            keepUpright:
              type: boolean
              description: Item must remain upright (this-side-up)
            arrowsUp:
              type: boolean
              description: Arrows-up constraint
            floorContactFaces:
              type: array
              items:
                type: string
              description: Faces that may contact the floor
        maxWeightOnTop:
          type: number
          format: double
        maxStackHeight:
          type: integer
        crushClass:
          type: string
          enum: [none, light, moderate, fragile, extreme]
        shipAlone:
          type: boolean
        maxPerContainer:
          type: integer
        packagingLevels:
          type: array
          items:
            type: object
            properties:
              level:
                type: string
              quantity:
                type: integer
              length:
                type: number
                format: double
              width:
                type: number
                format: double
              height:
                type: number
                format: double
              weight:
                type: number
                format: double
              tiHiTi:
                type: integer
              tiHiHi:
                type: integer
        casePackQuantity:
          type: integer
        caseLength:
          type: number
          format: double
        caseWidth:
          type: number
          format: double
        caseHeight:
          type: number
          format: double
        caseWeight:
          type: number
          format: double
        hazmat:
          type: object
          properties:
            unNumber:
              type: string
            hazardClass:
              type: string
            packingGroup:
              type: string
            properShippingName:
              type: string
        freightClass:
          type: string
          maxLength: 64
        imageUrl:
          type: string
          maxLength: 2048
          format: uri

    PatchItemRequest:
      type: object
      required: [version]
      properties:
        version:
          type: integer
          minimum: 1
          description: Current version for optimistic concurrency
        name:
          type: string
          maxLength: 256
        description:
          type: string
          maxLength: 2000
        category:
          type: string
          maxLength: 64
        tags:
          type: array
          items:
            type: string
        length:
          type: number
          format: double
        width:
          type: number
          format: double
        height:
          type: number
          format: double
        weight:
          type: number
          format: double
        shipAlone:
          type: boolean
        packagingLevels:
          type: array
          items:
            type: object

    ItemMaster:
      type: object
      description: Full item record from the Item Master
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        externalIds:
          type: object
          additionalProperties:
            type: string
        category:
          type: string
        tags:
          type: array
          items:
            type: string
        length:
          type: number
          format: double
        width:
          type: number
          format: double
        height:
          type: number
          format: double
        weight:
          type: number
          format: double
        unitOfMeasure:
          type: string
        shape:
          type: string
        shipAlone:
          type: boolean
        packagingLevels:
          type: array
          items:
            type: object
        version:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdBy:
          type: string

    ItemListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ItemMaster'
        nextCursor:
          type: string
          

    BulkImportRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CreateItemRequest'
          minItems: 1
          maxItems: 1000

    BulkImportResult:
      type: object
      properties:
        created:
          type: integer
        updated:
          type: integer
        errors:
          type: array
          items:
            type: object
            properties:
              index:
                type: integer
              itemId:
                type: string
              error:
                type: string

    BatchGetRequest:
      type: object
      required: [itemIds]
      properties:
        itemIds:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 100

    # ── Containers ───────────────────────────────────────────────────
    CreateContainerRequest:
      type: object
      required: [id, name, containerType, innerLength, innerWidth, innerHeight, weightMaxGross]
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 128
        name:
          type: string
          maxLength: 256
        containerType:
          type: string
          enum: [Box, Pallet, Equipment]
        description:
          type: string
          maxLength: 2000
        subType:
          type: string
          maxLength: 64
        tags:
          type: array
          items:
            type: string
        customAttributes:
          type: object
          additionalProperties: true
        innerLength:
          type: number
          format: double
        innerWidth:
          type: number
          format: double
        innerHeight:
          type: number
          format: double
        outerLength:
          type: number
          format: double
        outerWidth:
          type: number
          format: double
        outerHeight:
          type: number
          format: double
        dimensionUnit:
          type: string
          maxLength: 64
        weightTare:
          type: number
          format: double
        weightMaxGross:
          type: number
          format: double
          maximum: 200000
        weightUnit:
          type: string
          maxLength: 64
        isDynamic:
          type: boolean
        minLength:
          type: number
          format: double
        minWidth:
          type: number
          format: double
        minHeight:
          type: number
          format: double
        stackable:
          type: boolean
        maxStackWeight:
          type: number
          format: double
        material:
          type: string
          maxLength: 64
        wallThickness:
          type: number
          format: double
        baseCost:
          type: number
          format: double
        isActive:
          type: boolean
          default: true
        palletProperties:
          type: object
          properties:
            platformHeight:
              type: number
              format: double
            deckType:
              type: string
        equipmentProperties:
          type: object
          properties:
            equipmentType:
              type: string

    PatchContainerRequest:
      type: object
      required: [version]
      properties:
        version:
          type: integer
          minimum: 1
        name:
          type: string
          maxLength: 256
        description:
          type: string
          maxLength: 2000
        innerLength:
          type: number
          format: double
        innerWidth:
          type: number
          format: double
        innerHeight:
          type: number
          format: double
        weightMaxGross:
          type: number
          format: double
        isActive:
          type: boolean

    ContainerMaster:
      type: object
      description: Full container record from the Container Master
      properties:
        id:
          type: string
        name:
          type: string
        containerType:
          type: string
          enum: [Box, Pallet, Equipment]
        innerLength:
          type: number
          format: double
        innerWidth:
          type: number
          format: double
        innerHeight:
          type: number
          format: double
        weightMaxGross:
          type: number
          format: double
        weightTare:
          type: number
          format: double
        isDynamic:
          type: boolean
        isActive:
          type: boolean
        tags:
          type: array
          items:
            type: string
        version:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ContainerListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ContainerMaster'
        nextCursor:
          type: string
          

    BulkContainerImportRequest:
      type: object
      required: [containers]
      properties:
        containers:
          type: array
          items:
            $ref: '#/components/schemas/CreateContainerRequest'
          minItems: 1
          maxItems: 1000

    BulkContainerImportResult:
      type: object
      properties:
        created:
          type: integer
        updated:
          type: integer
        errors:
          type: array
          items:
            type: object
            properties:
              index:
                type: integer
              containerId:
                type: string
              error:
                type: string

    # ── Orders ───────────────────────────────────────────────────────
    CreateOrderRequest:
      type: object
      required: [lines]
      properties:
        sourceSystem:
          type: string
          maxLength: 128
        sourceOrderId:
          type: string
          maxLength: 128
        orderType:
          type: string
          enum: [Outbound, Inbound, Transfer, Return]
          default: Outbound
        priority:
          type: string
          enum: [Standard, High, Urgent, Critical]
          default: Standard
        serviceLevel:
          type: string
          enum: [Economy, Standard, Expedited, NextDay, SameDay]
          default: Standard
        channel:
          type: string
          maxLength: 128
        shipFrom:
          $ref: '#/components/schemas/AddressParty'
        shipTo:
          $ref: '#/components/schemas/AddressParty'
        customer:
          type: object
          properties:
            name:
              type: string
            email:
              type: string
            phone:
              type: string
        orderedAt:
          type: string
          format: date-time
        requiredShipBy:
          type: string
          format: date-time
        requiredDeliverBy:
          type: string
          format: date-time
        references:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
              value:
                type: string
        tags:
          type: array
          items:
            type: string
        customAttributes:
          type: object
          additionalProperties: true
        externalShipmentId:
          type: string
          maxLength: 128
          description: Host system consolidation signal
        submit:
          type: boolean
          default: true
          description: If true, order created in RECEIVED status; if false, DRAFT
        lines:
          type: array
          items:
            $ref: '#/components/schemas/CreateOrderLineRequest'
          minItems: 1
          maxItems: 200

    CreateOrderLineRequest:
      type: object
      properties:
        lineNumber:
          type: integer
        itemId:
          type: string
          maxLength: 128
          description: Reference to Item Master
        sku:
          type: string
          maxLength: 128
        description:
          type: string
          maxLength: 2000
        quantityOrdered:
          type: number
        unitOfMeasure:
          type: string
          default: "EA"
        length:
          type: number
          format: double
          description: Required when itemId is not provided
        width:
          type: number
          format: double
        height:
          type: number
          format: double
        weight:
          type: number
          format: double
        tags:
          type: array
          items:
            type: string
        specialInstructions:
          type: string
          maxLength: 5000
        lotNumber:
          type: string
          maxLength: 128
        serialNumbers:
          type: array
          items:
            type: string

    UpdateOrderRequest:
      type: object
      required: [version]
      properties:
        version:
          type: integer
        priority:
          type: string
          enum: [Standard, High, Urgent, Critical]
        serviceLevel:
          type: string
          enum: [Economy, Standard, Expedited, NextDay, SameDay]
        channel:
          type: string
        shipFrom:
          $ref: '#/components/schemas/AddressParty'
        shipTo:
          $ref: '#/components/schemas/AddressParty'
        requiredShipBy:
          type: string
          format: date-time
        requiredDeliverBy:
          type: string
          format: date-time
        tags:
          type: array
          items:
            type: string
        lines:
          type: array
          items:
            $ref: '#/components/schemas/CreateOrderLineRequest'

    Order:
      type: object
      description: Order entity
      properties:
        id:
          type: string
        sourceSystem:
          type: string
        sourceOrderId:
          type: string
        orderType:
          type: string
        priority:
          type: string
        serviceLevel:
          type: string
        status:
          type: string
          enum: [Draft, Received, Planned, Cancelled]
        channel:
          type: string
        shipFrom:
          $ref: '#/components/schemas/AddressParty'
        shipTo:
          $ref: '#/components/schemas/AddressParty'
        lines:
          type: array
          items:
            type: object
        tags:
          type: array
          items:
            type: string
        version:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    OrderImportResult:
      type: object
      properties:
        ordersCreated:
          type: integer
        ordersUpdated:
          type: integer
        ordersUnchanged:
          type: integer
        totalLines:
          type: integer
        errors:
          type: array
          items:
            type: object
            properties:
              orderId:
                type: string
              error:
                type: string

    AddressParty:
      type: object
      properties:
        name:
          type: string
        company:
          type: string
        addressLine1:
          type: string
        addressLine2:
          type: string
        city:
          type: string
        state:
          type: string
        postalCode:
          type: string
        country:
          type: string
        phone:
          type: string
        email:
          type: string

    # ── Shipments ────────────────────────────────────────────────────
    CreateShipmentRequest:
      type: object
      required: [orderIds]
      properties:
        orderIds:
          type: array
          items:
            type: string
          minItems: 1
        packingConfig:
          type: object
          description: Optional packing configuration overrides
        mode:
          type: string
          enum: [Parcel, Ltl, Ftl, Air, Ocean, Intermodal]
        serviceLevel:
          type: string
          enum: [Economy, Standard, Expedited, NextDay, SameDay]
        carrier:
          type: string
          maxLength: 128

    UpdateShipmentRequest:
      type: object
      required: [version]
      properties:
        version:
          type: integer
        packingConfig:
          type: object
        mode:
          type: string
          enum: [Parcel, Ltl, Ftl, Air, Ocean, Intermodal]
        serviceLevel:
          type: string
          enum: [Economy, Standard, Expedited, NextDay, SameDay]
        carrier:
          type: string
          maxLength: 128

    OptimizeShipmentRequest:
      type: object
      properties:
        algorithm:
          type: string
          maxLength: 64
          description: Packing algorithm to use

    RateShipmentRequest:
      type: object
      properties:
        carriers:
          type: array
          items:
            type: string
          description: Carrier IDs to rate (defaults to all connected)

    Shipment:
      type: object
      description: Shipment entity
      properties:
        id:
          type: string
        orderIds:
          type: array
          items:
            type: string
        status:
          type: string
        mode:
          type: string
        serviceLevel:
          type: string
        carrier:
          type: string
        packResult:
          $ref: '#/components/schemas/PackResponse'
        version:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    # ── Rules ────────────────────────────────────────────────────────
    Rule:
      type: object
      properties:
        id:
          type: string
          maxLength: 128
        name:
          type: string
          maxLength: 256
        description:
          type: string
          maxLength: 2000
        enabled:
          type: boolean
          default: true
        priority:
          type: integer
          description: Lower numbers execute first
        ruleType:
          type: string
          enum:
            - BoxSetFilter
            - CarrierPackagingRule
            - DestinationOverride
            - OrderPreprocessing
            - IncompatibilityGroup
            - StackingConstraint
            - OrientationRestriction
            - ItemGrouping
            - PostSolveValidation
            - ObjectiveWeightModifier
            - ReSolveTrigger
            - ConsolidationProfile
        executionTarget:
          type: string
          enum: [PreSolve, SolverConfig, PostSolve]
          default: PreSolve
        conditions:
          type: object
          description: Condition group with operator and nested conditions
          properties:
            operator:
              type: string
              enum: [and, or]
            conditions:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  operator:
                    type: string
                  value: {}
        action:
          type: object
          description: Rule action with type and payload
          properties:
            type:
              type: string
            payload: {}
        version:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdBy:
          type: string

    # ── Consolidation ────────────────────────────────────────────────
    ConsolidationEvaluateRequest:
      type: object
      required: [orderIds]
      properties:
        orderIds:
          type: array
          items:
            type: string
          minItems: 1
        profileId:
          type: string

    ConsolidationEvaluationResult:
      type: object
      properties:
        suggestedGroups:
          type: array
          items:
            type: object
            properties:
              orderIds:
                type: array
                items:
                  type: string
              profileId:
                type: string
              groupingKeyValues:
                type: object
                additionalProperties:
                  type: string
        ungrouped:
          type: array
          items:
            type: object
            properties:
              orderId:
                type: string
              reason:
                type: string

    CreateConsolidationGroupRequest:
      type: object
      required: [sourceOrderIds]
      properties:
        profileId:
          type: string
          maxLength: 128
        groupingKeyValues:
          type: object
          additionalProperties:
            type: string
        sourceOrderIds:
          type: array
          items:
            type: string
          minItems: 2
        forceOverride:
          type: boolean
          default: false
          description: Override validation warnings

    ConsolidationGroupResponse:
      type: object
      properties:
        id:
          type: string
        sourceOrderIds:
          type: array
          items:
            type: string
        profileId:
          type: string
        status:
          type: string
          enum: [Pending, Packed, Dissolved]
        groupingKeyValues:
          type: object
          additionalProperties:
            type: string
        wasManualOverride:
          type: boolean
        createdAt:
          type: string
          format: date-time
        createdBy:
          type: string
        version:
          type: integer

    ConsolidationPackRequest:
      type: object
      properties:
        containers:
          type: array
          items:
            $ref: '#/components/schemas/ContainerDto'
        dynamicContainers:
          type: array
          items:
            $ref: '#/components/schemas/DynamicContainerDto'
        pallets:
          type: array
          items:
            $ref: '#/components/schemas/PalletDto'
        equipment:
          type: array
          items:
            $ref: '#/components/schemas/EquipmentDto'
        algorithm:
          type: string
          maxLength: 64
        loadingMode:
          type: string
          maxLength: 64
        allowMultipleBoxes:
          type: boolean
          default: true
        allowPalletStacking:
          type: boolean
          default: false

    # ── Consolidation Profiles ───────────────────────────────────────
    CreateConsolidationProfileRequest:
      type: object
      required: [name, groupingKeys]
      properties:
        name:
          type: string
          maxLength: 256
        description:
          type: string
          maxLength: 2000
        groupingKeys:
          type: array
          items:
            type: string
          minItems: 1
          description: 'Fields to group by (e.g. "shipTo.postalCode", "serviceLevel")'
        consolidationLevel:
          type: string
          enum: [Box, Pallet]
          default: Box
        allowMixedOrdersInCarton:
          type: boolean
          default: false
        constraints:
          type: object
          properties:
            maxWeightPerGroup:
              type: number
              format: double
            maxItemsPerGroup:
              type: integer
            maxOrdersPerGroup:
              type: integer

    ConsolidationProfileResponse:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        groupingKeys:
          type: array
          items:
            type: string
        consolidationLevel:
          type: string
        allowMixedOrdersInCarton:
          type: boolean
        constraints:
          type: object
        version:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    # ── Wave Planning ────────────────────────────────────────────────
    WavePlanRequest:
      type: object
      required: [orders]
      properties:
        orders:
          type: array
          items:
            type: object
            required: [orderId, items]
            properties:
              orderId:
                type: string
              priority:
                type: integer
                default: 0
              items:
                type: array
                items:
                  type: object
                  required: [sku]
                  properties:
                    sku:
                      type: string
                    quantity:
                      type: integer
                      default: 1
          minItems: 1
        config:
          type: object
          properties:
            maxPicksPerWave:
              type: integer
              default: 200
            maxOrdersPerWave:
              type: integer
              default: 50
            zoneStrategy:
              type: string
              enum: [minimize-cross-zone, ignore-zones]
              description: Zone strategy (null for default zone-aware)

    WavePlanResponse:
      type: object
      properties:
        warehouseId:
          type: string
        waves:
          type: array
          items:
            type: object
            properties:
              waveId:
                type: string
              orders:
                type: array
                items:
                  type: string
              picks:
                type: array
                items:
                  type: object
                  properties:
                    sku:
                      type: string
                    location:
                      type: string
                    zone:
                      type: string
                    quantity:
                      type: integer
              zones:
                type: array
                items:
                  type: string
              metrics:
                type: object
                properties:
                  totalPicks:
                    type: integer
                  totalDistance:
                    type: number
                    format: double
                  naiveDistance:
                    type: number
                    format: double
                  improvementPercent:
                    type: number
                    format: double
                  estimatedTimeSeconds:
                    type: number
                    format: double
        summary:
          type: object
          properties:
            totalWaves:
              type: integer
            totalOrders:
              type: integer
            totalPicks:
              type: integer
            totalDistance:
              type: number
              format: double
            totalNaiveDistance:
              type: number
              format: double
            overallImprovementPercent:
              type: number
              format: double
            totalEstimatedTimeSeconds:
              type: number
              format: double

    # ── Fulfillment Plans ────────────────────────────────────────────
    CreateFulfillmentPlanRequest:
      type: object
      required: [orderIds]
      properties:
        orderIds:
          type: array
          items:
            type: string
          minItems: 1
        packingConfig:
          type: object
          description: Packing configuration overrides
        parcelCarriers:
          type: array
          items:
            type: string
          description: Parcel carrier IDs to rate (defaults to all connected)
        ltlCarriers:
          type: array
          items:
            type: string
          description: LTL carrier IDs to rate (defaults to all connected)

    SelectFulfillmentOptionRequest:
      type: object
      required: [optionKey]
      properties:
        optionKey:
          type: string
          description: 'Option key to select (e.g. "parcel", "ltl")'

    FulfillmentPlan:
      type: object
      description: Fulfillment plan with cost comparison options
      properties:
        id:
          type: string
        orderIds:
          type: array
          items:
            type: string
        status:
          type: string
        options:
          type: object
          additionalProperties:
            type: object
            description: Fulfillment option with cost breakdown
        selectedOptionKey:
          type: string
        shipmentIds:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    # ── Integrations ─────────────────────────────────────────────────
    IntegrationConfig:
      type: object
      properties:
        connectorType:
          type: string
        displayName:
          type: string
        enabled:
          type: boolean
        tenantUrl:
          type: string
        fieldMappings:
          $ref: '#/components/schemas/IntegrationFieldMappings'
        settings:
          $ref: '#/components/schemas/IntegrationSettings'
        status:
          type: object
          properties:
            lastSyncAt:
              type: string
              format: date-time
              
            lastSyncStatus:
              type: string
              
            lastSyncItemCount:
              type: integer
              
            lastError:
              type: string
              
            lastWebhookAt:
              type: string
              format: date-time
              
            lastWebhookStatus:
              type: string
              
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateOrUpdateIntegrationRequest:
      type: object
      properties:
        tenantUrl:
          type: string
        enabled:
          type: boolean
        credentials:
          type: object
          additionalProperties:
            type: string
          description: Connector-specific credentials (stored encrypted)
        settings:
          $ref: '#/components/schemas/IntegrationSettings'
        fieldMappings:
          $ref: '#/components/schemas/IntegrationFieldMappings'

    IntegrationFieldMappings:
      type: object
      properties:
        itemMappings:
          type: array
          items:
            $ref: '#/components/schemas/FieldMappingRule'
        deliveryMappings:
          type: array
          items:
            $ref: '#/components/schemas/FieldMappingRule'

    FieldMappingRule:
      type: object
      properties:
        sourceField:
          type: string
        targetField:
          type: string
        transform:
          type: string
          description: Optional transformation expression

    IntegrationSettings:
      type: object
      properties:
        defaultAlgorithm:
          type: string
        enableCostOptimization:
          type: boolean
        preferredDimensionUnit:
          type: string
        preferredWeightUnit:
          type: string
        webhookSecret:
          type: string
          description: HMAC secret for webhook signature validation

    ConnectorInfo:
      type: object
      properties:
        connectorType:
          type: string
        displayName:
          type: string
        description:
          type: string
        requiredCredentialFields:
          type: array
          items:
            type: string

    ConnectionTestResult:
      type: object
      properties:
        success:
          type: boolean
        message:
          type: string
        latencyMs:
          type: integer
          format: int64

    SyncResult:
      type: object
      properties:
        success:
          type: boolean
        itemsSynced:
          type: integer
        itemsSkipped:
          type: integer
        warnings:
          type: array
          items:
            type: string
        errors:
          type: array
          items:
            type: string

    # ── Identity ─────────────────────────────────────────────────────
    CurrentUserResponse:
      type: object
      properties:
        email:
          type: string
        orgId:
          type: string
        isAdmin:
          type: boolean

    # ── API Keys ─────────────────────────────────────────────────────
    CreateApiKeyRequest:
      type: object
      properties:
        label:
          type: string
          maxLength: 256
          description: Human-readable label for the key
        expiresInDays:
          type: integer
          
          description: "Expiration in days (null = no expiration)"

    ApiKeyResponse:
      type: object
      properties:
        keyId:
          type: string
        label:
          type: string
        apiKey:
          type: string
          
          description: Full key value (only returned on creation)
        keyPrefix:
          type: string
          description: Truncated prefix for identification
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
          
        mode:
          type: string
          enum: [test, live]
          description: '"test" or "live"'

    # ── Users ────────────────────────────────────────────────────────
    CreateUserRequest:
      type: object
      required: [email, temporaryPassword]
      properties:
        email:
          type: string
          format: email
          maxLength: 256
        temporaryPassword:
          type: string
          minLength: 1
          maxLength: 256
        isAdmin:
          type: boolean
          default: false

    UpdateUserRequest:
      type: object
      properties:
        nickname:
          type: string
          maxLength: 256
        isAdmin:
          type: boolean

    UserResponse:
      type: object
      properties:
        username:
          type: string
        email:
          type: string
        nickname:
          type: string
        status:
          type: string
        enabled:
          type: boolean
        isAdmin:
          type: boolean
        createdAt:
          type: string
          format: date-time

    # ── Usage ────────────────────────────────────────────────────────
    UsageResponse:
      type: object
      properties:
        totalRequests:
          type: integer
        totalItemsPacked:
          type: integer
        totalContainersUsed:
          type: integer
        dailyBreakdown:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
              requests:
                type: integer
              itemsPacked:
                type: integer
              containersUsed:
                type: integer
              byCategory:
                type: object
                additionalProperties:
                  type: integer

    # ── Entitlements ─────────────────────────────────────────────────
    EntitlementsResponse:
      type: object
      properties:
        enabledProducts:
          type: array
          items:
            type: string
        maxUsers:
          type: integer
        rateLimits:
          type: object
          additionalProperties:
            type: integer
          description: Rate limits per category
        monthlyRequestQuota:
          type: integer
