openapi: 3.0.3
info:
  title: ACMEcrm API
  version: 0.4.0
  description: |
    Sales system of **ACME analog Inc.** and system of record for **customers & resellers**, **products**, **price lists**, **quotes** and **orders** (production contracts).

    Orders are confirmed via the **e-approval system**: above defined thresholds an authorized signatory must approve (resolved via ACMEhr — approval limits/deputies). Confirmed orders (products, quantities, delivery dates) drive factory, port and treasury.

    **Pipeline (sales overlay).** `/pipeline` is a sales-facing **projection** of the deal lifecycle across leads, quotes and orders onto five ordered stages (`NEU · QUALIFIZIERT · ANGEBOT · VERHANDLUNG · GEWONNEN`). A `Deal` may be a bare lead or linked to its backing quote/order as it progresses; `probability` is derived from `stage`. The overlay carries sales attributes the transactional models don't (assigned `owner`, `contact`, `stage`), so `PATCH /pipeline/{id}` can set the stage directly (for board drag-drop / inline edit) while the transactional endpoints remain the source of truth for quotes and orders. Read model — see each operation for the status→stage derivation.

    **Contacts & mail.** A `Customer` is the company (system of record); `/contacts` are the people at a customer (a contact may be linked to one customer). A `Deal` references both its `customerId` and the `contactId` it is with, so a company/contact detail view can list its opportunities (`/pipeline?customerId=…`/`?contactId=…`) alongside its correspondence (`/threads?customerId=…`/`?contactId=…`). Mail threads are a lightweight overlay for the CRM UI; a real mail-sync backend is future work (currently mocked, like `/pipeline`).

    **Web forms & newsletter.** `/forms` are lead-capture forms / newsletter signups. Their layout (fields, required flags) **and** the actions a valid submission triggers (send email · create contact/opportunity · schedule appointment · newsletter subscribe/unsubscribe) are maintained in the Admin area. `POST /forms/{id}/submit` is the **public** (unauthenticated) submission tool: it validates the required fields, stores the submission on success and runs the configured actions. Captured submissions are readable by CRM users via `/forms/{id}/submissions`.

    Roles (ACMEbase): `WATCH` reads, `WORK` writes operationally (quotes/orders/approval), `ADMIN` maintains master data (products/price lists). Per operation under `x-required-role`; hierarchy ADMIN > WORK > WATCH.
  contact:
    name: ACME analog Inc · ACMEsuite
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0
servers:
  - url: https://demo.acmesoftware.de/api/crm
    description: ACMEsuite API (TLS at the edge)
security:
  - bearerAuth: []
tags:
  - name: Customers
    description: Customers and resellers (sales partners).
  - name: Products
    description: Product catalog (analog goods from the ACME factory).
  - name: PriceLists
    description: Price lists (list/reseller) with tiered prices + price resolution.
  - name: Quotes
    description: Quotes — draft through acceptance, conversion into an order.
  - name: Orders
    description: Orders incl. e-approval (submission, approval/rejection, production, fulfillment).
  - name: Contacts
    description: People at a customer (company). A contact may be linked to one customer.
  - name: Pipeline
    description: |
      Sales pipeline overlay — a stage projection over leads/quotes/orders for the CRM board, table and funnel. Read-mostly; stage is settable for drag-drop / inline edit.
  - name: Mail
    description: |
      Mail threads (correspondence) attached to a customer/contact. UI overlay — a real mail-sync backend is future work (currently mocked).
  - name: Forms
    description: |
      Web forms / newsletter signups. Layout and trigger actions are configured in Admin; `POST /forms/{id}/submit` is the public submission tool (validate → store → trigger).
paths:
  /customers:
    get:
      tags:
        - Customers
      summary: List customers/resellers
      operationId: listCustomers
      x-required-role: WATCH
      parameters:
        - name: kind
          in: query
          schema:
            $ref: '#/components/schemas/CustomerKind'
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/CustomerStatus'
        - name: resellerId
          in: query
          schema:
            type: string
          description: Customers of a reseller.
        - name: q
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Matches.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Customers
      summary: Create customer/reseller
      operationId: createCustomer
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerWrite'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /customers/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Customers
      summary: Read customer
      operationId: getCustomer
      x-required-role: WATCH
      responses:
        '200':
          description: Customer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Customers
      summary: Update customer
      operationId: updateCustomer
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerWrite'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /products:
    get:
      tags:
        - Products
      summary: List products
      operationId: listProducts
      x-required-role: WATCH
      parameters:
        - name: category
          in: query
          schema:
            type: string
        - name: active
          in: query
          schema:
            type: boolean
        - name: q
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Products.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Products
      summary: Create product (master data)
      operationId: createProduct
      x-required-role: ADMIN
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductWrite'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /products/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Products
      summary: Read product
      operationId: getProduct
      x-required-role: WATCH
      responses:
        '200':
          description: Product.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Products
      summary: Update product (master data)
      operationId: updateProduct
      x-required-role: ADMIN
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductWrite'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /price-lists:
    get:
      tags:
        - PriceLists
      summary: List price lists
      operationId: listPriceLists
      x-required-role: WATCH
      parameters:
        - name: kind
          in: query
          schema:
            $ref: '#/components/schemas/PriceListKind'
      responses:
        '200':
          description: Price lists.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PriceList'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - PriceLists
      summary: Create price list (master data)
      operationId: createPriceList
      x-required-role: ADMIN
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PriceListWrite'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /price-lists/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - PriceLists
      summary: Read price list
      operationId: getPriceList
      x-required-role: WATCH
      responses:
        '200':
          description: Price list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /price:
    get:
      tags:
        - PriceLists
      summary: Resolve effective unit price
      description: Returns the unit price applicable for customer + product + quantity (incl. tier/reseller).
      operationId: resolvePrice
      x-required-role: WATCH
      parameters:
        - name: customerId
          in: query
          required: true
          schema:
            type: string
        - name: productId
          in: query
          required: true
          schema:
            type: string
        - name: quantity
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
      responses:
        '200':
          description: Price.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResolvedPrice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /quotes:
    get:
      tags:
        - Quotes
      summary: List quotes
      operationId: listQuotes
      x-required-role: WATCH
      parameters:
        - name: customerId
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/QuoteStatus'
      responses:
        '200':
          description: Quotes.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Quote'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Quotes
      summary: Create quote
      description: Unit prices are resolved from the customer's price list if not provided.
      operationId: createQuote
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteWrite'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /quotes/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Quotes
      summary: Read quote
      operationId: getQuote
      x-required-role: WATCH
      responses:
        '200':
          description: Quote.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Quotes
      summary: Update quote (status/validity)
      operationId: updateQuote
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteUpdate'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /quotes/{id}/order:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Quotes
      summary: Convert quote into order
      description: Marks the quote as accepted and creates an order from it.
      operationId: convertQuoteToOrder
      x-required-role: WORK
      responses:
        '201':
          description: Order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /orders:
    get:
      tags:
        - Orders
      summary: List orders
      description: Consumers read the approved orders via `status=APPROVED` for the production cycle.
      operationId: listOrders
      x-required-role: WATCH
      parameters:
        - name: customerId
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/OrderStatus'
      responses:
        '200':
          description: Orders.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Orders
      summary: Create order
      operationId: createOrder
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderWrite'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
    delete:
      tags:
        - Orders
      summary: Delete all orders (data reset)
      description: |
        Destructive reset of the order stock — intended only for the data/economy reset, hence `ADMIN`. Not for normal operation.
      operationId: deleteAllOrders
      x-required-role: ADMIN
      responses:
        '204':
          description: Stock cleared.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /orders/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Orders
      summary: Read order
      operationId: getOrder
      x-required-role: WATCH
      responses:
        '200':
          description: Order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /orders/{id}/submission:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Orders
      summary: Submit order for approval (e-approval)
      description: Sets the status to PENDING_APPROVAL or — below the threshold — directly to APPROVED.
      operationId: submitOrder
      x-required-role: WORK
      responses:
        '200':
          description: Submitted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /orders/{id}/decision:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Orders
      summary: Approval decision (e-approval)
      operationId: decideOrder
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApprovalDecision'
      responses:
        '200':
          description: Decided.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /orders/{id}/production:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Orders
      summary: Record produced quantities (partial fulfillment)
      description: |
        Reports the quantities produced on a day per line; reduces `remainingQuantity`. The production cycle calls this after manufacturing. Once the order is thereby complete, it moves to FULFILLED.
      operationId: recordProduction
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductionReport'
      responses:
        '200':
          description: Updated order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /orders/{id}/fulfillment:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Orders
      summary: Mark order as fully fulfilled
      operationId: fulfillOrder
      x-required-role: WORK
      responses:
        '200':
          description: Fulfilled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /orders/{id}/cancellation:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Orders
      summary: Cancel order
      operationId: cancelOrder
      x-required-role: WORK
      responses:
        '200':
          description: Cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /pipeline:
    get:
      tags:
        - Pipeline
      summary: List pipeline deals
      description: |
        The sales pipeline as a list of `Deal`s, each projected onto one of five stages.

        **Status → stage derivation** (for deals backed by a quote/order): quote `DRAFT` → `NEU`; quote `SENT` → `QUALIFIZIERT`; quote `ACCEPTED` or order `CREATED` → `ANGEBOT`; order `PENDING_APPROVAL` → `VERHANDLUNG`; order `APPROVED` or `FULFILLED` → `GEWONNEN`. Lost deals (quote `REJECTED`/`EXPIRED`, order `REJECTED`/`CANCELLED`) are excluded by default. Bare leads (no quote yet) sit in the stage last set on them (default `NEU`).
      operationId: listPipeline
      x-required-role: WATCH
      parameters:
        - name: stage
          in: query
          schema:
            $ref: '#/components/schemas/PipelineStage'
        - name: owner
          in: query
          schema:
            type: string
          description: Filter by owner initials.
        - name: customerId
          in: query
          schema:
            type: string
          description: Deals of a customer.
        - name: contactId
          in: query
          schema:
            type: string
          description: Deals with a contact.
        - name: q
          in: query
          schema:
            type: string
          description: Free-text over company/contact.
      responses:
        '200':
          description: Deals.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Deal'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Pipeline
      summary: Create a pipeline deal (lead)
      description: |
        Adds a new deal to the pipeline. Without a `customerId`/backing quote it is a bare lead (enters `NEU` by default); the sales team later attaches a quote/order as it progresses.
      operationId: createDeal
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DealCreate'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Deal'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /pipeline/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Pipeline
      summary: Read a pipeline deal
      operationId: getDeal
      x-required-role: WATCH
      responses:
        '200':
          description: Deal.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Deal'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Pipeline
      summary: Update a pipeline deal (stage / owner / value / contact)
      description: |
        Sets sales-overlay attributes. Moving `stage` reflects the board drag-drop / inline edit; where a deal is backed by a quote/order the server advances the underlying record along its legal transition, otherwise it records the stage on the overlay.
      operationId: updateDeal
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DealUpdate'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Deal'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /contacts:
    get:
      tags:
        - Contacts
      summary: List contacts
      operationId: listContacts
      x-required-role: WATCH
      parameters:
        - name: customerId
          in: query
          schema:
            type: string
          description: Contacts of a customer.
        - name: q
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Contacts.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Contacts
      summary: Create contact
      operationId: createContact
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactWrite'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /contacts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Contacts
      summary: Read contact
      operationId: getContact
      x-required-role: WATCH
      responses:
        '200':
          description: Contact.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Contacts
      summary: Update contact
      operationId: updateContact
      x-required-role: WORK
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactWrite'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /threads:
    get:
      tags:
        - Mail
      summary: List mail threads
      description: Correspondence attached to a customer and/or contact. Filter by either.
      operationId: listThreads
      x-required-role: WATCH
      parameters:
        - name: customerId
          in: query
          schema:
            type: string
        - name: contactId
          in: query
          schema:
            type: string
        - name: q
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Threads (newest first).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/MailThread'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /threads/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Mail
      summary: Read a mail thread (with messages)
      operationId: getThread
      x-required-role: WATCH
      responses:
        '200':
          description: Thread.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MailThread'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /forms:
    get:
      tags:
        - Forms
      summary: List web forms
      description: Configured web forms / newsletter signups (definition maintained in Admin).
      operationId: listForms
      x-required-role: WATCH
      responses:
        '200':
          description: Forms.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebForm'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Forms
      summary: Create a web form (Admin)
      operationId: createForm
      x-required-role: ADMIN
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebFormWrite'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebForm'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /forms/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Forms
      summary: Read a web form (definition)
      operationId: getForm
      x-required-role: WATCH
      responses:
        '200':
          description: Form.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebForm'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Forms
      summary: Update a web form (Admin — fields, required flags, trigger actions)
      operationId: updateForm
      x-required-role: ADMIN
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebFormWrite'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebForm'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /forms/{id}/submit:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Forms
      summary: Submit a web form (public)
      description: |
        Public submission tool for the embedded web form / newsletter signup — **no auth** (overrides the global bearer security). Validates the submitted values against the form's required fields (`422` listing the offending fields on failure); on success stores the submission and runs the configured trigger actions (send email, create contact/opportunity, schedule appointment, newsletter subscribe/unsubscribe). Returns the stored submission incl. which actions were triggered.
      operationId: submitForm
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormSubmitRequest'
      responses:
        '201':
          description: Accepted & stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormSubmission'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /forms/{id}/submissions:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Forms
      summary: List captured submissions of a form
      operationId: listFormSubmissions
      x-required-role: WORK
      responses:
        '200':
          description: Submissions (newest first).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/FormSubmission'
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT from ACMEbase auth. The bearer role (WATCH/WORK/ADMIN) must satisfy the role required per operation under `x-required-role` (hierarchy ADMIN > WORK > WATCH).
  schemas:
    CustomerKind:
      type: string
      enum:
        - CUSTOMER
        - RESELLER
    CustomerStatus:
      type: string
      enum:
        - PROSPECT
        - ACTIVE
        - BLOCKED
    Customer:
      type: object
      required:
        - id
        - name
        - kind
        - status
      properties:
        id:
          type: string
        name:
          type: string
          example: Kontor Nord GmbH
        kind:
          $ref: '#/components/schemas/CustomerKind'
        status:
          $ref: '#/components/schemas/CustomerStatus'
        email:
          type: string
          format: email
          nullable: true
        country:
          type: string
          nullable: true
          example: DE
        parentResellerId:
          type: string
          nullable: true
          description: For end customers of a reseller.
        priceListId:
          type: string
          nullable: true
          description: Assigned price list.
    CustomerWrite:
      type: object
      required:
        - name
        - kind
      properties:
        name:
          type: string
        kind:
          $ref: '#/components/schemas/CustomerKind'
        status:
          $ref: '#/components/schemas/CustomerStatus'
        email:
          type: string
          format: email
        country:
          type: string
        parentResellerId:
          type: string
          nullable: true
        priceListId:
          type: string
          nullable: true
    Product:
      type: object
      required:
        - id
        - sku
        - name
        - active
      properties:
        id:
          type: string
        sku:
          type: string
          example: ACME-RADIO-01
        name:
          type: string
          example: Tube radio „Nostalgie“
        category:
          type: string
          nullable: true
        unit:
          type: string
          default: piece
        active:
          type: boolean
        listPrice:
          $ref: '#/components/schemas/Money'
    ProductWrite:
      type: object
      required:
        - sku
        - name
      properties:
        sku:
          type: string
        name:
          type: string
        category:
          type: string
        unit:
          type: string
        active:
          type: boolean
        listPrice:
          $ref: '#/components/schemas/Money'
    PriceListKind:
      type: string
      enum:
        - LIST
        - RESELLER
        - CUSTOM
    PriceListItem:
      type: object
      required:
        - productId
        - unitPrice
      properties:
        productId:
          type: string
        unitPrice:
          type: number
          example: 149
        minQuantity:
          type: integer
          default: 1
          description: The price applies from this quantity on (tier).
    PriceList:
      type: object
      required:
        - id
        - name
        - currency
        - kind
        - items
      properties:
        id:
          type: string
        name:
          type: string
        currency:
          type: string
          minLength: 3
          maxLength: 3
          example: EUR
        kind:
          $ref: '#/components/schemas/PriceListKind'
        validity:
          $ref: '#/components/schemas/DateRange'
        items:
          type: array
          items:
            $ref: '#/components/schemas/PriceListItem'
    PriceListWrite:
      type: object
      required:
        - name
        - currency
        - kind
      properties:
        name:
          type: string
        currency:
          type: string
          minLength: 3
          maxLength: 3
        kind:
          $ref: '#/components/schemas/PriceListKind'
        validFrom:
          type: string
          format: date
        validUntil:
          type: string
          format: date
          nullable: true
        items:
          type: array
          items:
            $ref: '#/components/schemas/PriceListItem'
    ResolvedPrice:
      type: object
      required:
        - productId
        - customerId
        - quantity
        - unitPrice
      properties:
        productId:
          type: string
        customerId:
          type: string
        quantity:
          type: integer
        unitPrice:
          type: number
        currency:
          type: string
        priceListId:
          type: string
          nullable: true
        source:
          type: string
          enum:
            - PRICE_LIST
            - LIST_PRICE
    QuoteStatus:
      type: string
      enum:
        - DRAFT
        - SENT
        - ACCEPTED
        - REJECTED
        - EXPIRED
    LineWrite:
      type: object
      required:
        - productId
        - quantity
      properties:
        productId:
          type: string
        quantity:
          type: integer
          minimum: 1
        unitPrice:
          type: number
          nullable: true
          description: Optional — otherwise resolved from the price list.
        discountPercent:
          type: number
          default: 0
    Line:
      type: object
      required:
        - productId
        - quantity
        - unitPrice
        - lineTotal
      properties:
        productId:
          type: string
        productName:
          type: string
        quantity:
          type: integer
        unitPrice:
          type: number
        discountPercent:
          type: number
        lineTotal:
          type: number
        remainingQuantity:
          type: integer
          description: Quantity still to be produced (order lines).
    Quote:
      type: object
      required:
        - id
        - customerId
        - status
        - currency
        - lines
        - netTotal
      properties:
        id:
          type: string
        customerId:
          type: string
        customerName:
          type: string
        status:
          $ref: '#/components/schemas/QuoteStatus'
        currency:
          type: string
        validUntil:
          type: string
          format: date
          nullable: true
        lines:
          type: array
          items:
            $ref: '#/components/schemas/Line'
        netTotal:
          type: number
        createdOn:
          type: string
          format: date
          nullable: true
    QuoteWrite:
      type: object
      required:
        - customerId
        - lines
      properties:
        customerId:
          type: string
        validUntil:
          type: string
          format: date
          nullable: true
        lines:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/LineWrite'
    QuoteUpdate:
      type: object
      properties:
        status:
          $ref: '#/components/schemas/QuoteStatus'
        validUntil:
          type: string
          format: date
          nullable: true
    OrderStatus:
      type: string
      enum:
        - CREATED
        - PENDING_APPROVAL
        - APPROVED
        - REJECTED
        - FULFILLED
        - CANCELLED
    Order:
      type: object
      required:
        - id
        - customerId
        - status
        - orderDate
        - lines
        - total
      properties:
        id:
          type: string
        customerId:
          type: string
        customerName:
          type: string
        customerCountry:
          type: string
          nullable: true
        quoteId:
          type: string
          nullable: true
        status:
          $ref: '#/components/schemas/OrderStatus'
        orderDate:
          type: string
          format: date
        lines:
          type: array
          items:
            $ref: '#/components/schemas/Line'
        total:
          $ref: '#/components/schemas/Money'
        approval:
          $ref: '#/components/schemas/Approval'
        shippingMode:
          type: string
          nullable: true
          description: Shipping mode (e.g. AIR = air freight, otherwise sea freight).
        note:
          type: string
          nullable: true
    OrderWrite:
      type: object
      required:
        - customerId
        - lines
      properties:
        customerId:
          type: string
        quoteId:
          type: string
          nullable: true
        lines:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/LineWrite'
        shippingMode:
          type: string
          nullable: true
        note:
          type: string
          nullable: true
    ProductionReport:
      description: Per product, the share produced today (additionally fulfilled).
      type: object
      required:
        - lines
      properties:
        lines:
          type: array
          minItems: 1
          items:
            type: object
            required:
              - productId
              - producedQuantity
            properties:
              productId:
                type: string
              producedQuantity:
                type: integer
                minimum: 0
    PipelineStage:
      description: |
        Ordered sales stage. `probability` is derived from it: NEU 15 · QUALIFIZIERT 35 · ANGEBOT 60 · VERHANDLUNG 80 · GEWONNEN 100 (%).
      type: string
      enum:
        - NEU
        - QUALIFIZIERT
        - ANGEBOT
        - VERHANDLUNG
        - GEWONNEN
    DealSource:
      description: What the deal is currently backed by in the transactional model.
      type: string
      enum:
        - LEAD
        - QUOTE
        - ORDER
    Owner:
      description: Sales owner assigned to a deal (avatar initials + display name).
      type: object
      required:
        - initials
      properties:
        initials:
          type: string
          minLength: 1
          maxLength: 3
          example: JS
        name:
          type: string
          nullable: true
          example: Jana Schmidt
    Deal:
      description: A pipeline deal — a stage projection over a lead/quote/order (see `GET /pipeline`).
      type: object
      required:
        - id
        - source
        - company
        - stage
        - probability
        - value
      properties:
        id:
          type: string
          example: deal-1024
        source:
          $ref: '#/components/schemas/DealSource'
        quoteId:
          type: string
          nullable: true
        orderId:
          type: string
          nullable: true
        customerId:
          type: string
          nullable: true
        contactId:
          type: string
          nullable: true
          description: The contact this deal is with.
        company:
          type: string
          example: Nordwind Logistik
        contact:
          type: string
          nullable: true
          example: Michael Braun · CPO
        stage:
          $ref: '#/components/schemas/PipelineStage'
        probability:
          type: integer
          minimum: 0
          maximum: 100
          description: Derived from `stage`.
        value:
          $ref: '#/components/schemas/Money'
        owner:
          $ref: '#/components/schemas/Owner'
        lastActivity:
          type: string
          nullable: true
          description: Human label
          e.g. "Angebot gesendet · vor 18 Min".: null
        lastActivityAt:
          type: string
          format: date-time
          nullable: true
        ageDays:
          type: integer
          nullable: true
          description: Days since the deal entered its current stage.
    DealCreate:
      type: object
      required:
        - company
      properties:
        company:
          type: string
        contact:
          type: string
          nullable: true
        customerId:
          type: string
          nullable: true
        contactId:
          type: string
          nullable: true
        stage:
          $ref: '#/components/schemas/PipelineStage'
        value:
          $ref: '#/components/schemas/Money'
        ownerInitials:
          type: string
          nullable: true
    DealUpdate:
      description: Partial update of a deal's sales-overlay attributes.
      type: object
      properties:
        stage:
          $ref: '#/components/schemas/PipelineStage'
        company:
          type: string
        contact:
          type: string
          nullable: true
        contactId:
          type: string
          nullable: true
        value:
          $ref: '#/components/schemas/Money'
        ownerInitials:
          type: string
          nullable: true
    Contact:
      description: A person at a customer (company).
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
          example: contact-1001
        customerId:
          type: string
          nullable: true
          description: The company this contact belongs to.
        name:
          type: string
          example: Sara Mena
        role:
          type: string
          nullable: true
          example: CTO
        email:
          type: string
          format: email
          nullable: true
        phone:
          type: string
          nullable: true
        primary:
          type: boolean
          default: false
          description: Primary contact of the customer.
        newsletter:
          type: boolean
          default: false
          description: Newsletter opt-in (set by form newsletter actions).
    ContactWrite:
      type: object
      required:
        - name
      properties:
        customerId:
          type: string
          nullable: true
        name:
          type: string
        role:
          type: string
          nullable: true
        email:
          type: string
          format: email
          nullable: true
        phone:
          type: string
          nullable: true
        primary:
          type: boolean
        newsletter:
          type: boolean
    MailDirection:
      type: string
      enum:
        - INBOUND
        - OUTBOUND
    MailMessage:
      type: object
      required:
        - id
        - direction
        - sentAt
      properties:
        id:
          type: string
        direction:
          $ref: '#/components/schemas/MailDirection'
        from:
          type: string
          nullable: true
        to:
          type: string
          nullable: true
        sentAt:
          type: string
          format: date-time
        snippet:
          type: string
          nullable: true
        body:
          type: string
          nullable: true
    MailThread:
      description: A correspondence thread attached to a customer and/or contact.
      type: object
      required:
        - id
        - subject
      properties:
        id:
          type: string
          example: thread-1001
        subject:
          type: string
          example: Angebot Rahmen XL
        customerId:
          type: string
          nullable: true
        contactId:
          type: string
          nullable: true
        participants:
          type: array
          items:
            type: string
        messageCount:
          type: integer
        lastMessageAt:
          type: string
          format: date-time
          nullable: true
        preview:
          type: string
          nullable: true
          description: Snippet of the latest message.
        messages:
          type: array
          items:
            $ref: '#/components/schemas/MailMessage'
    FormFieldType:
      type: string
      enum:
        - TEXT
        - EMAIL
        - PHONE
        - NUMBER
        - TEXTAREA
        - CHECKBOX
        - SELECT
        - DATE
    FormField:
      description: One field of a web form.
      type: object
      required:
        - key
        - label
        - type
      properties:
        key:
          type: string
          example: email
          description: Unique within the form; keys the submission values.
        label:
          type: string
          example: E-Mail
        type:
          $ref: '#/components/schemas/FormFieldType'
        required:
          type: boolean
          default: false
        placeholder:
          type: string
          nullable: true
        options:
          type: array
          items:
            type: string
          nullable: true
          description: Choices for SELECT.
    FormActionType:
      description: |
        What a valid submission triggers (configured in Admin): send an email, create a contact or opportunity (deal), schedule an appointment, or subscribe/unsubscribe the submitter from the newsletter.
      type: string
      enum:
        - SEND_EMAIL
        - CREATE_CONTACT
        - CREATE_OPPORTUNITY
        - SCHEDULE_APPOINTMENT
        - NEWSLETTER_SUBSCRIBE
        - NEWSLETTER_UNSUBSCRIBE
    FormAction:
      description: A trigger configured in Admin, run on a valid submission.
      type: object
      required:
        - type
      properties:
        type:
          $ref: '#/components/schemas/FormActionType'
        config:
          type: object
          nullable: true
          additionalProperties: true
          description: |
            Action-specific settings, e.g. template + recipient for SEND_EMAIL, target stage/owner for CREATE_OPPORTUNITY, calendar/slot for SCHEDULE_APPOINTMENT, list id for NEWSLETTER_*.
    WebForm:
      description: A web form / newsletter signup. Layout and trigger actions are maintained in Admin.
      type: object
      required:
        - id
        - name
        - slug
        - fields
      properties:
        id:
          type: string
          example: form-lead
        name:
          type: string
          example: Kontaktformular
        slug:
          type: string
          example: kontakt
          description: Public identifier used to embed the form.
        description:
          type: string
          nullable: true
        enabled:
          type: boolean
          default: true
        fields:
          type: array
          items:
            $ref: '#/components/schemas/FormField'
        actions:
          type: array
          items:
            $ref: '#/components/schemas/FormAction'
          description: Triggered on a valid submission.
        createdOn:
          type: string
          format: date
          nullable: true
    WebFormWrite:
      type: object
      required:
        - name
        - slug
        - fields
      properties:
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        enabled:
          type: boolean
        fields:
          type: array
          items:
            $ref: '#/components/schemas/FormField'
        actions:
          type: array
          items:
            $ref: '#/components/schemas/FormAction'
    FormSubmitRequest:
      description: A public submission — field values keyed by `FormField.key`.
      type: object
      required:
        - values
      properties:
        values:
          type: object
          additionalProperties: true
          example:
            name: Sara Mena
            email: sara@example.com
            newsletter: true
    TriggeredAction:
      description: Outcome of one configured action after a submission.
      type: object
      required:
        - type
        - status
      properties:
        type:
          $ref: '#/components/schemas/FormActionType'
        status:
          type: string
          enum:
            - DONE
            - SKIPPED
            - FAILED
            - PENDING
        detail:
          type: string
          nullable: true
        ref:
          type: string
          nullable: true
          description: Id of a created contact/deal/appointment
          if any.: null
    FormSubmission:
      description: A stored form submission and the outcome of its trigger actions.
      type: object
      required:
        - id
        - formId
        - values
        - submittedAt
      properties:
        id:
          type: string
        formId:
          type: string
        values:
          type: object
          additionalProperties: true
        submittedAt:
          type: string
          format: date-time
        triggered:
          type: array
          items:
            $ref: '#/components/schemas/TriggeredAction'
    Problem:
      description: Error per RFC 7807.
      type: object
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
    Money:
      description: Monetary amount. `unlimited=true` → no amount limit (then amount/currency are empty).
      type: object
      properties:
        amount:
          type: number
          nullable: true
          example: 50000
        currency:
          type: string
          minLength: 3
          maxLength: 3
          nullable: true
          example: EUR
        unlimited:
          type: boolean
          default: false
    DateRange:
      description: Validity period. `until=null` = open-ended.
      type: object
      properties:
        from:
          type: string
          format: date
        until:
          type: string
          format: date
          nullable: true
    Approval:
      description: E-approval status of an order/procurement.
      type: object
      properties:
        required:
          type: boolean
        approverId:
          type: string
          nullable: true
          description: Authorized signatory (resolved from ACMEhr).
        decision:
          type: string
          nullable: true
          enum:
            - APPROVE
            - REJECT
        decidedOn:
          type: string
          format: date
          nullable: true
        comment:
          type: string
          nullable: true
    ApprovalDecision:
      description: Approval decision by an authorized signatory.
      type: object
      required:
        - approverId
        - decision
      properties:
        approverId:
          type: string
          description: Person (ACMEhr) making the decision.
        decision:
          type: string
          enum:
            - APPROVE
            - REJECT
        comment:
          type: string
          nullable: true
  responses:
    Unauthorized:
      description: Missing/invalid authentication or insufficient role.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unprocessable:
      description: Semantically invalid.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: Resource not found.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
