openapi: 3.1.0
info:
  title: prro-service API
  description: |
    REST API сервісу фіскалізації ПРРО.

    Машинні інтеграції автентифікуються довгоживучим Bearer JWT, виданим у кабінеті (або через prroctl); сам кабінет працює через вхід Google OIDC і сесійну cookie.
  version: 0.2.1
paths:
  /api/v1/version:
    get:
      operationId: getVersion
      summary: Версія збірки сервісу
      tags:
        - system
      responses:
        "200":
          description: Версія, зашита у збірку.
          content:
            application/json:
              schema:
                type: object
                required:
                  - version
                properties:
                  version:
                    type: string
                    examples:
                      - dev
                      - v0.3.0
  /api/v1/whoami:
    get:
      operationId: whoami
      summary: Дізнатися, що стоїть за наданим машинним токеном
      tags:
        - system
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Клієнт, дозволи (scopes) та обмеження за касами.
          content:
            application/json:
              schema:
                type: object
                required:
                  - client_id
                  - scopes
                  - jti
                  - all_cash_registers
                properties:
                  client_id:
                    type: string
                    format: uuid
                  scopes:
                    type: array
                    items:
                      type: string
                  all_cash_registers:
                    type: boolean
                    description: |
                      true — токен діє на всі каси клієнта (поточні й майбутні); false — лише на перелічені у cash_registers.
                  cash_registers:
                    type: array
                    description: |
                      Присутнє лише для обмеженого токена — перелік uuid кас, до яких він прив'язаний. Відсутнє, коли all_cash_registers.
                    items:
                      type: string
                      format: uuid
                  jti:
                    type: string
                    format: uuid
                  expires_at:
                    type: string
                    format: date-time
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/receipts:
    post:
      operationId: createReceipt
      summary: Зареєструвати чек (реалізація/повернення/сторно або рух готівки)
      description: |
        Реєструє або розрахунок за товари (sale/return/storno), або рух готівки повз продаж (service_in/service_out — службове внесення та службова видача, тобто інкасація). І те, й інше — документи DOCTYPE 0, які проходять однаковий шлях.

        Виконується повний фіскальний цикл: локальний номер, XML за check01, підпис КЕП (CAdES-T), надсилання на фіскальний сервер ДПС, квитанція. У синхронному режимі (типовому) відповідь чекає на результат до 30 секунд; якщо не вклалися — 202 і номер завдання, за яким статус опитують через GET /tasks/{id}. Режим mode=async (у query або в тілі) віддає 202 одразу, а результат приходить опитуванням та/або вебхуком.

        Заголовок Idempotency-Key обов'язковий: повтор із тим самим ключем поверне те саме завдання, а не зареєструє другий чек.
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Mode"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReceiptInput"
      responses:
        "200":
          description: Повтор за тим самим Idempotency-Key; завдання вже завершене.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "201":
          description: Зареєстровано в межах синхронного очікування.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "202":
          description: Ще виконується; статус — через GET /tasks/{id}.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: |
            Передплачений баланс опустився нижче порога: нові чеки заблоковано до поповнення. Операції зі змінами лишаються доступними — закрити зміну можна завжди.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          description: Цей Idempotency-Key уже використано для іншої операції.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Не пройшла валідація або завдання завершилося помилкою (немає відкритої зміни, відмова ДПС тощо).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/tasks/{id}:
    get:
      operationId: getTask
      summary: Статус і результат операції (опитування)
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Завдання в поточному стані.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
  /api/v1/cash-registers:
    get:
      operationId: listCashRegistersForToken
      summary: Каси, доступні цьому токену (звідки інтегратор бере cash_register_id)
      description: |
        Перелік реєстраторів, на які діє наданий токен: повний токен бачить усі каси клієнта, обмежений — лише свої. Це разовий виклик під час налаштування інтеграції: усі інші маршрути адресують касу за її uuid, і взяти цей uuid більше ніде — фіскальний номер сторонній сервіс показує людині, а працює за id.

        Віддає лише те, чим касу впізнають і адресують. Поточний стан (зміна, лічильники, офлайн-бюджет) — за GET /cash-registers/{id}.
      tags:
        - fiscal
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Каси в межах токена, від найстаріших.
          content:
            application/json:
              schema:
                type: object
                required:
                  - cash_registers
                properties:
                  cash_registers:
                    type: array
                    items:
                      $ref: "#/components/schemas/CashRegisterListItem"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/cash-registers/{id}:
    get:
      operationId: getCashRegisterState
      summary: Стан реєстратора (зміна, режим, лічильники)
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Поточний стан реєстратора.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CashRegisterState"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
  /api/v1/cash-registers/{id}/shifts:
    post:
      operationId: openShift
      summary: Відкрити зміну (DOCTYPE 100; семантика завдання така сама, як у чеків)
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Mode"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                cashier:
                  type: string
                mode:
                  type: string
                  enum:
                    - sync
                    - async
                  default: sync
      responses:
        "201":
          description: Зміну відкрито.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "202":
          description: Ще виконується; статус — через GET /tasks/{id}.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/cash-registers/{id}/shifts/current:
    delete:
      operationId: closeShift
      summary: Закрити поточну зміну (Z-звіт і повідомлення про закриття однією операцією)
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Mode"
      responses:
        "201":
          description: Зміну закрито; у результаті — і Z-звіт, і документ закриття.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "202":
          description: Ще виконується; статус — через GET /tasks/{id}.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/cash-registers/{id}/offline-session/close:
    post:
      operationId: closeOfflineSession
      summary: Примусово закрити активну офлайн-сесію (документ завершення сесії потрапляє до журналу одразу, а пакети досилаються у фоні)
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Mode"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                cashier:
                  type: string
                mode:
                  type: string
                  enum:
                    - sync
                    - async
                  default: sync
      responses:
        "201":
          description: Сесію закрито; у результаті — документ завершення.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "202":
          description: Ще виконується; статус — через GET /tasks/{id}.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/cash-registers/{id}/offline-session/retry:
    post:
      operationId: retryOfflineSubmission
      summary: Відновити зупинене надсилання офлайн-пакетів (сервіс зупинив сесію після повторюваних непоправних відмов — спершу усуньте причину)
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Скільки сесій повернуто до надсилання.
          content:
            application/json:
              schema:
                type: object
                required:
                  - resumed_sessions
                properties:
                  resumed_sessions:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
  /api/v1/cash-registers/{id}/attention:
    delete:
      operationId: clearRegisterAttention
      summary: Зняти позначку «потребує уваги» з реєстратора
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Чи справді було знято позначку.
          content:
            application/json:
              schema:
                type: object
                required:
                  - cleared
                properties:
                  cleared:
                    type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
  /api/v1/cash-registers/{id}/cash-balance:
    put:
      operationId: setCashBalance
      summary: Заявити, скільки готівки насправді в касі
      description: |
        Переставляє лічильник готівки на перераховану суму. Це те, що потрібно реєстратору, який приєднується до сервісу з непорожньою касою: лічильник стартує з нуля і не має звідки дізнатися початкову суму. Так само виправляють лічильник, який розійшовся з касою.

        Нічого фіскального не відбувається: заявлений залишок не потрапляє в жоден документ і не змінює підсумків зміни. Щоб зареєструвати готівку, яка справді рухається, надсилайте чек service_in або service_out.

        Заявлений залишок не може бути від'ємним — на відміну від самого лічильника, який чесно йде в мінус, коли зустрічає гроші, приходу яких не бачив.
      tags:
        - fiscal
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount
              properties:
                amount:
                  allOf:
                    - $ref: "#/components/schemas/CashAmount"
                  description: Перерахований залишок; не може бути від'ємним.
      responses:
        "200":
          description: Лічильник після заяви.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CashBalanceDeclaration"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/billing/usage:
    get:
      operationId: billingUsage
      summary: Передплачений баланс і підсумок тарифікації за період
      description: |
        Суми — копійки десятковим рядком, ніколи не числа з рухомою комою. Типовий період — поточний календарний місяць за UTC; межа `to` не включається. Чеки тарифікуються асинхронно після реєстрації, тож підсумок може відставати від останнього чека на кілька секунд. Період не може перевищувати 366 днів — ширший діапазон дає 422.
      tags:
        - billing
      security:
        - bearerAuth: []
      parameters:
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Баланс і використання за період.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingUsage"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/invoices:
    get:
      operationId: listInvoices
      summary: Рахунки клієнта, від найновішого періоду
      description: |
        Рахунок — це підсумок одного розрахункового періоду, а не окремий платіж: на клієнта припадає один рядок за період, тож типова сторінка у 50 рахунків покриває роки щомісячної тарифікації.
      tags:
        - billing
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Рахунки клієнта.
          content:
            application/json:
              schema:
                type: object
                required:
                  - invoices
                properties:
                  invoices:
                    type: array
                    items:
                      $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/webhooks:
    get:
      operationId: listWebhooks
      summary: Перелік вебхуків клієнта (без секретів)
      tags:
        - webhooks
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Точки доставки клієнта.
          content:
            application/json:
              schema:
                type: object
                required:
                  - webhooks
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEndpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createWebhook
      summary: Зареєструвати вебхук (секрет HMAC показують рівно один раз)
      description: |
        Доставка — це POST з тілом {delivery_id, event, attempt, occurred_at, data} і заголовком X-Signature вигляду "sha256=<HMAC-SHA256 тіла у hex>", порахованим на секреті цієї точки доставки.

        Гарантія — «щонайменше один раз»: приймач має відкидати повтори за delivery_id. Події одного реєстратора приходять по порядку. Якщо secret не вказати, його згенерує сервер і поверне лише у цій відповіді.
      tags:
        - webhooks
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
                  description: Абсолютний http(s)-URL приймача.
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEvent"
                  description: Події, на які підписані; порожній список або відсутнє поле означає всі події.
                secret:
                  type: string
                  description: Секрет HMAC; якщо не вказати, згенерує сервер.
      responses:
        "201":
          description: Створено; секрет повертається рівно один раз.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/WebhookEndpoint"
                  - type: object
                    required:
                      - secret
                    properties:
                      secret:
                        type: string
                        description: Отримати повторно неможливо.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/webhooks/{id}:
    patch:
      operationId: updateWebhook
      summary: Змінити URL, підписку або ознаку активності (часткове оновлення)
      tags:
        - webhooks
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEvent"
                active:
                  type: boolean
                  description: false призупиняє доставку, не втрачаючи черги.
      responses:
        "200":
          description: Оновлено.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
    delete:
      operationId: deleteWebhook
      summary: Видалити точку доставки (недоставлену чергу буде відкладено)
      tags:
        - webhooks
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Видалено.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
  /api/v1/webhooks/{id}/deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: Історія доставок, від найновішої (status=dead — черга невдалих)
      tags:
        - webhooks
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            type: string
            enum:
              - pending
              - delivering
              - delivered
              - failed
              - dead
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Доставки.
          content:
            application/json:
              schema:
                type: object
                required:
                  - deliveries
                properties:
                  deliveries:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookDelivery"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
        "422":
          $ref: "#/components/responses/Error"
  /api/v1/webhooks/{id}/deliveries/{deliveryId}/replay:
    post:
      operationId: replayWebhookDelivery
      summary: Повернути доставку в чергу з новим запасом спроб (зазвичай — невдалу)
      tags:
        - webhooks
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: deliveryId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Повернуто в чергу; пізніші події того самого реєстратора зачекають на неї — порядок зберігається й після повтору.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDelivery"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          description: Саме зараз триває спроба доставки.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /system/dps:
    get:
      operationId: systemDPSStatus
      summary: Поточний режим роботи з ДПС (онлайн/офлайн)
      description: |
        Той самий знімок доступності, доступний будь-якій автентифікованій інтеграції: клієнт може показувати «ДПС офлайн» власним операторам.
      tags:
        - system
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Знімок стану доступності.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DPSStatus"
        "401":
          $ref: "#/components/responses/Unauthorized"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "Довгоживучий машинний JWT, виданий у кабінеті або через prroctl. Токен може бути обмежений переліком кас (див. cash_registers у видачі та whoami). Обмежений токен: до касових операцій поза його переліком — 404 (як до чужої каси, існування не розкривається); до ресурсів рівня клієнта (білінг, вебхуки) — 403. Токен без обмежень діє на всі каси клієнта, поточні й майбутні."
  parameters:
    Mode:
      name: mode
      in: query
      required: false
      schema:
        type: string
        enum:
          - sync
          - async
      description: |
        Режим доставки результату; має перевагу над однойменним полем у тілі. sync (типовий) чекає на результат у межах синхронного очікування; async одразу відповідає 202 і номером завдання, а результат приходить через GET /tasks/{id} та/або вебхуком.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
      description: |
        Ключ операції, який обирає клієнт. Повтор запиту з тим самим ключем поверне початкове завдання, а не виконає фіскальну операцію вдруге.
  responses:
    Error:
      description: Конверт помилки.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Немає облікових даних, або вони недійсні, протерміновані чи відкликані.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          examples:
            - unauthorized
            - validation_failed
        message:
          type: string
        details: {}
    CashRegisterState:
      type: object
      required:
        - id
        - fiscal_number
        - local_number
        - mode
        - state
        - offline_limits
      properties:
        id:
          type: string
          format: uuid
        fiscal_number:
          type: string
        local_number:
          type: string
        mode:
          type: string
          enum:
            - production
            - test
        state:
          type: string
          enum:
            - closed
            - opened
            - offline
        offline_ready:
          type: boolean
          description: Чи має реєстратор видані ДПС офлайн-реквізити (приходять із квитанцією на відкриття зміни).
        shift_mode:
          $ref: "#/components/schemas/ShiftMode"
        shift_close_times:
          $ref: "#/components/schemas/ShiftCloseTimes"
        cash_balance:
          $ref: "#/components/schemas/CashBalance"
        current_shift:
          type: object
          properties:
            id:
              type: string
              format: uuid
            number:
              type: integer
              format: int64
              description: "Наскрізний номер зміни в межах каси («Зміна №147»). Присвоюється локально: ДПС номера зміни не видає."
            status:
              type: string
              enum:
                - opening
                - opened
                - closing
                - closed
            testing:
              type: boolean
            opened_at:
              type: string
              format: date-time
            documents:
              type: integer
              description: "Скільки документів створила зміна — усіх видів, а не лише розрахункових, які рахують підсумки: сповіщення про відкриття, чеки, рухи готівки. Відсутнє, якщо порахувати не вдалося (нуля, якого в зміні немає, тут не буває)."
        shift_totals:
          $ref: "#/components/schemas/ShiftTotals"
        offline_session:
          type: object
          description: Присутня, поки реєстратор працює офлайн.
          properties:
            id:
              type: string
              format: uuid
            dps_session_id:
              type: integer
              format: int64
            started_at:
              type: string
              format: date-time
            documents:
              type: integer
              format: int64
            last_significant_at:
              type: string
              format: date-time
              nullable: true
        offline_limits:
          type: object
          description: "Законодавчі межі офлайн-роботи в секундах: 36 годин на одну сесію і 168 годин на календарний місяць. Скільки лишилося в сесії, рахують від offline_session.started_at, а скільки лишилося на місяць — від offline_month_used_seconds."
          required:
            - session_seconds
            - month_seconds
          properties:
            session_seconds:
              type: integer
              format: int64
            month_seconds:
              type: integer
              format: int64
        offline_month_used_seconds:
          type: integer
          format: int64
          description: Час, відпрацьований офлайн у цьому календарному місяці (за київським часом).
        attention:
          type: object
          description: Присутня, коли сервіс позначив реєстратор як такий, що потребує уваги (див. CashRegister.attention_reason). Знімається через DELETE .../attention; якщо зупинилося надсилання офлайн-пакетів, його додатково відновлюють через POST .../offline-session/retry.
          required:
            - reason
          properties:
            reason:
              type: string
              enum:
                - recurring_remediation
                - offline_submission_paused
                - auto_close_failing
            since:
              type: string
              format: date-time
              nullable: true
    ShiftMode:
      type: string
      enum:
        - manual
        - round_the_clock
        - working_hours
      description: "Як керують змінами реєстратора. `manual` — клієнт відкриває і закриває їх через API, автоматично не відбувається нічого. `round_the_clock` — реєстратор не припиняє продавати: зміна закривається в кожен із shift_close_times, а наступний чек відкриває нову. `working_hours` — денне вікно, задане двома межами у shift_close_times. В обох керованих режимах зміну відкриває перший же чек, тож жоден продаж не втрачається через закритий реєстратор."
    ShiftCloseTimes:
      type: array
      items:
        type: string
        pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
      description: |-
        Час щоденного закриття зміни за київським часом. Порожній рівно тоді, коли shift_mode — `manual`. Часи прив'язані до годинника, тож реєстратор, який відкрився за секунду після закриття, все одно закриється в той самий час, що й щодня, а не поповзе.

        Потрібно щонайменше два значення з проміжком не більше 18 годин (рахуючи через опівніч): зміна триває стільки, скільки лишилося до наступного закриття, а решта законодавчої доби — це єдиний запас, у якому можна повторити невдале закриття.

        `working_hours` містить рівно два — початок і кінець дня, саме в цьому порядку, щоб вікно через опівніч ("08:00", потім "02:00") зберігало напрямок. Обидва є точками закриття: денна зміна завершується в кінці вікна, а чек, що прийшов у неробочий час, усе одно відкриє зміну (у продажу ніколи не відмовляють), і вона закриється з початком наступного дня. `round_the_clock` приймає два або більше значень за зростанням.
      example:
        - 09:00
        - 21:00
    CashRegisterListItem:
      type: object
      description: "Каса так, як її показує перелік токена: чим її впізнає людина (фіскальний і локальний номер, точка) і чим її адресує машина (id)."
      required:
        - id
        - fiscal_number
        - local_number
        - mode
        - state
      properties:
        id:
          type: string
          format: uuid
          description: Ідентифікатор каси в цьому API — те саме значення, що йде в cash_register_id чека і в {id} решти маршрутів.
        fiscal_number:
          type: string
        local_number:
          type: string
          description: "CASHDESKNUM: власний номер каси у клієнта."
        org_name:
          type: string
        point_name:
          type: string
        point_address:
          type: string
        mode:
          type: string
          enum:
            - production
            - test
        state:
          type: string
          enum:
            - closed
            - opened
            - offline
    Task:
      type: object
      required:
        - task_id
        - type
        - status
        - created_at
      properties:
        task_id:
          type: string
          format: uuid
        type:
          type: string
          enum:
            - receipt
            - open_shift
            - close_shift
            - close_offline_session
            - verify_key
            - sync_register
            - dps_objects
        status:
          type: string
          enum:
            - pending
            - processing
            - succeeded
            - failed
        created_at:
          type: string
          format: date-time
        finished_at:
          type: string
          format: date-time
        error:
          type: string
        fault:
          $ref: "#/components/schemas/Fault"
        result:
          $ref: "#/components/schemas/TaskResult"
    Fault:
      type: object
      description: "Структурований опис збою завдання: стабільний код із простором імен (dps.* — відмовив фіскальний сервер; fiscal.* — валідація документа; signer.* — рівень КЕП; state.* — стан реєстратора), клас реакції, параметри для підстановки, повідомлення мовою з Accept-Language (типово українською), дослівний текст ДПС і автоматичні виправлення, застосовані до завершення завдання."
      required:
        - code
        - class
      properties:
        code:
          type: string
          description: Стабільний ідентифікатор, напр. dps.check_local_number_invalid.
        class:
          type: string
          enum:
            - resync
            - config
            - precursor
            - user_action
            - transient
            - request
            - internal
        params:
          type: object
          additionalProperties: true
        message:
          type: string
          description: Текст для людини, локалізований із каталогу помилок.
        upstream_message:
          type: string
          description: Дослівне повідомлення фіскального сервера.
        remediation:
          type: array
          items:
            type: string
            enum:
              - local_number_synced
              - shift_adopted
              - shift_abandoned
              - zreport_recovered
              - register_config_filled
          description: Автоматичні виправлення, застосовані під час обробки завдання.
    TaskResult:
      type: object
      description: Результати фіскальних операцій (prro_id/shift_id/testing/*) стосуються завдань на чек, зміну та офлайн-сесію; key_verification — єдиний результат завдання verify_key, решти полів у ньому немає.
      properties:
        prro_id:
          type: string
          format: uuid
        shift_id:
          type: string
          format: uuid
        shift_number:
          type: integer
          format: int64
          description: "Наскрізний номер зміни в межах каси. Присвоюється локально: ДПС номера зміни не видає (у квитанції такого поля немає)."
        testing:
          type: boolean
        receipt:
          $ref: "#/components/schemas/DocResult"
        shift_open:
          $ref: "#/components/schemas/DocResult"
        zreport:
          $ref: "#/components/schemas/DocResult"
        shift_close:
          $ref: "#/components/schemas/DocResult"
        offline_end:
          $ref: "#/components/schemas/DocResult"
        key_verification:
          $ref: "#/components/schemas/KeyVerificationResult"
        key_password_check:
          $ref: "#/components/schemas/KeyPasswordCheckResult"
        reconcile:
          $ref: "#/components/schemas/ReconcileReport"
        dps_objects:
          type: array
          items:
            $ref: "#/components/schemas/DPSTaxObject"
    ReconcileReport:
      type: object
      description: "Підсумок звіряння реєстратора з ДПС (sync_register): який стан побачили на боці сервера і що виправили в себе."
      required:
        - fiscal_number
        - dps_shift_opened
        - dps_next_local_number
      properties:
        fiscal_number:
          type: string
        dps_shift_opened:
          type: boolean
        dps_next_local_number:
          type: integer
          format: int64
        dps_testing:
          type: boolean
        counter_old:
          type: integer
          format: int64
        counter_new:
          type: integer
          format: int64
        shift_adopted:
          type: boolean
        shift_abandoned:
          type: boolean
        cash:
          type: object
          description: |-
            Звіряння каси. Присутнє лише тоді, коли на сервері справді триває зміна: скільки готівки ця зміна зрушила за нашим підрахунком — поруч із тією самою величиною, виведеною з підсумків зміни на боці ДПС.

            Це довідка, за нею нічого не виправляється автоматично. Показник ДПС охоплює одну зміну, а cash_balance — усе життя реєстратора, і розбіжність не каже, чия сторона помиляється: документ, який на цьому ПРРО зареєструвало інше програмне забезпечення, тут невидимий, а там порахований. Вирішує людина.
          required:
            - ours
            - dps
            - match
          properties:
            ours:
              $ref: "#/components/schemas/CashAmount"
            dps:
              $ref: "#/components/schemas/CashAmount"
            match:
              type: boolean
        skipped:
          type: string
          enum:
            - offline_session
          description: Чому нічого не чіпали (офлайн-сесію звіряння не торкається).
    DPSTaxObject:
      type: object
      description: Одна господарська одиниця власника ключа (dps_objects).
      required:
        - name
        - tin
        - org_name
        - registrars
      properties:
        name:
          type: string
        address:
          type: string
        tin:
          type: string
        ipn:
          type: string
        org_name:
          type: string
        registrars:
          type: array
          items:
            $ref: "#/components/schemas/DPSRegistrar"
    DPSRegistrar:
      type: object
      description: Один ПРРО, зареєстрований за господарською одиницею.
      required:
        - fiscal_number
        - local_number
      properties:
        fiscal_number:
          type: string
        local_number:
          type: string
        name:
          type: string
        closed:
          type: boolean
    KeyPasswordCheckResult:
      type: object
      description: |-
        Наслідок швидкої локальної перевірки пароля контейнера (TaskVerifyPassword), яку кабінет проганяє синхронно під час завантаження ключа: контейнер лише розшифровується, без CMP і без мережі, щоб хибний пароль повертався формі за мілісекунди, а не аж після повної асинхронної перевірки.

        Завдання ВВАЖАЄТЬСЯ УСПІШНИМ у тому числі при ok=false — воно дійшло до вердикту. Інфраструктурні збої (БД, KEK, слот підпису) вердиктом не є: там завдання повторюється.
      required:
        - signing_key_id
        - ok
      properties:
        signing_key_id:
          type: string
          format: uuid
        ok:
          type: boolean
        verify_code:
          type: string
          description: Присутній завжди при ok=false (напр. wrong_password).
        verify_error:
          type: string
    KeyVerificationResult:
      type: object
      required:
        - signing_key_id
        - status
        - prro_capable
      properties:
        signing_key_id:
          type: string
          format: uuid
          description: Вказує, який ключ перевіряли. Якщо deleted істинне, цей ідентифікатор більше ні на що не вказує — запис видалено.
        status:
          type: string
          enum:
            - active
            - invalid
        verify_code:
          type: string
          enum:
            - wrong_password
            - cert_not_found
            - cert_expired
            - not_prro
            - verify_failed
            - ca_unreachable
        verify_error:
          type: string
        key_type:
          type: string
          enum:
            - individual
            - legal
        prro_capable:
          type: boolean
        cert_subject:
          type: string
        org_name:
          type: string
          description: Назва організації (або ім'я особи) власника з сертифіката.
        edrpou:
          type: string
          description: ЄДРПОУ юридичної особи з сертифіката, лише цифри.
        drfo:
          type: string
          description: РНОКПП (ДРФО) фізичної особи з сертифіката, лише цифри.
        deleted:
          type: boolean
          description: "Запис ключа остаточно видалено (так відбувається при будь-якому відхиленні, крім not_prro): недійсний ключ у базі не лишається."
    DocResult:
      type: object
      required:
        - document_id
        - local_number
        - fiscal_number
      properties:
        document_id:
          type: string
          format: uuid
        local_number:
          type: integer
        fiscal_number:
          type: string
          description: Присвоєний фіскальним сервером ДПС.
        offline:
          type: boolean
          description: Документ підписано в межах офлайн-сесії. Його фіскальний номер обчислено локально (<sid>.<n>.<crc>), і на фіскальному сервері він з'явиться лише після надсилання пакета сесії.
        receipt_url:
          type: string
          format: uri
          description: |-
            Копія чека для покупця на короткому домені сервісу (https://r.prro.cloud/<код>) — те, що інтеграція пересилає листом або перетворює на QR. Сторінка відкривається без авторизації: код і є доступом, як паперовий чек у кишені.

            Формат обирає параметр запиту: ?format=html (типово), text або qr (PNG із адресою цієї ж сторінки).

            Це подання чека, а не доказ фіскалізації: доказ — fiscal_number і посилання tax_url поруч із ним. Присутнє лише в розрахункових документів: тільки вони мають друковану форму.
        tax_url:
          type: string
          format: uri
          description: |-
            Той самий документ у кабінеті платника податків — незалежне підтвердження, яке читач перевіряє, не довіряючи нам.

            Відсутнє, доки документ не потрапив на фіскальний сервер: у офлайн-чека до надсилання пакета сесії є лише локально обчислений номер, і посилання за ним нічого не знайде.
    ReceiptInput:
      description: |
        Чек буває двох форм, які розрізняє поле `type`. Розрахунок за товари несе позиції та оплати; рух готівки — лише суму, і більше нічого. Сам документ саме такий тонкий: заголовок і CHECKTOTAL/SUM.

        Поля чужої форми не ігноруються, а відхиляються з 422. Службовий документ, зібраний із позиціями, був би цілком дійсним чеком, який мовчки викинув ці позиції, — тому сервіс радше відмовить, ніж зареєструє не те, що ви мали на увазі.
      oneOf:
        - $ref: "#/components/schemas/SettlementReceiptInput"
        - $ref: "#/components/schemas/ServiceReceiptInput"
      discriminator:
        propertyName: type
        mapping:
          sale: "#/components/schemas/SettlementReceiptInput"
          return: "#/components/schemas/SettlementReceiptInput"
          storno: "#/components/schemas/SettlementReceiptInput"
          service_in: "#/components/schemas/ServiceReceiptInput"
          service_out: "#/components/schemas/ServiceReceiptInput"
    SettlementReceiptInput:
      type: object
      description: Розрахунок за товари — реалізація, повернення або сторно.
      required:
        - type
        - items
        - payments
      properties:
        cash_register_id:
          type: string
          format: uuid
          description: "Реєстратор. Обов'язковий для POST /api/v1/receipts, де тіло — вся адреса запиту. Кабінетний маршрут бере реєстратор з URL: там поле або відсутнє, або збігається з ним, інакше 422."
        type:
          type: string
          enum:
            - sale
            - return
            - storno
        cashier:
          type: string
        comment:
          type: string
          description: Вільна примітка, яку друкують на самому документі (CHECKHEAD/COMMENT) — наприклад номер замовлення.
        mode:
          type: string
          enum:
            - sync
            - async
          default: sync
          description: Режим доставки результату; параметр запиту ?mode= має перевагу над цим полем.
        original:
          type: object
          description: Зв'язок із початковим чеком; обов'язковий для повернення та сторно.
          required:
            - fiscal_number
          properties:
            fiscal_number:
              type: string
            register_fiscal_number:
              type: string
              description: Для повернень, оформлених на іншому реєстраторі.
            date:
              type: string
              format: date
              description: У форматі РРРР-ММ-ДД.
        items:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/ReceiptItem"
        payments:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/ReceiptPayment"
        rounding_step:
          type: string
          description: Крок заокруглення готівки десятковим рядком ("0.10" за правилами НБУ); без цього поля заокруглення не застосовується.
    ServiceReceiptInput:
      type: object
      description: |
        Готівка, що проходить через касу повз продаж: `service_in` — службове внесення (DOCSUBTYPE 2), `service_out` — службова видача, тобто інкасація (DOCSUBTYPE 4).

        Це так само розрахунковий документ, тож він тягне за собою весь звичайний шлях чека: автоматичне відкриття зміни в керованих режимах, офлайн-сесію з ланцюжком хешів, журнал і тарифікацію як за чек.

        У підсумках зміни він не чіпає ні реалізації, ні повернення: сума накопичується окремо в service_input/service_output і потрапляє до ZREPBODY Z-звіту — так само, як веде свої підсумки ДПС. Залишок готівки реєстратора (cash_balance) він зсуває на всю свою суму.

        Підтип 3 (отримання підкріплення) не підтримується: це валютний документ (DOCTYPE 2) обмінних пунктів і ПТКС.
      required:
        - type
        - sum
      properties:
        cash_register_id:
          type: string
          format: uuid
          description: "Реєстратор. Обов'язковий для POST /api/v1/receipts, де тіло — вся адреса запиту. Кабінетний маршрут бере реєстратор з URL: там поле або відсутнє, або збігається з ним, інакше 422."
        type:
          type: string
          enum:
            - service_in
            - service_out
        cashier:
          type: string
        comment:
          type: string
          description: Вільна примітка, яку друкують на самому документі (CHECKHEAD/COMMENT). Для руху готівки це єдине місце, де сказано, за що гроші — «Інкасація», «Розмінна монета».
        mode:
          type: string
          enum:
            - sync
            - async
          default: sync
          description: Режим доставки результату; параметр запиту ?mode= має перевагу над цим полем.
        sum:
          type: string
          description: "Сума руху — додатний десятковий рядок, до 2 знаків після коми. Це весь документ: підсумовувати тут нічого, позицій немає."
          examples:
            - "1500.00"
    ReceiptItem:
      type: object
      description: |
        Грошові величини — десяткові рядки ("259.90"); кількість допускає до 3 знаків після коми, ціни — до 2.
      required:
        - name
        - quantity
        - price
      properties:
        code:
          type: string
        barcode:
          type: string
        uktzed:
          type: string
        dkpp:
          type: string
          description: Взаємовиключний з uktzed.
        name:
          type: string
        unit_code:
          type: integer
        unit_name:
          type: string
        quantity:
          type: string
          examples:
            - "1"
            - "0.750"
        price:
          type: string
          examples:
            - "259.90"
        tax_letters:
          type: string
          description: Податкові літери з tax_rates реєстратора, напр. "А" або "АГ". Порожнє значення застосовує default_tax_letter реєстратора (а якщо типової літери немає — позиція лишається без податку); "-" примусово робить позицію неоподаткованою попри типову літеру.
        discount:
          type: object
          required:
            - sum
          properties:
            percent:
              type: string
              description: Позначає відсоткову знижку; суму все одно вказують у sum.
            sum:
              type: string
        excise_labels:
          type: array
          items:
            type: string
        comment:
          type: string
    PaymentType:
      type: string
      enum:
        - cash
        - card
        - certificate
        - bank_transfer
        - direct_debit
      description: |
        Форма оплати. Фіскальний код і назву, які потраплять у чек, визначає сервіс: `cash` — 0 ГОТІВКА, `card` — 301 КАРТКА, `certificate` — 305 СЕРТИФІКАТ, `bank_transfer` — 306 ПЕРЕКАЗ З ПОТОЧНОГО РАХУНКУ, `direct_debit` — 100000 ПРЯМИЙ ДЕБЕТ.

        Через касу фізично проходить лише `cash` — саме вона рухає залишок готівки реєстратора. Решта форм на лічильник готівки не впливає.
    ReceiptPayment:
      type: object
      required:
        - type
        - sum
      properties:
        type:
          $ref: "#/components/schemas/PaymentType"
        sum:
          type: string
        provided:
          type: string
          description: Отримано готівкою; решта = provided − sum.
        card:
          type: object
          properties:
            system_name:
              type: string
            acquirer_name:
              type: string
            transaction_date:
              type: string
              format: date-time
            transaction_number:
              type: string
            device_id:
              type: string
            epz_details:
              type: string
              description: Замаскований номер картки.
            auth_code:
              type: string
    WebhookEvent:
      type: string
      enum:
        - task.completed
        - task.failed
        - shift.opened
        - shift.closed
        - key.expiring
        - register.offline
        - register.online
        - register.offline_limit
        - register.remediated
        - register.needs_attention
      description: |
        task.completed і task.failed повідомляють про кожне завершене завдання; shift.opened і shift.closed — похідні події життєвого циклу зміни; key.expiring попереджає про сертифікати ключів, що спливають протягом 30 днів, раз на добу для кожного ключа; register.offline/register.online/register.offline_limit стежать за офлайн-сесіями та законними межами їх тривалості (36 та 168 годин); register.remediated повідомляє про кожне автоматичне усунення розбіжності між реєстратором і ДПС; register.needs_attention спрацьовує, коли виправлення не тримаються (повторювані усунення, зупинене надсилання офлайн-пакетів) і на реєстратор має поглянути людина.
    WebhookEndpoint:
      type: object
      required:
        - id
        - url
        - events
        - active
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
        events:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEvent"
          description: Порожній список означає підписку на всі події.
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    WebhookDelivery:
      type: object
      required:
        - id
        - event
        - status
        - attempts
        - created_at
        - data
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          description: "Тип події доставки. Крім значень WebhookEvent, доставка може нести зарезервовані типи, на які не можна підписатися: webhook.ping (тестова відправка) та system.dps_* (події доступності ДПС)."
        status:
          type: string
          enum:
            - pending
            - delivering
            - delivered
            - failed
            - dead
        attempts:
          type: integer
        cash_register_id:
          type: string
          format: uuid
        data:
          description: Корисне навантаження події (поле data конверта).
        last_error:
          type: string
        next_attempt_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        delivered_at:
          type: string
          format: date-time
    Money:
      type: string
      pattern: ^-?\d+(\.\d{1,4})?$
      description: Копійки десятковим рядком (numeric(14,4)); ніколи не число з рухомою комою.
    CashAmount:
      type: string
      pattern: ^-?\d+\.\d{2}$
      description: Гривні з копійками десятковим рядком — у тій самій формі, що й суми в чеках. На відміну від Money, який рахує копійки і належить тарифікації.
      examples:
        - "1500.00"
        - "-12.30"
    CashBalance:
      type: object
      description: |
        Скільки готівки в касі реєстратора. Це лічильник, а не налаштування і не фіскальна величина: він стартує з нуля при створенні реєстратора і бачить лише ті документи, що пройшли через цей сервіс.

        Рухають його готівкові рядки оплат — додають на реалізації, віднімають на поверненні та сторно — і службові документи. Картка не рухає нічого: через касу фізично нічого не проходить. Решта вже врахована, бо береться `sum` рядка оплати, а не `provided`.

        Лічильник живе на реєстраторі, а не на зміні, бо готівка переживає Z-звіт: торговельний автомат закривається двічі на добу і тримає свої монети, доки по них не приїдуть.

        Значення **може бути від'ємним**, і ніщо цьому не заважає. Гроші лежали в касі ще до того, як сервіс побачив бодай один документ, тож перша інкасація цих грошей — цілком законна операція. Від'ємне значення саме по собі корисний сигнал: у касі було більше, ніж ми знали. Щоб переставити лічильник на справжню суму, скористайтеся PUT .../cash-balance.
      required:
        - amount
      properties:
        amount:
          $ref: "#/components/schemas/CashAmount"
        as_of:
          type: string
          format: date-time
          nullable: true
          description: Коли залишок рухався востаннє. Null означає, що його ще ніщо не рухало, — тоді нуль читається як «ми ще нічого не бачили», а не «каса порожня».
    ShiftTotalsSide:
      type: object
      description: "Один бік підсумків зміни — реалізація або повернення: скільки розрахунків прийнято і на яку суму, з розкладом за засобами оплати."
      required:
        - count
        - sum
      properties:
        count:
          type: integer
          description: Кількість розрахункових документів цього боку.
        sum:
          $ref: "#/components/schemas/CashAmount"
        pay_forms:
          type: array
          description: Накопичено за засобами оплати. code/name — це пара PAYFORMCD / PAYFORMNM, яка піде в Z-звіт; масиву немає, поки бік порожній.
          items:
            type: object
            required:
              - code
              - name
              - sum
            properties:
              code:
                type: integer
              name:
                type: string
              sum:
                $ref: "#/components/schemas/CashAmount"
    ShiftTotals:
      type: object
      description: |
        Поточні підсумки відкритої зміни — те саме накопичення, з якого потім буде побудований Z-звіт. Сервіс складає його чек за чеком у тій самій транзакції, що реєструє документ, тож розійтися з журналом він не може.

        Сторно віднімається з боку реалізації, а не додається до повернень. Службові внесення й видачі не належать до жодного боку — ДПС тримає їх в окремих полях, і Z-звіт робить так само.

        Розклад за податковими літерами тут не віддається навмисно: він найширша частина підсумків і належить Z-звіту, а цей об'єкт їде у відповіді про стан, яку панель опитує кожні кілька секунд.
      required:
        - realiz
        - return
        - service_in
        - service_out
      properties:
        realiz:
          $ref: "#/components/schemas/ShiftTotalsSide"
        return:
          $ref: "#/components/schemas/ShiftTotalsSide"
        service_in:
          $ref: "#/components/schemas/CashAmount"
        service_out:
          $ref: "#/components/schemas/CashAmount"
    CashBalanceDeclaration:
      type: object
      description: "Підсумок заяви про залишок: скільки лічильник показував, скільки показує тепер і поправка між ними. Дивитися варто саме на поправку — це те, наскільки наш підрахунок розійшовся з касою."
      required:
        - amount
        - previous
        - adjustment
      properties:
        amount:
          $ref: "#/components/schemas/CashAmount"
        previous:
          $ref: "#/components/schemas/CashAmount"
        adjustment:
          $ref: "#/components/schemas/CashAmount"
    BillingUsage:
      type: object
      required:
        - balance
        - soft_limit
        - blocked
        - period
        - receipts
        - charged
        - tariff
      properties:
        balance:
          $ref: "#/components/schemas/Money"
        soft_limit:
          allOf:
            - $ref: "#/components/schemas/Money"
          description: |
            Нижня межа балансу: поки balance < soft_limit, чеки заблоковані.
        blocked:
          type: boolean
        period:
          type: object
          required:
            - from
            - to
          properties:
            from:
              type: string
              format: date
            to:
              type: string
              format: date
              description: Не включно.
        receipts:
          type: integer
          format: int64
          description: Кількість тарифікованих чеків за період.
        charged:
          $ref: "#/components/schemas/Money"
        tariff:
          type: object
          required:
            - plan_id
            - price_per_receipt
          properties:
            plan_id:
              type: string
              format: uuid
            price_per_receipt:
              $ref: "#/components/schemas/Money"
    Invoice:
      type: object
      required:
        - id
        - period_start
        - period_end
        - amount
        - currency
        - status
        - created_at
      properties:
        id:
          type: string
          format: uuid
        period_start:
          type: string
          format: date
        period_end:
          type: string
          format: date
          description: Включно.
        amount:
          $ref: "#/components/schemas/Money"
        currency:
          type: string
        status:
          type: string
          enum:
            - draft
            - issued
            - paid
            - overdue
        created_at:
          type: string
          format: date-time
    DPSStatus:
      type: object
      description: |
        Знімок супервізора доступності ДПС плюс агрегати парку. Ефективний режим (effective) — це відповідь, на яку реагує конвеєр.
      required:
        - verdict
        - override
        - effective
        - manual
        - since
        - window
        - flap_count
        - offline_registers
        - oldest_offline_age_seconds
      properties:
        verdict:
          type: string
          enum:
            - online
            - degraded
            - offline
          description: Думка детектора незалежно від override.
        override:
          type: string
          enum:
            - none
            - forced_offline
            - forced_online
          description: Ручний режим оператора.
        effective:
          type: string
          enum:
            - online
            - offline
          description: Підсумковий операційний режим (override переважає вердикт).
        manual:
          type: boolean
          description: Чи діє зараз ручний override.
        since:
          type: string
          format: date-time
        override_reason:
          type: string
        override_by:
          type: string
        override_expires:
          type: string
          format: date-time
          description: Коли TTL override повертає режим у auto (якщо заданий).
        last_probe_at:
          type: string
          format: date-time
        last_ok_at:
          type: string
          format: date-time
        last_error:
          type: string
        flap_count:
          type: integer
          description: Переходи у поточному вікні flap-damping.
        probe_latency_ms:
          type: integer
          format: int64
        clock_drift_ms:
          type: integer
          format: int64
        tunables:
          type: object
          description: |
            Пороги, за якими судять числа вище (те саме, що у DPSPulse): ширина вікна, поріг degraded і крок перевірок.
          properties:
            window_seconds:
              type: integer
              format: int64
            degraded_latency_ms:
              type: integer
              format: int64
            probe_interval_seconds:
              type: integer
              format: int64
        window:
          type: object
          description: Вікно детектора цієї репліки (по реальних викликах ДПС).
          required:
            - samples
            - failures
            - degraded
            - latency_ms
          properties:
            samples:
              type: integer
            failures:
              type: integer
            degraded:
              type: boolean
            latency_ms:
              type: integer
              format: int64
            by_class:
              type: object
              additionalProperties:
                type: integer
        offline_registers:
          type: integer
          description: Кас, що зараз в офлайн-сесії.
        oldest_offline_age_seconds:
          type: integer
          format: int64
