openapi: 3.1.0
info:
  title: MyÚčto.cz REST API
  version: "1.0"
  summary: Veřejné REST API pro automatizaci fakturace
  description: |
    Veřejné API MyÚčto.cz pro integraci s e-shopy, CRM, automatizačními platformami
    (Make, Zapier) a vlastními skripty.

    ## Autentizace

    Použij Personal Access Token (PAT) v hlavičce `Authorization`:

    ```
    Authorization: Bearer mi_pat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ```

    Token vytvoříš v MyÚčto → **Nastavení → API tokeny**. Plain-text se zobrazí
    pouze jednou při vytvoření — ulož si ho. Pokud token ztratíš, odvolej ho a vytvoř
    nový.

    Token má:
    - **scope** — `read` (pouze GET) nebo `read_write` (všechny operace).
    - **supplier** — pokud je token bound na konkrétního supplier-a, všechny operace
      se odehrávají v jeho kontextu. Pokud je `supplier` `null`, klient může
      přepínat přes hlavičku `X-Supplier-Id: <id>`.
    - volitelnou expiraci.

    ## Verzování

    Stabilní veřejná cesta začíná `/api/v1/`. Pokud přidáme nekompatibilní změnu,
    půjde do `/api/v2/` — `/api/v1/` zůstane funkční.

    Každá response vrací hlavičku `X-API-Version: 1`.

    ## Rate limity

    Defaultně **600 requestů / minutu / token**. Při překročení vrátíme `429 Too Many Requests`
    s hlavičkou `Retry-After` (sekundy).

    ## Chyby

    Všechny chyby mají formát:

    ```json
    { "error": { "code": "validation_failed", "message": "..." } }
    ```

  contact:
    name: MyÚčto.cz
    url: https://myucto.cz
  license:
    name: MyÚčto.cz mixed MIT and commercial license

servers:
  - url: /
    description: Aktuální origin
  - url: http://localhost:8080
    description: Docker

security:
  - BearerAuth: []

tags:
  - name: System
    description: Health-check, verze, dokumentace
  - name: Auth
    description: Identita aktuálního klienta
  - name: User
    description: Uživatelský profil a nastavení účtu
  - name: Tokens
    description: Správa API tokenů (jen ze session — webového UI)
  - name: Clients
    description: Klienti / odběratelé
  - name: Invoices
    description: Faktury
  - name: Projects
    description: Zakázky (vázané na klienta, jednotka pro reporting a fakturaci)
  - name: WorkReports
    description: Výkaz víceprací (po hodinách) k faktuře — vytvoření, schválení zákazníkem, PDF
  - name: InvoiceComms
    description: Email faktury, upomínky, archiv vystavených PDF, přílohy
  - name: InvoiceOps
    description: Pokročilé operace s fakturami (storno, klonování, hromadné akce, proforma→final)
  - name: Lookup
    description: ARES (CZ obchodní rejstřík), VIES (EU VAT validation)
  - name: Reports
    description: Dashboard summary, CSV exports, activity log
  - name: Bank
    description: Bankovní výpisy a párování plateb
  - name: Settings
    description: Konfigurace dodavatele a per-supplier overrides codebooku (read-only přes API)
  - name: Codebooks
    description: Číselníky (země, měny, DPH sazby, jednotky)
  - name: Documents
    description: Dokumenty — úložiště souborů, složky, ZFO datové zprávy, fulltext, párování s entitami
  - name: Purchase invoices
    description: Přijaté faktury (náklady) — evidence, položky, DPH odpočet, export, párování záloh
  - name: Recurring
    description: Pravidelné (opakované) fakturace — šablony, jejich rozvrh a generování faktur
  - name: CRM
    description: CRM dashboard — KPI, aging, koncentrace, cash-flow, action-items
  - name: Tax
    description: Daňový optimalizátor (srovnání režimů, predikce ročních limitů)
  - name: Logbook
    description: Kniha jízd — vozidla, jízdy, tankování a daňové souhrny
  - name: Accounting
    description: |
      Podvojné účetnictví (Epic F1) — účtová osnova, účetní období, deník
      (journal) a kontační pravidla. Vše tenant-scoped. Zápisy vyžadují roli
      účetní nebo admin, čtení stačí readonly; změna stavu období je admin-only.
  - name: Stock
    description: |
      Sklad (Epic Stock) — sklady, skladové karty, pohyby a stavy, skladové
      doklady (příjem/výdej/převod), inventury a skladové reporty. Tenant-scoped,
      vyžaduje zapnutý skladový modul u dodavatele.
  - name: Eshop
    description: |
      E-shop / katalog zboží — číselníky (výrobci, štítky, poplatky, parametry),
      strom kategorií (i18n) a karta zboží (média, cenotvorba, dodavatelé, import).
  - name: Portal
    description: Přehledový portál (dashboard souhrn napříč agendami).
  - name: Portfolio
    description: |
      Přehled firem pro účetní kancelář (Fáze F) — cross-supplier agregace přes
      user_suppliers membership; NENÍ scoped na X-Supplier-Id. Role client bez přístupu.
  - name: DocumentRequests
    description: |
      Vyžádání chybějících dokladů od klienta (Fáze F) — účetní pohled (CRUD)
      i klientský portál (výčet vlastních požadavků + upload dokladu).
  - name: TaxReturn
    description: |
      Přiznání k dani z příjmů (DPFO/DPPO, Epic DP) — vstupy, lifecycle
      koncept→finalizace→reopen, EPO XML, přehledy pro OSSZ/zdravotní
      pojišťovnu a zálohy na daň (§38a) vč. rozhodnutí FÚ (§174).

paths:
  /api/v1/branding-profiles:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Aktivní brandingové profily aktuálního dodavatele
      description: "Vrací ID a vizuální údaje použitelné jako `branding_profile_id` při vytvoření faktury. Endpoint je pouze pro čtení."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/BrandingProfile' }

  /api/v1/health:
    get:
      tags: [System]
      summary: Health-check
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [status, version, db, redis, time]
                properties:
                  status: { type: string, enum: [ok] }
                  version: { type: string, example: "4.3.1" }
                  db: { type: boolean }
                  redis: { type: boolean }
                  time: { type: string, format: date-time }
                  warnings:
                    type: array
                    description: Diagnostika dostupná pouze přihlášenému uživateli.
                    items:
                      type: object
                      required: [code, message]
                      properties:
                        code:
                          type: string
                          enum:
                            - secret_encryption_key
                            - webauthn_configuration
                            - mfa_methods_configuration
                            - session_lock_configuration
                            - session_lock_without_unlock_method
                        message: { type: string }

  /api/v1/version:
    get:
      tags: [System]
      summary: Verze aplikace
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string, example: "4.3.1" }

  /api/v1/slug:
    get:
      tags: [System]
      summary: Slug z názvu (bez diakritiky, malá písmena, pomlčky)
      description: |
        Pomocník pro číselníky — z „Název" vyrobí kód (`Oblečení` → `obleceni`).
        Sdílený serverový Slugifier je jediný zdroj pravdy, takže UI preview
        odpovídá tomu, co by se uložilo. Max 50 znaků.
      parameters:
        - { in: query, name: text, required: true, schema: { type: string }, description: Vstupní název }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  slug: { type: string, example: "obleceni" }

  /api/v1/auth/api-me:
    get:
      tags: [Auth]
      summary: Identita aktuálního klienta
      description: |
        Vrátí informace o uživateli a supplier-ovi, pod kterým token / session
        operuje. Slouží jako connection-test pro integrace.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiMe' }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/clients:
    get:
      tags: [Clients]
      summary: Seznam klientů
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - in: query
          name: q
          description: Fulltextové vyhledávání (název firmy / IČO)
          schema: { type: string }
        - in: query
          name: filter[archived]
          description: "1 = jen archivované, 0/chybí = jen aktivní"
          schema: { type: boolean, default: false }
        - in: query
          name: sort
          description: "Řazení (default: name)"
          schema: { type: string, default: name, enum: [name, created_at, -created_at] }
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 5, maximum: 200, default: 50 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Client' }
                  meta:
                    allOf:
                      - $ref: '#/components/schemas/ListMeta'
                      - type: object
                        properties:
                          role_counts:
                            type: object
                            description: "Počty klientů dle role pro tab badge — bez vlivu stránkování a filtru kategorie nákladu"
                            properties:
                              all: { type: integer }
                              customers: { type: integer }
                              vendors: { type: integer }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags: [Clients]
      summary: Vytvoří klienta
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ClientInput' }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Client' }
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '403':
          $ref: '#/components/responses/InsufficientScope'

  /api/v1/clients/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Clients]
      summary: Detail klienta se statistikami
      description: |
        Vrátí klienta + odvozené metriky pro detail stránku:

        - `projects` — seznam zakázek klienta,
        - `invoices_count` — celkový počet faktur,
        - `revenue_by_month` — obrat po měsících za posledních 24 měsíců (VAT-aware),
        - `revenue_by_year` — obrat po letech (VAT-aware),
        - `revenue_by_project` — obrat agregovaný per zakázka pro graf „Obrat podle zakázek"
          (faktury bez `project_id` se agregují pod `project_id: null`),
        - `unpaid_summary` — nezaplaceno / po splatnosti per měna.

        Obraty zahrnují vystavené faktury + dobropisy (`issued`, `sent`,
        `reminded`, `paid`), zálohy a koncepty se nepočítají. Pro plátce DPH
        se používá `total_without_vat`, pro neplátce `total_with_vat`.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ClientDetail' }
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Clients]
      summary: Aktualizace klienta
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ClientInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Client' }
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Clients]
      summary: Smazání klienta (pokud nemá vystavené doklady)
      responses:
        '200':
          description: OK
        '409':
          description: Klient má vystavené doklady, použij archivaci
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/clients/{id}/archive:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Clients]
      summary: Archivuje klienta
      description: |
        Nastaví `archived_at = NOW()`. Archivovaný klient se nezobrazuje
        v defaultním seznamu, jeho historické faktury zůstávají dohledatelné.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Client' }

  /api/v1/clients/{id}/bank-accounts:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Clients]
      summary: Bankovní účty obchodního partnera
      responses:
        '200':
          description: Aktivní účty včetně příznaků původu
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ClientBankAccount' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Clients]
      summary: Ručně přidá bankovní účet obchodního partnera
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [account_number]
              properties:
                account_number: { type: string, maxLength: 40, example: '1000000005/0100' }
                bank_code: { type: [string, "null"], maxLength: 11, example: '0100' }
                iban: { type: [string, "null"], maxLength: 34 }
      responses:
        '201':
          description: Účet přidán nebo znovu aktivován
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ClientBankAccount' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/clients/{id}/bank-accounts/sync-registry:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Clients]
      summary: Synchronizuje účty partnera z registru plátců DPH
      responses:
        '200':
          description: Výsledek synchronizace a aktuální seznam účtů
          content:
            application/json:
              schema:
                type: object
                properties:
                  found: { type: boolean }
                  source: { type: string }
                  synced: { type: integer }
                  accounts:
                    type: array
                    items: { $ref: '#/components/schemas/ClientBankAccount' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/clients/{id}/bank-accounts/{accountId}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: path
        name: accountId
        required: true
        schema: { type: integer, format: int64 }
    delete:
      tags: [Clients]
      summary: Deaktivuje bankovní účet obchodního partnera
      responses:
        '200': { description: Účet deaktivován }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/clients/{id}/unarchive:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Clients]
      summary: Vrátí archivovaného klienta zpět do aktivních
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Client' }

  /api/v1/invoices:
    get:
      tags: [Invoices]
      summary: Seznam faktur (grouped by month)
      description: |
        Vrací faktury aktuálního supplier-a, seskupené po měsících podle
        `COALESCE(tax_date, issue_date)` (DUZP, fallback datum vystavení).
        Filtry se předávají jako `filter[key]=value` (deep object). `status` a `type` mohou
        být čárkou oddělené seznamy (`filter[status]=draft,issued`).

        Stránkuje se po jednotlivých fakturách (`meta.total` = počet faktur);
        měsíční skupiny se skládají až z aktuální stránky, takže skupina na
        hranici stránky může pokračovat na stránce další.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - in: query
          name: q
          description: Fulltextové vyhledávání (varsymbol, klient, popis)
          schema: { type: string }
        - in: query
          name: filter[status]
          description: Stav faktury — lze čárkou oddělit více hodnot
          schema:
            type: string
            example: "draft,issued"
        - in: query
          name: filter[type]
          description: Typ dokladu — lze čárkou oddělit více hodnot
          schema:
            type: string
            example: "invoice,proforma"
        - in: query
          name: filter[client_id]
          schema: { type: integer }
        - in: query
          name: filter[project_id]
          schema: { type: integer }
        - in: query
          name: filter[year]
          description: Rok (dle `COALESCE(tax_date, issue_date)`)
          schema: { type: integer, example: 2026 }
        - in: query
          name: filter[month]
          description: Měsíc 1–12 (vyžaduje filter[year])
          schema: { type: integer, minimum: 1, maximum: 12 }
        - in: query
          name: filter[date_from]
          description: Datum od (YYYY-MM-DD, dle `COALESCE(tax_date, issue_date)`)
          schema: { type: string, format: date }
        - in: query
          name: filter[date_to]
          description: Datum do (YYYY-MM-DD, dle `COALESCE(tax_date, issue_date)`)
          schema: { type: string, format: date }
        - in: query
          name: filter[currency]
          description: Kód měny (CZK, EUR, …)
          schema: { type: string, example: CZK }
        - in: query
          name: filter[unpaid_only]
          description: Pouze nezaplacené (issued + neuhrazené)
          schema: { type: boolean }
        - in: query
          name: filter[overdue]
          description: Pouze po splatnosti
          schema: { type: boolean }
        - in: query
          name: filter[booked]
          description: "Zaúčtování (jen podvojné účetnictví): 1 = zaúčtováno (booked_at není NULL), 0 = nezaúčtováno"
          schema: { type: string, enum: ['1', '0'] }
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 5, maximum: 200, default: 50 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    description: Měsíční skupiny, řazené od nejnovějšího měsíce.
                    items: { $ref: '#/components/schemas/InvoiceMonthGroup' }
                  meta: { $ref: '#/components/schemas/ListMeta' }
    post:
      tags: [Invoices]
      summary: Vytvoří draft faktury
      description: |
        Vytvoří fakturu ve stavu `draft`. Pro vystavení (přidělení čísla, zápis do
        DB účetně) volej `POST /api/v1/invoices/{id}/issue`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InvoiceInput' }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }
        '400':
          $ref: '#/components/responses/ValidationFailed'

  /api/v1/invoices/preview-varsymbol:
    get:
      tags: [Invoices]
      summary: Náhled budoucího čísla faktury
      description: |
        Vrátí, jaké číslo (varsymbol) by faktura dostala při vystavení, podle
        aktuální `invoice_number_template` supplier-a a stávajícího counteru —
        **bez inkrementace**. Slouží pro náhled v editoru.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - in: query
          name: type
          schema:
            type: string
            enum: [invoice, proforma, credit_note]
            default: invoice
        - in: query
          name: issue_date
          description: Datum vystavení (YYYY-MM-DD). Default = dnes.
          schema: { type: string, format: date }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  varsymbol:
                    type: string
                    example: "JD2026-01"
                    description: Prázdný řetězec pokud supplier nemá nastavenou template.
                  has_template: { type: boolean }
        '400':
          description: Chybí zvolený supplier nebo neplatný typ
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/export.pdf:
    get:
      tags: [Invoices]
      summary: Sloučené PDF vybraných faktur
      description: |
        Spojí až 100 vystavených faktur zvolených parametrem `ids` do jednoho
        PDF v uvedeném pořadí. Výstup obsahuje pouze vlastní strany faktur, bez
        příloh, vloženého ISDOC a výkazů práce. Všechny faktury musí patřit
        dodavateli z `X-Supplier-Id`. S `sign_pdf=true` se elektronicky podepíše
        až výsledný celek profilem nastaveným pro PDF faktur.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - in: query
          name: ids
          required: true
          description: ID faktur oddělená čárkou; duplicity se odstraní při zachování pořadí.
          schema: { type: string, pattern: '^[1-9][0-9]*(,[1-9][0-9]*)*$' }
          example: 125,124,119
        - { in: query, name: sign_pdf, schema: { type: boolean, default: false }, description: "Elektronicky podepíše výsledné sloučené PDF." }
      responses:
        '200':
          description: Sloučený PDF soubor (Content-Disposition attachment)
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404':
          description: Některá vybraná faktura neexistuje nebo patří jinému dodavateli
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422':
          description: Překročen limit, doklad není vystavený nebo výsledné PDF nelze podepsat
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/export:
    get:
      tags: [Invoices]
      summary: Hromadný export vystavených faktur za měsíc nebo čtvrtletí
      description: |
        Jeden soubor se všemi vystavenými doklady za období (status
        issued/sent/reminded/paid), supplier scope dle `X-Supplier-Id`:

        - `pdf-zip` — ZIP s jedním PDF na doklad (`Faktura-{vs}.pdf`, dobropisy
          `Dobropis-{vs}.pdf` atd.); s `merge_pdf=true` místo ZIPu vrátí jeden
          sloučený PDF soubor obsahující pouze faktury bez příloh a výkazů práce
        - `isdoc` — ISDOC XML; jeden doklad = samostatný `.isdoc`, víc dokladů = ZIP
        - `pohoda` — Pohoda XML dataPack
        - `stereo` — Stereo (Ježek) XML
        - `money_s3` — Money S3 (Seyfor) XML, formát „Faktury vydané"
          (SeznamFaktVyd/FaktVyd). Bez oficiálního veřejného XSD — struktura
          rekonstruovaná z reálného exportního vzorku. Sazby DPH přebírá z
          řádkových snapshotů dokladu, takže podporuje i historické sazby.
          Money S3 dovoluje nejvýše dvě různé nenulové sazby na jednom dokladu;
          třetí nenulová sazba export zastaví validační chybou.
        - `csv` — tabulkový přehled vydaných faktur v UTF-8

        Období: `month=YYYY-MM`, nebo `period=quarterly&year=YYYY&quarter=1..4`.
        `date_by=issue|tax` řídí zařazení do období (default issue; `tax` = DUZP,
        shodné s výkazy DPH).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf-zip, isdoc, pohoda, stereo, money_s3, csv] } }
        - { in: query, name: month, schema: { type: string, pattern: '^\d{4}-\d{2}$' } }
        - { in: query, name: period, schema: { type: string, enum: [monthly, quarterly] } }
        - { in: query, name: year, schema: { type: integer } }
        - { in: query, name: quarter, schema: { type: integer, minimum: 1, maximum: 4 } }
        - { in: query, name: date_by, schema: { type: string, enum: [issue, tax], default: issue } }
        - { in: query, name: type, schema: { type: string, enum: [invoice, proforma, credit_note, cancellation], description: "Jen jeden typ dokladu (default všechny)" } }
        - { in: query, name: merge_pdf, schema: { type: boolean, default: false }, description: "Jen pro format=pdf-zip. Vrátí jeden sloučený PDF soubor místo ZIP archivu." }
        - { in: query, name: sign_pdf, schema: { type: boolean, default: false }, description: "Jen se současným merge_pdf=true. Elektronicky podepíše výsledný soubor." }
      responses:
        '200':
          description: ZIP / PDF / XML stream (Content-Disposition attachment)
          content:
            application/zip:
              schema: { type: string, format: binary }
            application/pdf:
              schema: { type: string, format: binary }
            application/xml:
              schema: { type: string, format: binary }
            text/csv:
              schema: { type: string, format: binary }
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '404':
          description: Za období nejsou žádné vystavené faktury
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422':
          description: Překročen limit 200 faktur nebo výsledné PDF nelze podepsat
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}/isdoc:
    get:
      tags: [Invoices]
      summary: ISDOC XML jedné vystavené faktury
      description: |
        Strojově čitelná faktura (ISDOC) — symetrie
        s `/api/v1/purchase-invoices/{id}/isdoc`. Koncept (draft) nelze
        exportovat (400) — nemá přidělené číslo. Stornovanou fakturu (409)
        také ne — stejné pravidlo jako u hromadného exportu.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
      responses:
        '200':
          description: ISDOC XML (Content-Disposition attachment, Faktura-{vs}.isdoc)
          content:
            application/xml:
              schema: { type: string, format: binary }
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Stornovanou fakturu nelze exportovat do ISDOC
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Invoices]
      summary: Detail faktury
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Invoices]
      summary: Aktualizace draftu
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InvoiceInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }
        '409':
          description: Faktura už není draft (vystavená/storno) — nelze editovat
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
    delete:
      tags: [Invoices]
      summary: Smazání draftu
      responses:
        '200':
          description: OK
        '409':
          description: Vystavenou fakturu nelze smazat (použij storno)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}/issue:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Invoices]
      summary: Vystaví fakturu (z draftu)
      description: |
        Přidělí číslo dle nastavení supplier-a, archivuje PDF snapshot. Po vystavení
        nelze měnit položky/částky.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }

  /api/v1/invoices/{id}/mark-paid:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Invoices]
      summary: Označí fakturu jako uhrazenou
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                paid_at:
                  type: string
                  format: date
                  description: Datum platby (default = dnes)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }

  /api/v1/invoices/{id}/payments:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Invoices]
      summary: Evidované platby faktury
      description: |
        Seznam plateb (částečných úhrad), přímo spárovaných bankovních operací
        a souhrn faktury nebo zálohové faktury:
        `paid_total`, `remaining` (= `amount_to_pay - paid_total`) a odvozený
        `payment_status` (`unpaid` / `partially_paid` / `paid` / `overpaid`).
        `bank_transactions` je informační vazba a samo o sobě se nezapočítává
        do `paid_total`.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  payments:
                    type: array
                    items: { $ref: '#/components/schemas/InvoicePayment' }
                  bank_transactions:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceRelatedBankTransaction' }
                  paid_total: { type: number, format: float }
                  amount_to_pay: { type: number, format: float }
                  remaining: { type: number, format: float }
                  payment_status:
                    type: [string, "null"]
                    enum: [unpaid, partially_paid, paid, overpaid, null]
    post:
      tags: [Invoices]
      summary: Zaeviduje (částečnou) úhradu faktury
      description: |
        Přidá platbu v měně faktury. Pokud suma plateb pokryje `amount_to_pay`
        (tolerance 0,05), faktura se označí jako zaplacená (`paid_at` = datum
        poslední platby). Stávající `mark-paid` je zkratka „platba na zbytek".
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount:
                  type: number
                  format: float
                  description: Částka platby v měně faktury (> 0)
                paid_on:
                  type: string
                  format: date
                  description: Datum platby (default = dnes)
                variable_symbol: { type: [string, "null"], maxLength: 20 }
                bank_reference: { type: [string, "null"], maxLength: 120 }
                note: { type: [string, "null"], maxLength: 255 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoice: { $ref: '#/components/schemas/Invoice' }
                  payments:
                    type: array
                    items: { $ref: '#/components/schemas/InvoicePayment' }
                  payment: { $ref: '#/components/schemas/InvoicePayment' }
                  became_paid: { type: boolean }
                  remaining: { type: number, format: float }
        '409':
          description: Neplatný stav/částka (draft, storno, dobropis…)

  /api/v1/invoices/{id}/payments/{paymentId}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: path
        name: paymentId
        required: true
        schema: { type: integer, format: int64 }
    delete:
      tags: [Invoices]
      summary: Smaže evidovanou platbu
      description: |
        Platbu navázanou na bankovní transakci nelze smazat (409) — ruší se přes
        unmatch v detailu výpisu. Platbu s vystaveným daňovým dokladem k přijaté
        platbě lze smazat až po smazání/stornu dokladu. Pokud po smazání přestane
        být doklad pokrytý, vrátí se ze stavu `paid` zpět (`sent`/`issued`).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoice: { $ref: '#/components/schemas/Invoice' }
                  payments:
                    type: array
                    items: { $ref: '#/components/schemas/InvoicePayment' }
                  became_unpaid: { type: boolean }
                  remaining: { type: number, format: float }
        '409':
          description: Platba je zamčená (bankovní vazba / daňový doklad)

  /api/v1/invoices/{id}/payments/{paymentId}/tax-document:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: path
        name: paymentId
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Invoices]
      summary: Vystaví daňový doklad k přijaté platbě (DRAFT)
      description: |
        K platbě **zálohové faktury** vystaví koncept daňového dokladu k přijaté
        platbě (`invoice_type = tax_document`, § 28 odst. 2 ZDPH): DUZP = datum
        platby, DPH shora koeficientem (§ 37), částka rozdělená mezi sazby DPH
        proformy poměrně. Jen plátce DPH a ne-reverse-charge plnění. Idempotentní —
        existující nestornovaný doklad k platbě vrátí beze změny.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  tax_document_id: { type: integer, format: int64 }
                  payments:
                    type: array
                    items: { $ref: '#/components/schemas/InvoicePayment' }
        '409':
          description: Podmínky nesplněny (neplátce DPH, reverse charge, není proforma)

  /api/v1/invoices/{id}/pdf:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Invoices]
      summary: PDF faktury
      responses:
        '200':
          description: PDF stream
          content:
            application/pdf:
              schema: { type: string, format: binary }

  /api/v1/invoices/{id}/imported-pdf:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: query
        name: inline
        required: false
        description: "`1` → Content-Disposition: inline (náhled v prohlížeči); jinak attachment (stažení)."
        schema: { type: integer, enum: [1] }
    get:
      tags: [Invoices]
      summary: Zdrojové PDF vydané faktury z importu
      description: |
        Vrátí archivovaný ZDROJOVÝ PDF vydané faktury tak, jak dorazil z importu
        (iDoklad/Fakturoid) — nezaměňovat s naším vlastním rendered PDF
        (`GET /api/v1/invoices/{id}/pdf`). Dostupné jen pro faktury s neprázdným
        `imported_pdf_path`; jinak `404`.
      responses:
        '200':
          description: PDF stream
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '404':
          description: Faktura nenalezena, nemá archivovaný zdrojový PDF, nebo soubor chybí na disku.

  /api/v1/projects:
    get:
      tags: [Projects]
      summary: Seznam zakázek
      description: |
        Vrátí stránkovaný seznam **neArchivovaných** zakázek v rámci aktuálního
        supplier-a. Filtrování přes `filter[status]` a `filter[client_id]`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - in: query
          name: filter[status]
          schema:
            type: string
            enum: [active, paused, closed]
        - in: query
          name: filter[client_id]
          schema: { type: integer, format: int64 }
        - in: query
          name: sort
          schema:
            type: string
            enum: [name, client, revenue, last_activity]
            default: name
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 5, maximum: 200, default: 50 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Project' }
                  meta: { $ref: '#/components/schemas/ProjectsPagination' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags: [Projects]
      summary: Vytvoří zakázku
      description: |
        Klient (`client_id`) musí patřit aktuálnímu supplier-ovi. Currency se
        validuje proti `currencies` skoupeným pod stejným supplier-em.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProjectInput' }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '403':
          $ref: '#/components/responses/InsufficientScope'

  /api/v1/projects/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Projects]
      summary: Detail zakázky se statistikami
      description: |
        Vrátí celý záznam zakázky + odvozené metriky:
        - `invoices_count` — kolik faktur zakázka má,
        - `revenue_by_month` — obrat po měsících za posledních 24 měsíců (per currency),
        - `revenue_by_year` — obrat po letech (per currency),
        - `unpaid_summary` — nezaplaceno / po splatnosti per currency.

        Metriky zahrnují jen vystavené faktury + dobropisy (`issued`, `sent`,
        `reminded`, `paid`); zálohy a koncepty se nepočítají.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProjectDetail' }
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Projects]
      summary: Aktualizace zakázky
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProjectInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Projects]
      summary: Smazání zakázky
      description: |
        Smaže zakázku, pokud nemá vystavené doklady. Pro zakázky s historií
        použij `POST /api/v1/projects/{id}/archive`.
      responses:
        '200':
          description: OK
        '409':
          description: Zakázka má vystavené doklady — použij archivaci
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/projects/{id}/archive:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Projects]
      summary: Archivuje zakázku
      description: |
        Nastaví `archived_at = NOW()`. Archivované zakázky se nezobrazují
        v defaultním výpisu (lze je dohledat přes filtr).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }

  /api/v1/clients/{client_id}/projects:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: client_id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Projects]
      summary: Zakázky daného klienta (vč. archivovaných)
      description: |
        Light-weight výpis bez stránkování — vrátí všechny zakázky klienta včetně
        archivovaných (řazené active → paused → closed → archived). Užitečné pro
        plnění selectů ve frontu integrace.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/ProjectBrief' }
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/projects/stats:
    get:
      tags: [Projects]
      summary: Agregované statistiky zakázek (pro dashboard a stránku Grafy)
      description: |
        Vrací agregace pro vizualizace na stránce **Grafy** (`/stats`):

        - **top_this_year / top_prev_year** — top 12 zakázek + agregovaná položka „Ostatní"
        - **top_12m** — top 12 zakázek za rolling 12 měsíců (smart-mix tabulky v UI;
          v lednu jsou per-rok statistiky téměř prázdné, rolling 12m je robustní)
        - **totals_per_year** — sumy + počty per (year, currency)
        - **status_breakdown** — counts per status (active/paused/closed)
        - **is_vat_payer** — určuje, který sloupec se použil pro agregaci obratu
          (plátce = `total_without_vat`, neplátce = `total_with_vat`)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProjectStats' }

  /api/v1/invoices/{id}/work-report:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        description: ID faktury
        schema: { type: integer, format: int64 }
    get:
      tags: [WorkReports]
      summary: Výkaz víceprací k faktuře
      description: |
        Vrátí výkaz včetně všech `items` (úkony s hodinami a sazbou) nebo
        `null` pokud k faktuře žádný neexistuje.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/WorkReport'
                  - { type: "null" }
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [WorkReports]
      summary: Vytvoří nebo aktualizuje výkaz
      description: |
        Idempotentní upsert (jedna fakturace = max 1 výkaz). Výkaz lze měnit
        pouze pokud faktura má status `draft`. Admin může vynutit edit
        pomocí query parametru `?force=1` (bez ohledu na status).

        Suma `total_hours` a `total_amount` se přepočítá ze `items` na serveru.

        **Pozor:** výkaz víceprací je samostatná entita — ukládá se jen jako
        příloha (2. strana PDF) a **NEmění položky ani částku faktury**. Pokud chceš
        práci i vyúčtovat, přidej odpovídající položku faktury zvlášť přes
        `PUT /api/v1/invoices/{id}` (úprava `items`). Obojí lze dělat jen u draftu.
      parameters:
        - in: query
          name: force
          description: Admin-only override editace u nedraftových faktur
          schema: { type: boolean }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WorkReportInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkReport' }
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '409':
          description: Faktura není draft (a klient není admin s ?force=1)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
    delete:
      tags: [WorkReports]
      summary: Smaže výkaz
      responses:
        '200':
          description: OK
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/invoices/{id}/request-approval:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [WorkReports]
      summary: Pošle výkaz zákazníkovi ke schválení
      description: |
        Pre-conditions:
        - faktura je `draft` a má linknutý work-report,
        - projekt má `requires_work_report_approval = true`.

        Co se stane:
        - vygeneruje se 32-bytový `approval_token`, který dostane zákazník
          v emailu (link `/approval/{token}`),
        - faktura přejde do `approval_status = "requested"` (status zůstává `draft`),
        - alokuje se variabilní symbol a zafixují snapshoty supplier/client/bank
          (faktura se po schválení vystaví bez dalších změn),
        - zákazník dostane email `invoice_approval` s **PDF výkazu** v příloze
          (ne fakturu — tu uvidí až po vystavení),
        - příjemci: kontakty klienta s účelem `approvals` (viz `email_contacts`);
          bez nich `project.billing_emails`, fallback `client.main_email`,
        - audit log: `invoice.approval_requested`.

        Po rozhodnutí zákazníka (přes veřejný link) můžeš stav zjistit
        v `GET /api/v1/invoices/{id}` (pole `approval_*`). Pokud schválí,
        může proběhnout auto-issue + auto-send dle nastavení supplier-a.
      responses:
        '200':
          description: OK — email odeslán
          content:
            application/json:
              schema:
                type: object
                properties:
                  approval_token: { type: string, example: "a1b2…" }
                  approval_status: { type: string, example: requested }
                  recipients:
                    type: array
                    items: { type: string, format: email }
        '409':
          description: Faktura není draft / nemá výkaz / projekt nevyžaduje schválení
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}/approval-status:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    put:
      tags: [WorkReports]
      summary: Ruční update schvalovacího stavu (admin override)
      description: |
        Použij když potřebuješ obejít veřejný link — např. zákazník schválil
        telefonicky a chceš to v systému zaznamenat. Audit log loguje, kdo
        rozhodnutí vložil ručně.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [approval_status]
              properties:
                approval_status:
                  type: string
                  enum: [none, requested, approved, rejected]
                approval_decided_by_email:
                  type: string
                  format: email
                  description: Kdo schválil (pro audit)
                approval_rejection_reason:
                  type: string
                  description: Důvod (pokud `rejected`)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }

  /api/v1/invoices/{id}/public-link:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceComms]
      summary: Web faktura — vrátí (a případně vytvoří) trvalý veřejný odkaz
      description: |
        Trvalý veřejný odkaz `/invoice/{token}` — klient si na něm fakturu
        bez přihlášení prohlédne v HTML a stáhne PDF (obdoba „web faktury"
        ve Fakturoidu). Token se generuje lazy při prvním volání; opakované
        volání vrací tentýž odkaz (idempotentní). Odkaz se také automaticky
        přikládá do e-mailu při odeslání faktury klientovi.

        `public_viewed_at` = poslední zobrazení klientem (anonymní přístup);
        null dokud klient fakturu neotevřel.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, example: "https://faktury.example.cz/invoice/a1b2…" }
                  token: { type: string, description: "48 hex znaků" }
                  public_viewed_at: { type: [string, "null"], format: date-time }
        '409':
          description: Faktura je draft — web faktura je dostupná až po vystavení
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404': { description: "Faktura nenalezena" }

  /api/v1/invoices/{id}/public-link/regenerate:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceComms]
      summary: Web faktura — zneplatní stávající odkaz vygenerováním nového
      description: |
        Revokace veřejného odkazu (např. při úniku URL) — starý token přestane
        okamžitě platit, vrací se nová URL. `public_viewed_at` se resetuje.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string }
                  token: { type: string }
                  public_viewed_at: { type: [string, "null"], example: null }
        '409':
          description: Faktura je draft
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404': { description: "Faktura nenalezena" }

  /api/v1/invoices/{id}/pdfs:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [InvoiceComms]
      summary: Archiv vystavených/odeslaných verzí PDF
      description: |
        Pokaždé, když se faktura vystaví, znovu-vystaví nebo pošle e-mailem,
        se PDF archivuje. Archiv slouží jako důkaz, co přesně zákazník dostal —
        i když se snapshot na faktuře později změnil (přes admin override).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/PdfArchiveEntry' }

  /api/v1/invoices/{id}/pdfs/{archiveId}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: path
        name: archiveId
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [InvoiceComms]
      summary: Stažení archivované verze PDF
      responses:
        '200':
          description: PDF stream
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/invoices/{id}/recipients:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [InvoiceComms]
      summary: Vyřešení e-mailoví příjemci pro odeslání
      description: |
        Vrátí příjemce, na které by se daný typ zprávy odeslal (#86) — jednotný
        `RecipientResolver`: aktivní kontakty klienta s daným účelem
        (`email_contacts`, u upomínek fallback na účel `documents`); bez kontaktů
        dosavadní logika (`main_email` + `project.billing_emails`; u schvalování
        e-maily zakázky NAHRAZUJÍ hlavní e-mail). Kombinaci s e-maily zakázky
        řídí `project.billing_emails_mode`. Read-only — nic neodesílá; hodí se
        pro náhled příjemců před voláním `/send`.
      parameters:
        - in: query
          name: type
          schema: { type: string, enum: [documents, reminders, approvals], default: documents }
          description: Typ zprávy (doklad / upomínka / žádost o schválení výkazu).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  type: { type: string, enum: [documents, reminders, approvals] }
                  to:  { type: array, items: { type: string, format: email } }
                  cc:  { type: array, items: { type: string, format: email } }
                  bcc: { type: array, items: { type: string, format: email } }
                  resolved:
                    type: array
                    description: Provenance každého příjemce (zdroj + účel + popisek).
                    items:
                      type: object
                      properties:
                        email: { type: string, format: email }
                        recipient: { type: string, enum: [to, cc, bcc] }
                        source: { type: string, enum: [contact, project, main_email] }
                        usage: { type: [string, "null"], enum: [communication, documents, reminders, approvals, null] }
                        label: { type: [string, "null"] }
        '404': { description: Faktura nenalezena }

  /api/v1/invoices/{id}/send:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceComms]
      summary: Pošle fakturu emailem klientovi
      description: |
        Vyrenderuje aktuální PDF, archivuje ho a odešle přes SMTP. Faktura musí
        být `issued` nebo navazující stav. Předmět + tělo z šablony `invoice_send`
        (per locale, per supplier override v Email šablonách).

        Body je volitelné — když chybí, příjemce vyřeší jednotný resolver (#86):
        kontakty klienta s účelem `documents` (vč. cc/bcc rolí); bez kontaktů
        `client.main_email` + `project.billing_emails`. Explicitní `to` v body
        je autoritativní (resolver se přeskočí). Response obsahuje
        `resolved_recipients` s provenancí každého příjemce.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                to:
                  type: array
                  items: { type: string, format: email }
                cc:
                  type: array
                  items: { type: string, format: email }
                bcc:
                  type: array
                  items: { type: string, format: email }
                subject:
                  type: string
                  description: Override předmětu emailu
                body:
                  type: string
                  description: Override těla (HTML); proměnné z šablony se nesubstituují
      responses:
        '200':
          description: OK — email odeslán
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  recipients:
                    type: array
                    items: { type: string, format: email }
                  resolved_recipients:
                    type: array
                    description: "Provenance příjemců (#86) — prázdné při explicitním `to` v body."
                    items:
                      type: object
                      properties:
                        email: { type: string, format: email }
                        recipient: { type: string, enum: [to, cc, bcc] }
                        source: { type: string, enum: [contact, project, main_email] }
                        usage: { type: [string, "null"], enum: [communication, documents, reminders, approvals, null] }
                        label: { type: [string, "null"] }
                  archived_pdf_id: { type: integer, format: int64 }
        '409':
          description: Faktura není ve stavu pro odeslání (draft)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}/reminder:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceComms]
      summary: Pošle upomínku po splatnosti
      description: |
        Šablona `invoice_reminder` (per locale, per supplier override). Klient
        nemá vliv na obsah — text je z nastavení.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  reminder_count: { type: integer, description: Kolikátá upomínka }
        '409':
          description: Faktura není po splatnosti / je již zaplacená
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}/attachments:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [InvoiceComms]
      summary: Seznam příloh faktury
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  attachments:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceAttachment' }
    post:
      tags: [InvoiceComms]
      summary: Nahraje přílohu
      description: |
        Multipart upload (max ~10 MB). Příloha se přidá do dalšího emailu faktury.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: Nahráno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InvoiceAttachment' }
        '413':
          description: Soubor je příliš velký
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}/attachments/{attId}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: path
        name: attId
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [InvoiceComms]
      summary: Stažení přílohy
      responses:
        '200':
          description: Soubor (typ podle původního MIME)
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
    delete:
      tags: [InvoiceComms]
      summary: Smaže přílohu
      responses:
        '200':
          description: OK

  /api/v1/clients/lookup-ares:
    post:
      tags: [Lookup]
      summary: Načte firmu z ARES (CZ obchodní rejstřík)
      description: |
        Vrátí strukturované údaje firmy podle IČO. Vhodné jako první krok
        v „create client" flow — pošli IČO, dostaneš název, adresu, DIČ
        a flag plátce DPH, ty pak pošleš do `POST /api/v1/clients`.

        Cache 24h — opakované volání pro stejné IČO nezatíží ARES.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ic]
              properties:
                ic:
                  type: string
                  pattern: '^\d{8}$'
                  example: "27082440"
      responses:
        '200':
          description: "OK (`found: false` pokud IČO neexistuje v ARES)"
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AresLookupResult' }
        '400':
          description: IČO není 8 číslic
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '503':
          description: ARES dočasně nedostupný
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/clients/lookup-vies:
    post:
      tags: [Lookup]
      summary: Validuje EU DIČ přes VIES
      description: |
        Validace DIČ obchodního partnera v EU. Vrátí `valid: true/false`
        a (pokud platné) registrované jméno a adresu. Cache 24h.

        Reverse-charge fakturace pro EU plátce by měla DIČ validovat před
        vystavením — VIES je oficiální zdroj pravdy EK.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [vat_id]
              properties:
                vat_id:
                  type: string
                  description: DIČ ve formátu countryCode + číslo (např. DE123456789)
                  example: "DE123456789"
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ViesLookupResult' }
        '400':
          description: Neplatný formát DIČ
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '503':
          description: VIES dočasně nedostupný
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}/activity:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [InvoiceOps]
      summary: Audit log faktury
      description: |
        Vrátí chronologický log všech akcí, které faktura prošla
        (created, updated, issued, sent, paid, reminded, approval_requested,
        approval_decided, cancelled, ...). Vhodné pro debugging integrace
        a zobrazení timeline v custom UI.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/ActivityLogEntry' }

  /api/v1/invoices/{id}/unmark-paid:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceOps]
      summary: Vrátí fakturu z „paid" zpět do „issued"
      description: "Reverzace omylem označené platby. Audit log: `invoice.unmark_paid`."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }
        '409':
          description: Faktura není ve stavu `paid`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/{id}/penalty/preview:
    get:
      tags: [InvoiceOps]
      summary: Náhled výpočtu úroku z prodlení (NV 351/2013)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: query, name: as_of, schema: { type: string, format: date } }
        - { in: query, name: principal, schema: { type: number }, description: "Override dlužné částky" }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Faktura není v uhraditelném stavu, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/invoices/{id}/penalty:
    post:
      tags: [InvoiceOps]
      summary: Založí penalizační fakturu (úrok z prodlení)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                as_of: { type: string, format: date }
                principal: { type: number }
      responses:
        '201': { description: Založená penalizační faktura, content: { application/json: { schema: { $ref: '#/components/schemas/Invoice' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Faktura není v uhraditelném stavu, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/invoices/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceOps]
      summary: Storno vystavené faktury
      description: |
        Vystavená faktura **nelze smazat** — storno vytvoří záporný protiklad
        (`invoice_type = cancellation`) se stejnou částkou, který původní fakturu
        účetně ruší. Číselná řada je zachovaná (číslo původní faktury nezmizí).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Důvod storna (audit + volitelně do PDF)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  original:     { $ref: '#/components/schemas/Invoice' }
                  cancellation: { $ref: '#/components/schemas/Invoice' }

  /api/v1/invoices/{id}/clone:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceOps]
      summary: Vytvoří kopii faktury jako nový draft
      description: |
        Kopíruje hlavičku + položky + work-report. Nový draft má dnešní
        `issue_date`, prázdný `number`, status `draft`. Vhodné pro periodicky
        opakované faktury (předplatné, paušál).
      responses:
        '201':
          description: Vytvořen nový draft
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }

  /api/v1/invoices/{id}/issue-final:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceOps]
      summary: Vystaví finální fakturu ze zaplacené zálohy (proforma → invoice)
      description: |
        Pouze pro `invoice_type = proforma` ve stavu `paid`. Vytvoří finální
        daňový doklad navázaný na zálohu (`parent_id`). Číslo a snapshoty
        se zafixují stejně jako u běžného vystavení.
      responses:
        '201':
          description: Finální faktura vytvořena
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }
        '409':
          description: Zdrojový doklad není proforma `paid`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/invoices/bulk-reissue:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [InvoiceOps]
      summary: Hromadné znovu-vystavení faktur (admin)
      description: |
        Pro každou fakturu v `ids` re-renderuje PDF a uloží novou verzi do
        archivu. Existující snapshoty zůstávají (jen přibyde nová verze).
        Vhodné po globální změně nastavení supplier-a (logo, adresa apod.).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items: { type: integer, format: int64 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  reissued_count: { type: integer }
                  failed:
                    type: array
                    items:
                      type: object
                      properties:
                        invoice_id: { type: integer }
                        reason: { type: string }

  /api/v1/invoices/bulk-reminder:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [InvoiceOps]
      summary: Hromadné odeslání upomínek
      description: |
        Pošle upomínku pro všechny faktury v `ids` (nebo všechny po splatnosti
        pokud `ids` chybí). Vhodné pro cron job „každý pondělí ráno upomínky".
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  type: array
                  items: { type: integer, format: int64 }
                  description: Konkrétní faktury; pokud chybí, všechny po splatnosti
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  sent_count: { type: integer }
                  skipped_count: { type: integer }
                  failed:
                    type: array
                    items:
                      type: object
                      properties:
                        invoice_id: { type: integer }
                        reason: { type: string }

  /api/v1/invoices/export.csv:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Reports]
      summary: CSV export faktur
      description: |
        Stejné filtry jako `GET /api/v1/invoices`, ale vrátí CSV stream
        místo JSON. Vhodné pro BI / Google Sheets / Excel import.

        Sloupce: `id`, `number`, `client`, `issue_date`, `due_date`, `taxable_date`,
        `currency`, `total_excl_vat`, `total_vat`, `total_incl_vat`, `status`, `paid_at`.
      parameters:
        - in: query
          name: status
          schema:
            type: string
            enum: [draft, issued, sent, reminded, paid, cancelled]
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: client_id
          schema: { type: integer }
      responses:
        '200':
          description: CSV stream
          content:
            text/csv:
              schema: { type: string }

  /api/v1/dashboard/summary:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Reports]
      summary: Dashboard summary — KPI, top klienti, obrat, forecasty, statistiky
      description: |
        Agregovaná data pro Dashboard i stránku Grafy (`/stats`). Endpoint
        sjednocuje desítky agregací do jednoho dotazu (cache je řízena DB
        cache tabulkami `*_revenue_cache`).

        **Obrat (VAT-aware):**
        Pro plátce DPH (`supplier.is_vat_payer = 1`) se ve všech agregacích
        obratu používá `total_without_vat`; pro neplátce `total_with_vat`.
        Vrácený flag `is_vat_payer` to indikuje.

        **Obsah:**
        - **kpi** — obrat per měna (this/prev year + YTD loni) + počty faktur,
          klientů a zakázek per rok + ø doba úhrady + overdue,
        - **overdue / unpaid_upcoming** — tabulky faktur po/před splatnosti,
        - **top_clients_ytd / top_clients_prev_year** — top 12 klientů YTD a loni,
        - **top_clients_12m** — top 12 klientů za rolling 12 měsíců (smart-mix v UI),
        - **revenue_by_month** — 12 + 12 měsíců (rolling + prev_year) per měna,
        - **revenue_by_year** — všechny roky s aktivitou,
        - **rolling_12m** — plovoucí 12měsíční obrat per měna (+ 12m předtím
          pro YoY); relevantní pro DPH limit (2 mil. Kč),
        - **revenue_last_30d** — live obrat za posledních 30 dní per měna,
        - **revenue_forecast** — predikce ročního obratu (medián tří projekcí: run-rate / krátkodobý růst / trend, vč. rozpětí),
        - **cashflow_ytd** — kumulativní inkasované platby letošek vs loni,
        - **cashflow_forecast** — co přiteče z neuhrazených v 30/60/90 dnech,
        - **due_buckets** — splatné dnes / tento týden / tento měsíc,
        - **aging_report** — stáří pohledávek (current/1-30/31-60/61-90/90+),
        - **payment_days_histogram** — distribuce doby úhrady (12m),
        - **vat_breakdown_12m** — rozpad obratu per sazba DPH (jen plátce DPH),
        - **invoice_size_histogram** — distribuce velikosti faktur v CZK ekv.,
        - **active_clients_count / active_recurring_count** — counts,
        - **pending_approvals** — počet žádostí o schválení výkazu,
        - **is_vat_payer / today / year / prev_year** — kontext.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DashboardSummary' }

  /api/v1/bank-statements:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Bank]
      summary: Seznam nahraných bankovních výpisů
      parameters:
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
          description: Stránka (50 výpisů na stránku).
        - in: query
          name: filter
          style: deepObject
          explode: true
          description: "Volitelné filtry hlaviček a transakcí, zadávané jako filter[year], filter[month], filter[account], filter[counterparty_account], filter[vendor_id], filter[amount], filter[posting_status]."
          schema:
            type: object
            properties:
              year:
                type: integer
                description: "Rok dle statement_date."
              month:
                type: integer
                minimum: 1
                maximum: 12
                description: "Měsíc dle statement_date."
              account:
                type: string
                description: "Číslo účtu — porovnává se normalizovaně (padding, lomítko ani kód banky nevadí)."
              counterparty_account:
                type: string
                description: "Částečná normalizovaná shoda čísla protiúčtu nebo IBANu; mezery, pomlčky a lomítka se ignorují."
              client_id:
                type: integer
                format: int64
                description: "Zákazník nebo dodavatel z clients; hledá příchozí i odchozí platby přes spárované vydané/přijaté faktury nebo evidovaný účet partnera."
              amount:
                type: number
                format: float
                description: "Přesná částka transakce bez ohledu na znaménko."
              posting_status:
                type: string
                enum: [unposted]
                description: "Vrátí jen výpisy s alespoň jedním skutečným nezaúčtovaným pohybem; ignorované položky a e-mailová avíza se nepočítají."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/BankStatement' }
                  total:
                    type: integer
                    description: "Celkový počet výpisů odpovídajících filtru."
                  page: { type: integer }
                  limit:
                    type: integer
                    description: "Počet položek na stránku (50)."
                  years:
                    type: array
                    items: { type: integer }
                    description: "Roky s alespoň jedním výpisem (volby filtru; přes celý scope, ne aktuální filtr)."
                  accounts:
                    type: array
                    description: "Konfigurované bankovní účty s alespoň jedním výpisem (volby filtru)."
                    items:
                      type: object
                      properties:
                        account_number: { type: string }
                        bank_code: { type: [string, "null"] }
                        label: { type: [string, "null"] }
                  scan_configured:
                    type: boolean
                    description: "Je nakonfigurované hostitelské adresářové skenování (cfg.bank_import.scan_root)? Interní příznak pro UI tlačítko Skenovat adresář."

  /api/v1/bank-statements/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Bank]
      summary: Detail výpisu vč. stránkovaných transakcí
      parameters:
        - { in: query, name: page, schema: { type: integer, minimum: 1, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 5, maximum: 200, default: 50 } }
        - in: query
          name: status
          schema: { type: string, enum: [unmatched, auto_exact, auto_partial, manual, ignored] }
          description: Filtr stavu párování.
        - in: query
          name: posting_status
          schema: { type: string, enum: [unposted, posted] }
          description: Filtr podle existence aktivního účetního zápisu bankovního pohybu.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankStatementDetail' }
    delete:
      tags: [Bank]
      summary: Smaže výpis vč. transakcí a payment_matches (admin only)
      description: |
        Cascade delete přes FK constraints — smaže transakce výpisu i jejich
        payment_matches záznamy. Status zaplacených faktur se NEzmění (případný
        rollback paid→issued je doménou uživatele přes /unmatch endpoint).

        Vyžaduje roli `admin`. Účetní (`accountant`) může nahrávat a párovat,
        ale ne mazat (forensic integrity uzávěrek DPH/KH).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean, example: true }
        '403': { description: "Forbidden — role není admin" }
        '404': { description: "Výpis nenalezen nebo nepatří current supplier" }

  /api/v1/bank-statements/{id}/download:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Bank]
      summary: Stáhne originální obsah GPC souboru
      description: |
        Vrací bajt-přesný originál uploadovaného GPC souboru (uložený od v4.1.0
        v `bank_statements.file_content`). Pro výpisy importované před v4.1.0
        není soubor k dispozici → 410 Gone.

        Klient si může před voláním zkontrolovat pole `has_file` v `BankStatement`.
      responses:
        '200':
          description: OK — GPC bajty
          headers:
            Content-Disposition:
              schema: { type: string }
              description: 'attachment; filename="<original-filename>"'
          content:
            text/plain:
              schema: { type: string, format: binary }
        '404': { description: "Výpis nenalezen" }
        '410': { description: "Soubor není k dispozici (importováno před v4.1.0)" }

  /api/v1/bank-transactions/{id}/match:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Ručně přiřadí transakci k faktuře (a označí ji paid)
      description: |
        Použij když automatický matching nezachytil platbu (variabilní symbol
        chybí nebo neodpovídá). Označí transakci jako `matched`, fakturu jako
        `paid` s `paid_at = transaction.posted_at`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice_id]
              properties:
                invoice_id: { type: integer, format: int64 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  transaction: { $ref: '#/components/schemas/BankTransaction' }
                  invoice: { $ref: '#/components/schemas/Invoice' }

  /api/v1/bank-transactions/{id}/unmatch:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Odpojí transakci od faktury (a vrátí fakturu z paid → issued)
      responses:
        '200':
          description: OK

  /api/v1/bank-transactions/{id}/ignore:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Označí transakci jako „ignorovat" (nesedí na žádnou fakturu)
      responses:
        '200':
          description: OK
        '409':
          description: Zaúčtovanou transakci nelze ignorovat (posted_transaction_cannot_be_ignored)

  /api/v1/bank-transactions/{id}/post:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Ruční zaúčtování bankovní transakce (MD/D + volitelně založení pravidla)
      description: >
        Automatizace (mini-epic). Zaúčtuje nespárovanou transakci na zvolené účty
        (2 řádky, ABS(amount)). Saldokontní účty na ne-bankovní straně jsou zakázané
        (H2). Volitelný `create_rule` založí pravidlo (vždy mode=suggest) v téže transakci.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [debit_account_code, credit_account_code]
              properties:
                debit_account_code: { type: string }
                credit_account_code: { type: string }
                description: { type: [string, "null"] }
                create_rule: { type: [object, "null"] }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  journal_entry_id: { type: integer }
                  document_no: { type: [string, "null"] }
                  rule_id: { type: [integer, "null"] }
                  similar:
                    type: [object, "null"]
                    properties:
                      count: { type: integer }
                      period_months: { type: integer }
                      last_seen: { type: [string, "null"] }
        '409':
          description: Transakce už je zaúčtovaná (already_posted)
        '422':
          description: Chybná kontace (rule_account_forbidden / account_not_found / foreign_currency …)

  /api/v1/bank-transactions/{id}/unpost:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Zrušení zaúčtování transakce (storno + detach)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  reversed: { type: boolean }
                  reversal_entry_id: { type: integer }
        '404':
          description: K transakci není zaúčtovaný zápis (not_found)
        '409':
          description: Storno do uzavřeného období (period_closed)

  /api/v1/bank-transactions/{id}/ai-suggest:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Ruční AI návrh kontace podle doplňujícího dotazu
      description: Session-only. Dotaz se před odesláním redukuje na povolený účetní slovník; návrh pouze předvyplní MD/D.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query: { type: string, minLength: 3, maxLength: 500 }
      responses:
        '200':
          description: Návrh kontace
          content:
            application/json:
              schema:
                type: object
                properties:
                  suggestion_id: { type: integer, format: int64 }
                  debit_account_code: { type: string }
                  credit_account_code: { type: string }
                  reasoning: { type: string }
                  confidence: { type: number, minimum: 0, maximum: 0.4 }
        '404': { description: Bankovní transakce neexistuje v aktuální firmě }
        '409': { description: "AI je vypnutá, pozastavená, bez DPA nebo v konfliktu rezidence" }
        '422': { description: Neplatná délka dotazu nebo AI navrhla neplatný pár účtů (invalid_accounts) }

  /api/v1/bank-ai-suggestion-availability:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Bank]
      summary: Dostupnost ručního AI dotazu pro bankovní kontaci
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [available]
                properties:
                  available: { type: boolean }
        '403': { description: Chybí oprávnění bank.post }

  /api/v1/accounting/bank-posting-rules:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Accounting]
      summary: Seznam pravidel účtování opakovaných transakcí
      parameters:
        - in: query
          name: direction
          schema: { type: string, enum: [incoming, outgoing] }
        - in: query
          name: active
          schema: { type: boolean }
        - { in: query, name: page, schema: { type: integer, minimum: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankPostingRulePage' }
    post:
      tags: [Accounting]
      summary: Vytvoří pravidlo (mode vždy suggest); volitelně backfill návrhů
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BankPostingRuleWrite' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                required: [rule]
                properties:
                  rule: { $ref: '#/components/schemas/BankPostingRule' }
                  backfilled: { type: [integer, "null"] }
        '422': { description: rule_criteria_missing / rule_account_forbidden / account_not_found }

  /api/v1/accounting/bank-posting-rules/dry-run:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Accounting]
      summary: Test pravidla nad historií (read-only) — počet shod + vzorek
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BankPostingRuleWrite' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankPostingRuleDryRun' }

  /api/v1/accounting/bank-rule-templates:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Accounting]
      summary: Globální šablony bankovních pravidel s rozřešenými identifikátory firmy
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/BankRuleTemplate' }

  /api/v1/accounting/bank-rule-templates/{key}/instantiate:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: key
        required: true
        schema: { type: string }
    post:
      tags: [Accounting]
      summary: Vytvoří tenantové pravidlo ze šablony, vždy v režimu suggest
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                variable_symbol: { type: [string, "null"] }
                amount_min: { type: [number, "null"], minimum: 0 }
                amount_max: { type: [number, "null"], minimum: 0 }
                auto_amount_cap: { type: [number, "null"], minimum: 0 }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                required: [rule]
                properties:
                  rule: { $ref: '#/components/schemas/BankPostingRule' }
        '404': { description: template_not_found }
        '409': { description: template_already_instantiated }
        '422': { description: placeholder_missing / rule_account_forbidden }

  /api/v1/accounting/auto-posting-policy:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Accounting]
      summary: Efektivní policy automatiky účtování firmy
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutoPostingPolicy' }
    put:
      tags: [Accounting]
      summary: Nastaví preset, denní limit, digest a úrovně jednotlivých operací
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AutoPostingPolicyWrite' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutoPostingPolicy' }
        '422': { description: invalid_operation_type / invalid_level / ai_auto_forbidden }

  /api/v1/accounting/bank-posting-rules/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    put:
      tags: [Accounting]
      summary: Úprava kritérií a aktivního stavu pravidla; režim mění promote/demote
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BankPostingRuleWrite' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankPostingRule' }
        '409': { description: rule_promotion_required }
        '422': { description: rule_account_forbidden }
    delete:
      tags: [Accounting]
      summary: Smazání pravidla (suggestions FK SET NULL)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [items, total, page, per_page]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/BankPostingSuggestion' }
                  total: { type: integer }
                  page: { type: integer }
                  per_page: { type: integer }

  /api/v1/accounting/bank-posting-rules/{id}/promote:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    post:
      tags: [Accounting]
      summary: Povýší prověřené návrhové pravidlo na plnou automatiku
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { rule: { $ref: '#/components/schemas/BankPostingRule' } }
        '404': { description: not_found }
        '409': { description: rule_not_suggest / rule_not_ready }
        '422': { description: amount_band_required }

  /api/v1/accounting/bank-posting-rules/{id}/demote:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    post:
      tags: [Accounting]
      summary: Vrátí automatické pravidlo do režimu návrhů
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { rule: { $ref: '#/components/schemas/BankPostingRule' } }
        '404': { description: not_found }
        '409': { description: rule_not_auto }

  /api/v1/accounting/bank-posting-rules/{id}/backfill:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    post:
      tags: [Accounting]
      summary: Použije konkrétní aktivní pravidlo na historii a vytvoří pouze návrhy
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [backfilled]
                properties:
                  backfilled: { type: integer, minimum: 0 }
        '404': { description: not_found }
        '409': { description: rule_inactive }

  /api/v1/accounting/bank-posting-rules/{id}/history:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [Accounting]
      summary: Auditní historie pravidla a jeho korekcí
      parameters:
        - { in: query, name: page, schema: { type: integer, minimum: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankPostingRuleHistory' }
        '404': { description: not_found }

  /api/v1/accounting/bank-posting-suggestions:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Accounting]
      summary: Fronta návrhů zaúčtování + protokol automatiky
      parameters:
        - in: query
          name: status
          schema: { type: string, enum: [pending, needs_input, blocked, approved, rejected, auto_posted, superseded] }
        - in: query
          name: account
          schema: { type: string }
        - { in: query, name: page, schema: { type: integer, minimum: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankPostingSuggestionPage' }

  /api/v1/accounting/bank-posting-unposted:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Accounting]
      summary: Všechny skutečné bankovní pohyby bez aktivního účetního zápisu
      description: Zahrnuje i pohyby, pro které nevznikl návrh kontace; ignorované položky a e-mailová avíza vynechává.
      parameters:
        - { in: query, name: page, schema: { type: integer, minimum: 1, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
        - { in: query, name: scope, schema: { type: string, enum: [unposted, all] }, description: "'all' = záložka „Všechny pohyby\" (i zaúčtované, napříč účty)." }
        - { in: query, name: year, schema: { type: integer } }
        - { in: query, name: q, schema: { type: string }, description: "Fulltext přes protistranu, VS, popis, číslo účtu a částku." }
        - { in: query, name: account, schema: { type: string }, description: Náš zdrojový účet výpisu (account_number). }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/BankTransaction'
                        - type: object
                          properties:
                            period_closed: { type: boolean, description: Datum pohybu nemá otevřené období nebo spadá do účetního zámku. }
                            account_number: { type: string, description: Náš zdrojový účet výpisu. }
                            bank_code: { type: [string, "null"] }
                            account_label: { type: [string, "null"] }
                  total: { type: integer }
                  page: { type: integer }
                  per_page: { type: integer }
                  scope: { type: string, enum: [unposted, all] }
                  years: { type: array, items: { type: integer } }
                  accounts:
                    type: array
                    items:
                      type: object
                      properties:
                        account_number: { type: string }
                        bank_code: { type: [string, "null"] }
                        label: { type: [string, "null"] }

  /api/v1/accounting/bank-posting-unposted/count:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Accounting]
      summary: Počet skutečných bankovních pohybů bez aktivního účetního zápisu
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  unposted: { type: integer }

  /api/v1/accounting/bank-posting-suggestions/count:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Accounting]
      summary: Počty front pending a needs_input (blocked se započítá do needs_input)
      responses:
        '200': { description: OK }

  /api/v1/accounting/bank-posting-suggestions/bulk-approve:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Accounting]
      summary: Hromadné schválení návrhů (per-item výsledek)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids: { type: array, maxItems: 200, items: { type: integer, format: int64 } }
      responses:
        '200':
          description: Výsledek včetně batch_id pro hromadné storno.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankPostingBulkResult' }

  /api/v1/accounting/bank-posting-suggestions/bulk-preview:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Accounting]
      summary: Náhled účetního dopadu hromadného schválení bez zápisu
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids: { type: array, maxItems: 200, items: { type: integer, format: int64 } }
      responses:
        '200':
          description: Položky, agregované obraty účtů a per-item chyby.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankPostingBulkPreview' }

  /api/v1/accounting/bank-posting-suggestions/bulk-reject:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Accounting]
      summary: Hromadné odmítnutí návrhů se strukturovaným důvodem
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids, reason]
              properties:
                ids: { type: array, maxItems: 200, items: { type: integer, format: int64 } }
                reason: { type: string, enum: [wrong_account, not_ours, duplicate, other] }
      responses:
        '200': { description: Počet odmítnutých položek a per-item chyby. }

  /api/v1/accounting/bank-posting-suggestions/batches/{batchId}/undo:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: batchId, required: true, schema: { type: string, pattern: '^[a-f0-9]{32}$' } }
    post:
      tags: [Accounting]
      summary: Atomické storno všech aktivních zápisů jedné schválené dávky
      responses:
        '200': { description: Počet vytvořených storen a jejich ID. }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Některé původní období je uzavřené nebo zamčené; dávka se nezmění. }

  /api/v1/accounting/bank-posting-suggestions/{id}/approve:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Accounting]
      summary: Schválení návrhu (volitelný override kontace)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                debit_account_code: { type: string }
                credit_account_code: { type: string }
                selected_rule_id: { type: integer, minimum: 1, description: Aktivní odpovídající pravidlo pro vyřešení rule_conflict. }
      responses:
        '200': { description: OK }
        '409': { description: suggestion_not_pending / already_posted / period_closed }
        '422': { description: Neplatný override nebo selected_rule_id. }

  /api/v1/accounting/bank-posting-suggestions/{id}/reject:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Accounting]
      summary: Odmítnutí návrhu (streak / auto-deaktivace pravidla)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                note: { type: string, enum: [wrong_account, not_ours, duplicate, other] }
      responses:
        '200': { description: OK }

  /api/v1/accounting/bank-posting-suggestions/{id}/snooze:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    post:
      tags: [Accounting]
      summary: Odložení návrhu nebo návrat do běžné fronty
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                until: { type: [string, "null"], format: date }
                reason: { type: [string, "null"], enum: [later, waiting_document, needs_review, null] }
      responses:
        '200': { description: Aktuální odložení položky. }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/manual-posting-queue:
    get:
      tags: [Accounting]
      summary: Jednotná fronta ručního doúčtování (Featura H, read-only agregace)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: type, schema: { type: string } }
        - { in: query, name: reason, schema: { type: string } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50 } }
      responses:
        '200': { description: "OK {items, pagination}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/automation/feed:
    get:
      tags: [Accounting]
      summary: Sjednocený kokpit účetní automatiky napříč povolenými firmami
      description: Session-only. Výsledek je vždy omezen dynamickým RBAC uživatele.
      parameters:
        - { in: query, name: tab, required: true, schema: { type: string, enum: [auto, pending, needs_input] } }
        - { in: query, name: suppliers, schema: { type: string }, description: Čárkou oddělená ID firem. }
        - { in: query, name: source, schema: { type: string, enum: [rule, learned, payment_match, transfer, detector, schedule, knn, llm, ai, document] } }
        - { in: query, name: operation_type, schema: { type: string } }
        - { in: query, name: min_confidence, schema: { type: number, minimum: 0, maximum: 1 } }
        - { in: query, name: max_confidence, schema: { type: number, minimum: 0, maximum: 1 } }
        - { in: query, name: min_amount, schema: { type: number, minimum: 0 } }
        - { in: query, name: max_amount, schema: { type: number, minimum: 0 } }
        - { in: query, name: sort, schema: { type: string, enum: [default, date, confidence, amount, operation_type, source] } }
        - { in: query, name: direction, schema: { type: string, enum: [asc, desc] } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: page, schema: { type: integer, minimum: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 200 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutomationFeedPage' }

  /api/v1/automation/counts:
    get:
      tags: [Accounting]
      summary: Počty položek kokpitu celkem a po firmách
      description: Session-only, dynamický RBAC.
      parameters:
        - { in: query, name: suppliers, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutomationCounts' }

  /api/v1/automation/stats:
    get:
      tags: [Accounting]
      summary: Míra automatiky, trend, důvody zásahů a odhad úspory času
      description: Session-only, jedna firma s oprávněním accounting.read.
      parameters:
        - { in: query, name: supplier_id, required: true, schema: { type: integer } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutomationStats' }

  /api/v1/automation/overview:
    get:
      tags: [Accounting]
      summary: Přehled detektorů, pravidel, učení, AI a politiky automatiky
      description: Session-only.
      parameters:
        - { in: query, name: supplier_id, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /api/v1/automation/checklist:
    get:
      tags: [Accounting]
      summary: Denní, měsíční nebo DPH checklist automatiky
      description: Session-only.
      parameters:
        - { in: query, name: scope, schema: { type: string, enum: [daily, month_end, vat_return] } }
        - { in: query, name: supplier_id, schema: { type: integer } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
      responses:
        '200': { description: OK }

  /api/v1/automation/history:
    get:
      tags: [Accounting]
      summary: Auditní historie rozhodnutí automatiky
      description: Session-only, dynamický RBAC.
      parameters:
        - { in: query, name: suppliers, schema: { type: string } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: source, schema: { type: string } }
        - { in: query, name: page, schema: { type: integer, minimum: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 200 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutomationHistoryPage' }

  /api/v1/automation/wizard/analysis:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Accounting]
      summary: Analýza opakovaných nezaúčtovaných plateb pro průvodce
      description: Session-only.
      responses:
        '200': { description: OK }

  /api/v1/automation/wizard/apply:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Accounting]
      summary: Vytvoření pravidel v režimu návrhů a bezpečný backfill
      description: Session-only, vyžaduje bank.rules.write. Uzavřená období se přeskakují.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rules]
              properties:
                rules: { type: array, maxItems: 50, items: { $ref: '#/components/schemas/BankPostingRuleWrite' } }
                backfill: { type: boolean, default: true }
      responses:
        '200': { description: OK }

  /api/v1/settings/mode-switch-preview:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Náhled dopadu přepnutí režimu účetnictví (daňová evidence ↔ podvojné)
      description: Počty historických dokladů čekajících na doúčtování po přepnutí.
      responses:
        '200': { description: "OK {cash_documents, invoices, purchase_invoices, bank_transactions, total}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/settings/accounting-activation/status:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Stav aktivace podvojného účetnictví
      responses:
        '200': { description: "OK {activation_status, accounting_mode, starts_on, pending, opening, locked_until, active_job, last_job}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/settings/accounting-activation/opening:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Otevírací rozvaha (koncept aktivace podvojného účetnictví)
      responses:
        '200': { description: "OK {rows, totals}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/settings/accounting-activation/jobs:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Stránkovaná historie protokolů aktivace podvojného účetnictví
      description: Session-only, vyžaduje správu účetních období aktuální firmy.
      parameters:
        - { in: query, name: page, schema: { type: integer, minimum: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountingBackfillJobPage' }

  /api/v1/settings/accounting-activation/jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [Settings]
      summary: Stav konkrétního jobu aktivace podvojného účetnictví (polling)
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/settings/ai-assist:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Stav volitelné AI asistence účtování
      description: Session-only. Vrací opt-in, rozsah, DPA potvrzení, stav kNN a kill-switchů aktuální firmy.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AiAssistSettings' }
    put:
      tags: [Settings]
      summary: Nastavení AI asistence účtování
      description: Session-only, vyžaduje settings.ai_provider.write. Zapnutí bez DPA potvrzení selže fail-closed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled: { type: boolean }
                scope:
                  type: array
                  uniqueItems: true
                  items: { type: string, enum: [bank_tx, purchase_invoices] }
                dpa_confirm: { type: string, enum: [anthropic, azure_openai, openai, gemini] }
                dpa_revoke: { type: string, enum: [anthropic, azure_openai, openai, gemini] }
                unmute_source: { type: string, enum: [knn, llm] }
      responses:
        '200':
          description: Aktualizovaný stav
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AiAssistSettings' }
        '409':
          description: Chybí DPA potvrzení nebo se nepodařilo vytvořit pseudonymizační klíč

  /api/v1/ai/suggestions/{id}/accept:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Accounting]
      summary: Jednotlivé potvrzení AI návrhu kontace
      description: Session-only. AI návrhy nelze potvrdit hromadně a samy nikdy neúčtují.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                override:
                  type: object
                  properties:
                    debit_account_code: { type: string }
                    credit_account_code: { type: string }
                    expense_category_id: { type: [integer, "null"] }
      responses:
        '200': { description: Návrh přijat }
        '404': { description: Návrh neexistuje v aktuální firmě }
        '409': { description: Návrh už není pending }
        '422': { description: Neplatný nebo zakázaný účet }

  /api/v1/ai/suggestions/{id}/reject:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Accounting]
      summary: Odmítnutí AI návrhu kontace
      description: Session-only.
      responses:
        '200': { description: Návrh odmítnut }
        '404': { description: Návrh neexistuje v aktuální firmě }
        '409': { description: Návrh už není pending }

  /api/v1/settings/supplier:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Detail aktuálního dodavatele
      description: |
        Vrátí kompletní data supplier-a v jehož scope token operuje — adresa,
        IČO/DIČ, bankovní účty, číslování faktur, default platební podmínky.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplierFull' }
    put:
      tags: [Settings]
      summary: Úprava aktuálního dodavatele
      description: |
        Částečný update supplier-a v jehož scope token operuje — pošli jen pole,
        která chceš změnit. Vyžaduje token se scope `read_write` patřící adminovi.

        Zahrnuje i **číslování dokladů** (`invoice_number_format`,
        `proforma_number_format`, `credit_note_number_format`,
        `purchase_invoice_number_format`, `invoice_number_period`) a **branding
        PDF/e-mailů** (`email_branding_enabled`, `email_accent_color`,
        `pdf_logo_show_name`, `display_name`, `tagline`). Brandingová pole jsou
        původním nastavením dodavatele. Logo se nahrává zvlášť
        (není součástí tohoto endpointu).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SupplierUpdate' }
      responses:
        '200':
          description: Aktualizovaný supplier
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplierFull' }
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '403':
          description: Uživatel není admin nebo token nemá scope `read_write`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/settings/supplier/invoice-counter:
    put:
      tags: [Settings]
      summary: Nastavení counteru číselné řady dokladů
      description: |
        Nastaví supplier-wide counter tak, aby **příští vystavený doklad** daného
        typu dostal číslo `next_number`. `date` určuje, do kterého období (dle
        `invoice_number_period` supplier-a) se counter zapíše — default dnes.

        Na rozdíl od samoopravné synchronizace umí counter i **snížit**; pokud by
        nové číslo kolidovalo s už vystaveným dokladem, vystavení se automaticky
        posune na první volné číslo (číslo se nikdy nezduplikuje).

        Typický use-case: převzetí existující číselné řady při migraci z jiného
        fakturačního software nebo napojení externího systému.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [next_number]
              properties:
                type:
                  type: string
                  enum: [invoice, proforma, credit_note]
                  default: invoice
                next_number:
                  type: integer
                  minimum: 1
                  maximum: 999999999
                  example: 42
                  description: Číslo, které dostane příští vystavený doklad.
                date:
                  type: string
                  format: date
                  description: Datum určující období řady (YYYY-MM-DD). Default = dnes.
      responses:
        '200':
          description: Counter nastaven
          content:
            application/json:
              schema:
                type: object
                properties:
                  type: { type: string, example: invoice }
                  next_number: { type: integer, example: 42 }
                  counter:
                    type: integer
                    example: 41
                    description: Uložená hodnota `last_number` (= next_number - 1).
                  period:
                    type: string
                    example: "202607"
                    description: Klíč období counteru (YYYY / YYYYMM / ALL).
                  preview:
                    type: string
                    example: "2607042"
                    description: Jak bude vypadat příští vygenerované číslo.
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '403':
          description: Uživatel není admin nebo token nemá scope `read_write`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/settings/supplier/logo:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Settings]
      summary: Nahrání loga dodavatele
      description: |
        Multipart upload loga (pole `file`). Formáty PNG / JPG / SVG / WebP,
        max **1 MiB**. Rastr se zmenší na max 240×800 px; SVG se sanitizuje
        a rasterizuje (vektorová kopie se zachová pro ostré PDF). Logo se
        v e-mailech a PDF fakturách zobrazuje jen při
        `email_branding_enabled = true` (nastavíš přes
        `PUT /api/v1/settings/supplier`).

        `logo_path` NELZE měnit přes mass-assign na supplier update — vždy jen
        tímto endpointem (bezpečnost).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: PNG / JPG / SVG / WebP, max 1 MiB
      responses:
        '200':
          description: Logo uloženo
          content:
            application/json:
              schema:
                type: object
                properties:
                  logo_path: { type: string, example: "storage/supplier-logos/sup-1.png" }
                  width: { type: integer, example: 480 }
                  height: { type: integer, example: 160 }
        '400':
          description: Chybí soubor / nepodporovaný formát / konverze selhala
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '403':
          description: Uživatel není admin nebo token nemá scope `read_write`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '413':
          description: Soubor přes 1 MiB
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
    delete:
      tags: [Settings]
      summary: Odebrání loga dodavatele
      responses:
        '200':
          description: Logo odstraněno
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean, example: true }
        '403':
          description: Uživatel není admin nebo token nemá scope `read_write`
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/suppliers:
    get:
      tags: [Settings]
      summary: Seznam dodavatelů, ke kterým má token přístup
      description: |
        Užitečné pro klienta, který chce zjistit `id` supplier-ů (pro X-Supplier-Id
        header u tokenu bez bind, nebo pro multi-supplier reporting).

        **Membership (Epic F0):** pokud má uživatel v `user_suppliers` alespoň
        jedno přiřazení, vrací se JEN přiřazené firmy. Uživatel bez přiřazení
        (typicky vlastník single-tenant instalace) vidí všechny firmy — zpětně
        kompatibilní chování. Požadavek s `X-Supplier-Id` mimo membership vrací
        `403` s kódem `forbidden_supplier`.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/SupplierBrief' }

  /api/v1/suppliers/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    get:
      tags: [Settings]
      summary: Detail konkrétního dodavatele
      description: |
        Membership (Epic F0): firma mimo `user_suppliers` přiřazení uživatele
        vrací `404` (konvence pro cizí entity).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplierFull' }

  /api/v1/admin/users/{id}/suppliers:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
        description: ID uživatele (users.id)
    get:
      tags: [Settings]
      summary: Přiřazení uživatele k firmám (membership, Epic F0)
      description: |
        **Session-only, role `admin`** — přes bearer token nedostupné
        (`/api/admin/*` je mimo veřejný API subset).

        Vrací sadu přiřazení z `user_suppliers`. Prázdné pole = uživatel bez
        omezení (vidí všechny firmy — zpětně kompatibilní režim).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/UserSupplierAssignment' }
        '403': { description: "Forbidden — role není admin (nebo bearer token)" }
        '404': { description: Uživatel nenalezen }
    put:
      tags: [Settings]
      summary: Nahradí sadu přiřazení uživatele k firmám (Epic F0)
      description: |
        **Session-only, role `admin`.** Replace-all sémantika: poslaná sada
        NAHRAZUJE všechna dosavadní přiřazení. Prázdné `assignments` = odebrat
        všechna omezení. `role: null` = zdědit globální `users.role`;
        jinak per-supplier override (`admin`/`accountant`/`readonly`).
        Změna se zapisuje do activity logu (`user.suppliers_updated`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [assignments]
              properties:
                assignments:
                  type: array
                  items:
                    type: object
                    required: [supplier_id]
                    properties:
                      supplier_id: { type: integer }
                      role:
                        type: [string, "null"]
                        enum: [admin, accountant, readonly, null]
                        description: "NULL = zdědit globální users.role"
      responses:
        '200':
          description: Nová sada přiřazení
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/UserSupplierAssignment' }
        '400': { description: "Validace — neexistující supplier_id, duplicitní položka, neplatná role" }
        '403': { description: "Forbidden — role není admin (nebo bearer token)" }
        '404': { description: Uživatel nenalezen }

  /api/v1/admin/bank-rule-templates:
    get:
      tags: [Accounting]
      summary: Globální katalog šablon bankovních pravidel pro administraci
      description: "Session-only, superadmin. Vrací šablony, povolené typy operací a globální předkontace."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminBankRuleTemplateCatalog' }
        '403': { description: Pouze superadmin }
    post:
      tags: [Accounting]
      summary: Vytvoří globální šablonu bankovního pravidla
      description: "Session-only, superadmin. Šablona se po aktivaci nabízí všem firmám."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AdminBankRuleTemplateWrite' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminBankRuleTemplate' }
        '409': { description: template_key_taken }
        '422': { description: validation_failed / posting_rule_not_found / rule_account_forbidden }

  /api/v1/admin/bank-rule-templates/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    put:
      tags: [Accounting]
      summary: Upraví globální šablonu bankovního pravidla
      description: "Session-only, superadmin. template_key je po vytvoření neměnný."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AdminBankRuleTemplateWrite' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminBankRuleTemplate' }
        '404': { description: template_not_found }
        '409': { description: template_key_immutable }
        '422': { description: validation_failed / posting_rule_not_found / rule_account_forbidden }
    delete:
      tags: [Accounting]
      summary: Smaže nepoužitou globální šablonu bankovního pravidla
      description: "Session-only, superadmin. Použitou šablonu je nutné místo smazání deaktivovat."
      responses:
        '200': { description: Deleted }
        '404': { description: template_not_found }
        '409': { description: template_in_use }
  /api/v1/settings/currencies:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Měny nakonfigurované pro aktuálního dodavatele
      description: |
        Per-supplier override hlavního codebooku — obsahuje i bank-accounts
        per currency. Mutace (POST/PUT/DELETE) jsou jen přes UI, ne přes API.
      responses:
        '200':
          description: OK

  /api/v1/settings/vat-rates:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: DPH sazby aktuálního dodavatele
      responses:
        '200':
          description: OK

  /api/v1/settings/units:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Měrné jednotky aktuálního dodavatele
      responses:
        '200':
          description: OK

  /api/v1/settings/countries:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Settings]
      summary: Země nakonfigurované pro aktuálního dodavatele
      responses:
        '200':
          description: OK

  /api/v1/bank-statements/upload:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Bank]
      summary: Nahraje bankovní výpis (GPC/CSV/ABO)
      description: |
        Multipart upload. Server detekuje formát z obsahu a parsuje transakce.
        Auto-matching proti otevřeným fakturám se spustí po `POST /scan`.

        GPC/ABO hlavička neobsahuje měnu. Pokud jedno číslo účtu evidujete ve více
        měnách (např. Raiffeisenbank: CZK/EUR/USD = jedno číslo), je nutné zvolit
        cílový měnový účet přes `account_id`. Když `account_id` chybí a číslo je
        víceznačné, server vrátí `409` s kódem `ambiguous_account_currency` a polem
        `candidates` (seznam měnových variant). Při jednoznačném čísle je `account_id`
        nepovinné (zpětně kompatibilní).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: GPC, CSV (FIO/KB/MB formáty) nebo ABO výpis
                account_id:
                  type: integer
                  description: |
                    Cílový měnový účet (`currencies.id`). Povinné jen u víceměnového účtu
                    se sdíleným číslem účtu (jinak server vrátí 409 `ambiguous_account_currency`).
      responses:
        '201':
          description: Naparsováno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BankStatement' }
        '400':
          description: Neznámý formát / poškozený soubor
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: |
            Číslo účtu není registrované u dodavatele (`wrong_supplier_account`),
            nebo má více měnových variant a chybí `account_id` (`ambiguous_account_currency`
            — `error.candidates` nese seznam `{account_id, code, label}`).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422':
          description: Zvolený `account_id` neodpovídá číslu účtu výpisu / dodavateli (`invalid_account`)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/bank-statements/upload-pdf:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Bank]
      summary: Nahraje bankovní výpis z PDF (banky bez GPC/CSV exportu)
      description: Multipart upload, max 5 MiB. Extrakce transakcí z PDF.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
                account_id: { type: integer, description: "Cílový měnový účet u víceměnového čísla účtu" }
      responses:
        '200':
          description: "Naparsováno {statement_id, transaction_count, duplicates_count, skipped}"
          content:
            application/json:
              schema: { type: object }
        '400':
          description: Chybí soubor / neplatný formát / parsování selhalo
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: Číslo účtu neodpovídá dodavateli (`wrong_supplier_account`)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '413': { description: Soubor je příliš velký }

  /api/v1/bank-statements/{id}/rematch:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
        description: ID výpisu (např. z odpovědi `/bank-statements/upload`).
    post:
      tags: [Bank]
      summary: Přepáruje nespárované transakce výpisu proti fakturám
      description: |
        Auto-matching nad transakcemi výpisu **přímo v databázi** — žádný
        souborový import ani konfigurace serveru se nepoužívá. Vhodné po nahrání
        výpisu přes `/bank-statements/upload`, nebo když uživatel ex-post doplnil
        faktury, které by se daly napárovat.

        Projde transakce výpisu ve stavu `unmatched` a `auto_partial` a pro každou
        zkusí najít fakturu podle:
        - variabilního symbolu (přesný match) — preferovaná cesta,
        - částky + protiúčtu (fallback).

        Příchozí platba se páruje na vystavenou fakturu, odchozí na přijatou.
        Při unikátní shodě se transakce označí jako spárovaná a faktura `paid`
        s `paid_at = transaction.posted_at`; transakce s více kandidáty zůstávají
        nepřiřazené pro manuální rozhodnutí. Stavy `auto_exact`, `manual`
        a `ignored` zůstávají nedotčené.

        Pozn.: hromadný **import výpisů z adresáře na serveru** (`scan_root`) je
        hostitelská/cron záležitost a není součástí veřejného API — výpisy se přes
        API nahrávají endpointem `/bank-statements/upload`.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  considered:
                    type: integer
                    description: Počet transakcí, které se přepárovávaly (unmatched + auto_partial).
                  newly_matched:
                    type: integer
                    description: Nově přesně spárované (auto_exact).
                  newly_partial:
                    type: integer
                    description: Nově částečně spárované (auto_partial — částka v širší toleranci).
                  still_unmatched:
                    type: integer
                    description: Po přepárování stále nespárované.
        '404': { description: "Výpis nenalezen nebo nepatří current supplier" }

  /api/v1/bank-statements/{id}/match-suggestions:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Bank]
      summary: Čekající skórované návrhy párování pro bankovní výpis
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  suggestions:
                    type: array
                    items: { $ref: '#/components/schemas/BankMatchSuggestion' }
        '404': { description: Výpis nebyl nalezen v aktuální firmě }

  /api/v1/bank-match-suggestions/{id}/accept:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Potvrdí vybraného kandidáta návrhu párování
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                candidate_index: { type: integer, minimum: 0, default: 0 }
      responses:
        '200': { description: Transakce byla spárována }
        '404': { description: Návrh nebyl nalezen v aktuální firmě }
        '409': { description: Návrh už byl vyřízen }
        '422': { description: Index kandidáta je mimo rozsah }

  /api/v1/bank-match-suggestions/{id}/reject:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Zamítne návrh párování
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 190 }
      responses:
        '200': { description: Návrh byl zamítnut }
        '404': { description: Návrh nebyl nalezen v aktuální firmě }
        '409': { description: Návrh už byl vyřízen }

  /api/v1/purchase-invoices/payment-orders/candidates:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Purchase invoices]
      summary: Kandidáti pro platební příkaz (nezaplacené přijaté faktury)
      parameters:
        - in: query
          name: currency
          schema: { type: string }
          description: "ISO 4217 — omezí kandidáty i účty plátce na danou měnu."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates:
                    type: array
                    items: { type: object }
                    description: "Nezaplacené přijaté faktury vhodné k zařazení do příkazu."
                  payer_accounts:
                    type: array
                    items: { type: object }
                    description: "Bankovní účty dodavatele (plátce) k výběru odesílacího účtu."
                  supplier_currencies:
                    type: array
                    items: { type: object }

  /api/v1/purchase-invoices/payment-orders/verify-account:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Purchase invoices]
      summary: Ověří bankovní účet dodavatele faktury (CRPDPH)
      parameters:
        - in: query
          name: invoice_id
          required: true
          schema: { type: integer, format: int64 }
      responses:
        '200':
          description: OK — výsledek ověření spolehlivosti účtu
          content:
            application/json:
              schema: { type: object }
        '404': { description: "Faktura nenalezena" }

  /api/v1/purchase-invoices/payment-orders:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Purchase invoices]
      summary: Historie vytvořených platebních příkazů
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { type: object }
    post:
      tags: [Purchase invoices]
      summary: Vytvoří platební příkaz z vybraných přijatých faktur
      description: |
        Vyžaduje roli `accountant` nebo vyšší. Sestaví příkaz k úhradě
        (ke stažení jako ABO/KPC, CSV, PDF nebo SEPA XML přes `/payment-orders/{id}/download`).
        Volitelně rovnou označí faktury jako „předáno k úhradě" (`mark_paid`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice_ids, payer_currency_id, payment_date]
              properties:
                invoice_ids:
                  type: array
                  items: { type: integer, format: int64 }
                payer_currency_id:
                  type: integer
                  format: int64
                  description: "Účet plátce (currencies.id) — určuje odesílací účet a měnu."
                payment_date:
                  type: string
                  format: date
                  description: "Datum splatnosti / provedení příkazu."
                constant_symbol: { type: [string, "null"] }
                note: { type: [string, "null"] }
                mark_paid:
                  type: boolean
                  description: "Označit zařazené faktury jako předané k úhradě."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  order_id: { type: integer, format: int64 }
                  view:
                    type: object
                    properties:
                      item_count: { type: integer }
                      total_amount: { type: number }
                      currency: { type: string }
                      mark_paid: { type: boolean }
                  skipped:
                    type: array
                    items: { type: object }
                    description: "Faktury vynechané z příkazu (s důvodem)."
        '422':
          description: Validace selhala
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/purchase-invoices/payment-orders/mark:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Purchase invoices]
      summary: Označí faktury jako předané k úhradě (bez generování souboru)
      description: "Vyžaduje roli `accountant` nebo vyšší. Nastaví příznak `payment_ordered_at` (NEmění stav faktury)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice_ids]
              properties:
                invoice_ids:
                  type: array
                  items: { type: integer, format: int64 }
                mark_paid: { type: boolean }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
        '422':
          description: Validace selhala
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/purchase-invoices/payment-orders/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Purchase invoices]
      summary: Detail platebního příkazu vč. položek
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  order_id: { type: integer, format: int64 }
                  created_at: { type: string, format: date-time }
                  items:
                    type: array
                    items: { type: object }
        '404': { description: "Příkaz nenalezen" }

  /api/v1/purchase-invoices/payment-orders/{id}/download:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: query
        name: format
        schema: { type: string, enum: [abo, csv, pdf, sepa], default: abo }
        description: "Formát ke stažení: abo (KPC/ABO, jen CZK), csv, pdf, nebo sepa (SEPA Credit Transfer XML, ISO 20022 pain.001.001.03 — vyžaduje IBAN plátce i příjemců)."
    get:
      tags: [Purchase invoices]
      summary: Stáhne soubor platebního příkazu (ABO/CSV/PDF/SEPA)
      responses:
        '200':
          description: OK — binární soubor příkazu
          content:
            text/plain:
              schema: { type: string, format: binary }
            text/csv:
              schema: { type: string, format: binary }
            application/pdf:
              schema: { type: string, format: binary }
            application/xml:
              schema: { type: string, format: binary }
        '404': { description: "Příkaz nenalezen" }
        '422': { description: "Export selhal" }

  /api/v1/purchase-invoices/{id}/payment-qr:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Purchase invoices]
      summary: QR platba a bankovní účet přijaté faktury
      description: |
        Vrátí QR platbu (SPAYD pro CZK, SEPA EPC jinak) a bankovní účet zjištěný
        v pořadí ISDOC → AI → ruční edit. `needs_account=true` signalizuje, že
        klient může spustit doplnění účtu přes `/payment-qr/extract-account`.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PurchasePaymentQr' }
        '400': { description: "Neplatné ID" }
        '404': { description: "Faktura nenalezena" }

  /api/v1/purchase-invoices/{id}/payment-qr/extract-account:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Purchase invoices]
      summary: Doplní bankovní účet přijaté faktury (lazy z ISDOC/AI)
      description: "Vyžaduje roli `accountant` nebo vyšší."
      responses:
        '200':
          description: OK — stejný tvar jako GET /payment-qr
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PurchasePaymentQr' }
        '404': { description: "Faktura nenalezena" }

  /api/v1/purchase-invoices/{id}/payment-account:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    put:
      tags: [Purchase invoices]
      summary: Ruční nastavení bankovního účtu přijaté faktury
      description: "Vyžaduje roli `accountant` nebo vyšší. Uloží účet a nastaví `source=manual`."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                account_number: { type: string, maxLength: 34 }
                bank_code: { type: string, maxLength: 10 }
                iban: { type: string, maxLength: 34 }
                bic: { type: string, maxLength: 11 }
                variable_symbol: { type: string, maxLength: 20 }
      responses:
        '200':
          description: OK — stejný tvar jako GET /payment-qr
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PurchasePaymentQr' }
        '404': { description: "Faktura nenalezena" }

  /api/v1/invoices/{id}/work-report/materials:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: query
        name: force
        schema: { type: integer, enum: [1] }
        description: "Jen admin — vynutí uložení i u vystavené faktury."
    put:
      tags: [WorkReports]
      summary: Uloží materiálovou část výkazu práce
      description: |
        Vyžaduje roli `accountant` nebo vyšší. Standardně jen u konceptu faktury
        (admin může vynutit u vystavené přes `?force=1`). Materiály jsou součástí
        téhož výkazu práce jako práce a na faktuře se promítnou jako souhrnná položka.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [materials]
              properties:
                material_title: { type: string, default: "Materiál" }
                material_vat_rate_id:
                  type: [integer, "null"]
                  description: "Povinné, je-li alespoň jeden materiál; musí existovat v sazbách DPH."
                project_id: { type: [integer, "null"] }
                materials:
                  type: array
                  items:
                    type: object
                    required: [description, quantity, unit, unit_price]
                    properties:
                      description: { type: string }
                      quantity: { type: number, description: "> 0" }
                      unit: { type: string }
                      unit_price: { type: number, description: ">= 0" }
                      order_index: { type: integer }
      responses:
        '200':
          description: OK — aktualizovaný výkaz práce
          content:
            application/json:
              schema: { type: object }
        '400': { description: "Validace selhala" }
        '404': { description: "Faktura nenalezena" }
        '409': { description: "Faktura není editovatelná (není koncept)" }

  /api/v1/clients/{id}/work-report-link:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [WorkReports]
      summary: Stav veřejného sledovacího odkazu na výkaz (klient)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkReportLinkStatus' }
        '404': { description: "Klient nenalezen" }
    delete:
      tags: [WorkReports]
      summary: Zneplatní sledovací odkaz klienta
      description: "Vyžaduje roli `accountant` nebo vyšší."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  exists: { type: boolean }
        '404': { description: "Klient nenalezen" }

  /api/v1/clients/{id}/work-report-link/recipients:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [WorkReports]
      summary: Příjemci pro odeslání odkazu (předvyplnění modalu) — klient
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkReportLinkRecipients' }
        '404': { description: "Klient nenalezen" }

  /api/v1/clients/{id}/work-report-link/send:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [WorkReports]
      summary: Odešle sledovací odkaz na výkaz e-mailem (klient)
      description: "Vyžaduje roli `accountant` nebo vyšší. Vytvoří odkaz, pokud ještě neexistuje."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                to: { type: array, items: { type: string } }
                cc: { type: array, items: { type: string } }
                bcc: { type: array, items: { type: string } }
                note: { type: string, maxLength: 2000 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  sent_to: { type: array, items: { type: string } }
                  sent_at: { type: string, format: date-time }
                  url: { type: string }
        '422': { description: "Žádný příjemce (no_recipients)" }
        '404': { description: "Klient nenalezen" }
        '502': { description: "Odeslání selhalo (send_failed)" }

  /api/v1/projects/{id}/work-report-link:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [WorkReports]
      summary: Stav veřejného sledovacího odkazu na výkaz (zakázka)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkReportLinkStatus' }
        '404': { description: "Zakázka nenalezena" }
    delete:
      tags: [WorkReports]
      summary: Zneplatní sledovací odkaz zakázky
      description: "Vyžaduje roli `accountant` nebo vyšší."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  exists: { type: boolean }
        '404': { description: "Zakázka nenalezena" }

  /api/v1/projects/{id}/work-report-link/recipients:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [WorkReports]
      summary: Příjemci pro odeslání odkazu (předvyplnění modalu) — zakázka
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkReportLinkRecipients' }
        '404': { description: "Zakázka nenalezena" }

  /api/v1/projects/{id}/work-report-link/send:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [WorkReports]
      summary: Odešle sledovací odkaz na výkaz e-mailem (zakázka)
      description: "Vyžaduje roli `accountant` nebo vyšší. Vytvoří odkaz, pokud ještě neexistuje."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                to: { type: array, items: { type: string } }
                cc: { type: array, items: { type: string } }
                bcc: { type: array, items: { type: string } }
                note: { type: string, maxLength: 2000 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  sent_to: { type: array, items: { type: string } }
                  sent_at: { type: string, format: date-time }
                  url: { type: string }
        '422': { description: "Žádný příjemce (no_recipients)" }
        '404': { description: "Zakázka nenalezena" }
        '502': { description: "Odeslání selhalo (send_failed)" }

  /api/v1/codebooks/countries:
    get:
      tags: [Codebooks]
      summary: Seznam zemí
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  countries:
                    type: array
                    items: { $ref: '#/components/schemas/Country' }

  /api/v1/codebooks/currencies:
    get:
      tags: [Codebooks]
      summary: Seznam podporovaných měn
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  currencies:
                    type: array
                    items: { $ref: '#/components/schemas/Currency' }

  /api/v1/codebooks/vat-rates:
    get:
      tags: [Codebooks]
      summary: Seznam DPH sazeb
      responses:
        '200':
          description: OK

  /api/v1/codebooks/units:
    get:
      tags: [Codebooks]
      summary: Seznam měrných jednotek
      responses:
        '200':
          description: OK

  /api/v1/codebooks/years:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Codebooks]
      summary: Distinct roky z faktur (zdroj pro year filter dropdown)
      description: |
        Vrací DISTINCT `YEAR(issue_date)` z `invoices` a `purchase_invoices`
        per aktuální supplier. Backend navíc doplňuje aktuální + minulý rok
        do `combined` setu (aby UI dropdown nikdy nebyl prázdný — fresh setup
        bez dokladů).

        Použij pro historické datové migrace (Fakturoid, iDoklad — typicky
        5-10+ let), kde hardcoded posledních N let v UI by data zakryl.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoices:
                    type: array
                    items: { type: integer }
                    example: [2026, 2025, 2024, 2023, 2018]
                    description: Distinct roky z `invoices` (vystavené)
                  purchase_invoices:
                    type: array
                    items: { type: integer }
                    example: [2026, 2025, 2024]
                    description: Distinct roky z `purchase_invoices` (přijaté)
                  combined:
                    type: array
                    items: { type: integer }
                    example: [2026, 2025, 2024, 2023, 2018]
                    description: Sjednocení obou množin + aktuální a minulý rok jako floor

  # ============================================================================
  # Purchase invoices (přijaté faktury) — fáze 1 integrace forku
  # ============================================================================
  /api/v1/purchase-invoices:
    get:
      tags: [Purchase invoices]
      summary: Seznam přijatých faktur seskupený po měsících
      description: |
        Vrací přijaté faktury aktuálního tenant (supplier_id), seskupené podle
        `COALESCE(tax_date, issue_date)` per měsíc. Stejný response tvar jako
        `/invoices` ale s `vendor_*` poli místo `client_*`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - in: query
          name: filter[status]
          schema: { type: string, enum: [draft, received, booked, paid, cancelled] }
        - in: query
          name: filter[document_kind]
          schema: { type: string, enum: [invoice, receipt, credit_note, advance] }
        - in: query
          name: filter[vendor_id]
          schema: { type: integer }
        - in: query
          name: filter[year]
          schema: { type: integer }
        - in: query
          name: filter[month]
          schema: { type: integer }
        - in: query
          name: filter[unpaid_only]
          schema: { type: boolean }
        - in: query
          name: filter[overdue]
          schema: { type: boolean }
        - in: query
          name: filter[booked]
          description: "Zaúčtování (jen podvojné účetnictví): 1 = zaúčtováno (booked_at není NULL), 0 = nezaúčtováno"
          schema: { type: string, enum: ['1', '0'] }
        - in: query
          name: q
          schema: { type: string }
        - in: query
          name: page
          schema: { type: integer, default: 1 }
        - in: query
          name: per_page
          schema: { type: integer, default: 50, maximum: 200 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        month: { type: string, example: "2026-05" }
                        count: { type: integer }
                        totals_per_currency:
                          type: array
                          items:
                            type: object
                            properties:
                              currency: { type: string }
                              without_vat: { type: number }
                              vat: { type: number }
                              with_vat: { type: number }
                        invoices:
                          type: array
                          items: { $ref: '#/components/schemas/PurchaseInvoiceListItem' }
                  meta:
                    type: object
                    properties:
                      total: { type: integer }
                      page: { type: integer }
                      per_page: { type: integer }
                      pages: { type: integer }
    post:
      tags: [Purchase invoices]
      summary: Vytvoří draft přijaté faktury
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PurchaseInvoicePayload' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PurchaseInvoice' }
        '400': { description: Validation error }

  /api/v1/purchase-invoices/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    get:
      tags: [Purchase invoices]
      summary: Detail přijaté faktury
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PurchaseInvoice' }
        '404': { description: Nenalezeno }
    put:
      tags: [Purchase invoices]
      summary: Update přijaté faktury (jen draft)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - in: query
          name: force
          description: Admin override pro received status (booked/paid/cancelled neumožněno).
          schema: { type: boolean }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PurchaseInvoicePayload' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PurchaseInvoice' }
        '409': { description: Not editable }
    delete:
      tags: [Purchase invoices]
      summary: Smaže draft (vystavené faktury použij transition cancel)
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses:
        '200': { description: OK }
        '409': { description: Lze smazat jen draft }

  /api/v1/purchase-invoices/{id}/items:
    put:
      tags: [Purchase invoices]
      summary: Přepíše položky a přepočte sumy
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                items:
                  type: array
                  items: { $ref: '#/components/schemas/PurchaseInvoiceItemPayload' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PurchaseInvoice' }

  /api/v1/purchase-invoices/{id}/exchange-rate:
    post:
      tags: [Purchase invoices]
      summary: Nastaví exchange rate manuálně
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                rate: { type: [number, "null"] }
                rate_date: { type: [string, "null"], format: date }
                source: { type: string, enum: [cnb, manual, idoklad, fakturoid], default: manual }
      responses:
        '200': { description: OK }

  /api/v1/purchase-invoices/{id}/transition:
    post:
      tags: [Purchase invoices]
      summary: Přechod stavu (state machine draft → received → booked → paid; + cancelled)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [target]
              properties:
                target: { type: string, enum: [received, booked, paid, cancelled] }
                paid_date:
                  type: string
                  format: date
                  description: Pouze pro target=paid (default = dnes)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PurchaseInvoice' }
        '409': { description: Invalid transition }

  /api/v1/purchase-invoices/{id}/pdf:
    post:
      tags: [Purchase invoices]
      summary: Upload originálního PDF od dodavatele (archive + SHA-256 dedup)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: PDF dokument (max 20 MiB, magic bytes %PDF- validation)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  pdf_path: { type: string }
                  pdf_hash: { type: string, description: SHA-256 hex }
                  pdf_size_bytes: { type: integer }
                  pdf_original_name: { type: string }
        '409':
          description: Stejné PDF už archivováno u jiné faktury (existing_purchase_invoice_id v error.extra)
        '413': { description: File too large (> 20 MiB) }
    get:
      tags: [Purchase invoices]
      summary: Stáhne archivované PDF od dodavatele
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: PDF stream
          content:
            application/pdf: {}
        '404': { description: K faktuře není archivovaný PDF }
    delete:
      tags: [Purchase invoices]
      summary: Smaže archivované originální PDF (dedup-aware; guard proti zamčené faktuře)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: Smazáno }
        '404': { description: K faktuře není archivovaný PDF }
        '409': { description: Faktura je zamčená (denyIfLocked) }

  /api/v1/purchase-invoices/{id}/our-pdf:
    get:
      tags: [Purchase invoices]
      summary: Naše PDF rekonstrukce přijaté faktury (mPDF z dat)
      description: |
        Vygeneruje naši vlastní PDF kopii přijaté faktury. Užitečné když chybí
        originál (importované jen metadata) nebo chceme vlastní layout.
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: inline, schema: { type: integer, enum: [0,1] }, description: '1 = Content-Disposition: inline (iframe preview)' }
      responses:
        '200':
          description: PDF stream
          content:
            application/pdf: {}
        '404': { description: Faktura nenalezena }

  /api/v1/purchase-invoices/{id}/isdoc:
    get:
      tags: [Purchase invoices]
      summary: ISDOC 6.0 XML export přijaté faktury
      description: |
        Role inversion — pro přijatou je `AccountingSupplierParty` = vendor,
        `AccountingCustomerParty` = náš tenant. Kořenové `ID` obsahuje číslo
        dokladu dodavatele; interní číslo MyInvoice je v
        `Extensions/myi:InternalDocumentNumber` s namespace
        `https://myinvoice.cz/isdoc/extensions/2026`. `PaymentMeans` obsahuje
        evidovaný bankovní účet dodavatele a platební `VariableSymbol`; pokud
        samostatný platební VS chybí, použije se jeho číselná podoba odvozená
        z čísla faktury dodavatele.
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: ISDOC XML
          content:
            application/xml: {}
        '404': { description: Faktura nenalezena }

  /api/v1/purchase-invoices/{id}/pohoda:
    get:
      tags: [Purchase invoices]
      summary: Pohoda XML (dataPack s `<pur:purchase>`) export přijaté faktury
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: Pohoda XML
          content:
            application/xml: {}
        '404': { description: Faktura nenalezena }

  /api/v1/purchase-invoices/scan-inbox:
    post:
      tags: [Purchase invoices]
      summary: Scan adresáře s PDF/ISDOC pro automatický import (admin/accountant only)
      description: |
        Rekurzivně projde adresář nastavený v `cfg.purchase_invoice.inbox_dir`,
        deduplikuje přes SHA-256 (vůči `purchase_invoices.pdf_hash`). Z PDF s embedded
        ISDOC rozpozná data a automaticky vytvoří draft přijaté faktury. Pure PDF bez
        ISDOC se přeskakují — pro AI extrakci použij endpoint
        `POST /api/integrations/anthropic/extract` (BYOK).
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                dry_run:
                  type: boolean
                  description: True = jen vrátit seznam nálezů bez zápisu
                  default: false
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InboxScanResult' }
        '403': { description: Pouze admin/accountant }

  /api/v1/reports/dphdp3/preview:
    get:
      tags: [Reports]
      summary: Náhled DPH přiznání (DPHDP3) pro období
      description: |
        ⚠️ XML vygenerovaný buildem je POUZE POMŮCKA. Před odesláním na EPO portál
        MFČR vždy ověř s účetní.
        Summary obsahuje také `document_refs` (snapshot dokladů zahrnutých do podání)
        a `cross_check`, včetně neblokujícího upozornění na nevystavené koncepty DDKP/finálu.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year,   required: true, schema: { type: integer, minimum: 2020, maximum: 2050 } }
        - { in: query, name: month,  schema: { type: integer, minimum: 1, maximum: 12 }, description: 'Pro měsíční periodu' }
        - { in: query, name: quarter, schema: { type: integer, minimum: 1, maximum: 4 }, description: 'Pro kvartální periodu' }
      responses:
        '200':
          description: JSON summary (lines 1-29 + 40+, totals, deadline)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DphPriznaniPreview' }
        '403': { description: Pouze admin/accountant }

  /api/v1/reports/dphdp3:
    get:
      tags: [Reports]
      summary: Stáhne XML DPH přiznání (DPHDP3 verzePis 03.01) pro EPO portál
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer } }
        - { in: query, name: month, schema: { type: integer } }
        - { in: query, name: quarter, schema: { type: integer } }
      responses:
        '200':
          description: XML download
          content:
            application/xml: {}
        '403': { description: Pouze admin/accountant }

  /api/v1/reports/dphkh1/preview:
    get:
      tags: [Reports]
      summary: Náhled Kontrolního hlášení (DPHKH1) — vždy měsíční
      description: |
        Kontrolní hlášení se podává **vždy měsíčně**, i pro kvartální plátce DPH.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year,  required: true, schema: { type: integer } }
        - { in: query, name: month, required: true, schema: { type: integer, minimum: 1, maximum: 12 } }
      responses:
        '200': { description: Summary s counts sekcí A.1/A.2/A.4/A.5/B.1/B.2/B.3 }
        '403': { description: Pouze admin/accountant }

  /api/v1/reports/dphkh1:
    get:
      tags: [Reports]
      summary: Stáhne XML Kontrolního hlášení (DPHKH1 verzePis 03.01)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year,  required: true, schema: { type: integer } }
        - { in: query, name: month, required: true, schema: { type: integer } }
      responses:
        '200':
          description: XML download
          content:
            application/xml: {}

  /api/v1/reports/dphshv/preview:
    get:
      tags: [Reports]
      summary: Náhled Souhrnného hlášení (EU dodání) — měsíční
      description: |
        Souhrnné hlášení podávají **plátci DPH i identifikované osoby** s alespoň
        1 EU dodávkou v měsíci. Mapování VAT kódů: 20→0 (zboží), 22→2 (služby).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year,  required: true, schema: { type: integer } }
        - { in: query, name: month, required: true, schema: { type: integer } }
      responses:
        '200': { description: "Summary s řádky (country, vat_id, sh_type, amount, count)" }

  /api/v1/reports/dphshv:
    get:
      tags: [Reports]
      summary: Stáhne XML Souhrnného hlášení (DPHSHV verzePis 06.01)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year,  required: true, schema: { type: integer } }
        - { in: query, name: month, required: true, schema: { type: integer } }
      responses:
        '200':
          description: XML download
          content:
            application/xml: {}

  # ── Tax submissions archive ──────────────────────────────────────────────
  /api/v1/reports/submissions:
    get:
      tags: [Reports]
      summary: Historie všech generovaných EPO XML výkazů
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: List archived submissions
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/TaxSubmission' } }

  /api/v1/reports/submissions/settings:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Reports]
      summary: Nastavení kořenových složek důkazního archivu EPO
      responses:
        '200':
          description: Aktuální nastavení
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoSubmissionSettings' }
        '403': { $ref: '#/components/responses/Forbidden' }
    put:
      tags: [Reports]
      summary: Uloží kořenové složky důkazního archivu EPO
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EpoSubmissionSettings' }
      responses:
        '200':
          description: Uložené nastavení
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoSubmissionSettings' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { description: Vybraná složka neexistuje nebo nepatří aktuální firmě }

  /api/v1/reports/submissions/epo-credentials:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Reports]
      summary: Seznam osobních certifikátů přihlášeného uživatele pro EPO
      description: Nikdy nevrací PFX, soukromý klíč, heslo PFX ani jejich ciphertext.
      responses:
        '200':
          description: Certifikáty vlastníka včetně příznaku povolení pro aktuální firmu
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/EpoSigningCredential' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Reports]
      summary: Zašifruje a uloží osobní P12/PFX certifikát pro přímé EPO
      description: |
        Vyžaduje session, znovu aktuální heslo a TOTP, pokud je u uživatele
        aktivní. Server odmítne ukládání bez samostatného
        `app.secret_encryption_key`. Certifikát se současně povolí pro aktuální firmu.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, label, pfx_password, password]
              properties:
                file: { type: string, format: binary, description: Soubor P12/PFX, nejvýše 256 KiB }
                label: { type: string, maxLength: 120 }
                pfx_password: { type: string, format: password }
                password: { type: string, format: password, description: Aktuální heslo uživatele }
                totp_code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '201':
          description: Veřejná metadata uloženého certifikátu
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoSigningCredential' }
        '400': { description: Neplatný PFX, heslo, certifikát nebo chybějící soukromý klíč }
        '401': { description: Step-up autentizace selhala }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Stejný certifikát už je uložen }
        '503': { description: Chybí samostatný šifrovací klíč nebo OpenSSL }

  /api/v1/reports/submissions/epo-credentials/{credentialId}/supplier:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: credentialId, required: true, schema: { type: integer, format: int64 } }
    put:
      tags: [Reports]
      summary: Povolí nebo odebere vlastní certifikát pro aktuální firmu
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enabled, password]
              properties:
                enabled: { type: boolean }
                password: { type: string, format: password }
                totp_code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200': { description: Aktualizovaný seznam certifikátů }
        '401': { description: Step-up autentizace selhala }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Certifikát používá aktivní podpisový profil aktuální firmy }

  /api/v1/reports/submissions/epo-credentials/{credentialId}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: credentialId, required: true, schema: { type: integer, format: int64 } }
    delete:
      tags: [Reports]
      summary: Vyřadí vlastní certifikát z aktivního EPO trezoru
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string, format: password }
                totp_code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200': { description: Šifrovaný certifikát byl fyzicky odstraněn; historické pokusy drží pouze jeho otisk }
        '401': { description: Step-up autentizace selhala }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Certifikát používá alespoň jeden aktivní podpisový profil }

  /api/v1/reports/submissions/{id}:
    get:
      tags: [Reports]
      summary: Detail záznamu (vč. XML)
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: Submission detail
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TaxSubmission' }
        '404': { description: Not found }
    delete:
      tags: [Reports]
      summary: Smaže archivovaný záznam (jen admin)
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: Deleted }
        '403': { description: Pouze admin }
        '409': { description: Podání už obsahuje EPO pokus, dokument nebo potvrzený stav }

  /api/v1/reports/submissions/{id}/xml:
    get:
      tags: [Reports]
      summary: Stáhne archivované XML
      description: Název obsahuje období, ID snapshotu a čas jeho vygenerování, takže opakované exporty nekolidují.
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: XML download
          content:
            application/xml: {}

  /api/v1/reports/submissions/{id}/epo-handoff:
    parameters:
      - { in: path, name: id, required: true, schema: { type: integer } }
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Předá uložený XML snapshot do asistovaného formuláře EPO
      description: |
        Odešle přesné archivované XML na oficiální EPO endpoint s
        `otevriFormular=1`. Krátkodobé návratové URL se vrací pouze v odpovědi,
        server je neukládá do databáze ani logu; klient je může ponechat pouze
        do uvedeného času expirace. Operace sama podání neodesílá.
        Asistovaný handoff vždy používá ostrý interaktivní formulář bez ohledu
        na konfiguraci zkušebního prostředí přímého podepsaného API.
        Pokud klient odkaz ztratí, může vědomě zrušit aktivní pokus a vytvořit
        nový pomocí `replace_active`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                replace_active:
                  type: boolean
                  default: false
                  description: Zruší dosavadní aktivní pokus a vytvoří nový krátkodobý odkaz.
      responses:
        '201':
          description: Dočasný odkaz na předvyplněný formulář EPO
          headers:
            Cache-Control:
              schema: { type: string, example: no-store }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoHandoffResult' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Aktivní předání již existuje nebo je snapshot podaný }
        '422': { description: Snapshot není podporovaný, XSD-validní nebo se změnil jeho hash }
        '502': { description: EPO endpoint vrátil neplatnou odpověď }
        '503': { description: EPO endpoint není dostupný }

  /api/v1/reports/submissions/{id}/epo-test:
    parameters:
      - { in: path, name: id, required: true, schema: { type: integer } }
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Podepíše snapshot a provede úplný test EPO s test=1
      description: |
        Vytvoří připojený CMS/PKCS#7 signedData v DER s právě jedním podpisem,
        kontrolní hlášení před podpisem vloží jako jediné XML do ZIP, odešle
        payload na nakonfigurované EPO prostředí a archivuje celý XML protokol.
        Podepsaný testovací payload zůstane pouze šifrovaně v interním úložišti
        a není dostupný ke stažení. Typy S, K a E test blokují; propustné N/P se
        vracejí jako upozornění. Operace nečiní daňové podání a vyžaduje
        session step-up heslem a případně TOTP.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [credential_id, password]
              properties:
                credential_id: { type: integer, format: int64 }
                password: { type: string, format: password }
                totp_code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200':
          description: Výsledek všech kontrol EPO
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoDirectTestResult' }
        '401': { description: Step-up autentizace selhala }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { description: Snapshot nebo certifikát není dostupný }
        '422': { description: Neplatný snapshot nebo certifikát }
        '502': { description: Neplatná odpověď EPO }
        '503': { description: EPO nebo kryptografická konfigurace není dostupná }

  /api/v1/reports/submissions/{id}/epo-submit:
    parameters:
      - { in: path, name: id, required: true, schema: { type: integer } }
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Právně účinně podepíše a odešle snapshot do EPO
      description: |
        Vyžaduje úspěšný test téhož snapshotu a certifikátu, session step-up
        heslem a případně TOTP. Při síťovém timeoutu výsledek označí jako
        `uncertain` a nikdy automaticky neopakuje POST. Při `epo_test=true`
        odešle pouze do zkušebního prostředí a snapshot neoznačí jako právně
        podaný.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [attempt_id, password]
              properties:
                attempt_id: { type: integer, format: int64 }
                password: { type: string, format: password }
                totp_code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200':
          description: Potvrzené nebo offline zpracovávané podání
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoDirectSubmissionResult' }
        '401': { description: Step-up autentizace selhala }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Chybí úspěšný test, snapshot už byl podán nebo se změnilo aktivní EPO prostředí }
        '422': { description: EPO podání odmítlo; odpověď obsahuje messages }
        '502': { description: Potvrzení EPO nelze bezpečně ověřit }
        '503': { description: Výsledek odeslání je nejistý nebo EPO nedostupné }

  /api/v1/reports/submissions/{id}/epo-status:
    parameters:
      - { in: path, name: id, required: true, schema: { type: integer } }
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Obnoví stav přímého nebo rozsáhlého EPO podání
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [attempt_id]
              properties:
                attempt_id: { type: integer, format: int64 }
      responses:
        '200':
          description: Uložený aktuální stav bez vrácení stavového hesla
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoDirectSubmissionResult' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Pokus nemá údaje pro stav nebo patří do jiného než aktuálně aktivního EPO prostředí }
        '502': { description: EPO vrátilo neplatnou odpověď }

  /api/v1/reports/submissions/{id}/epo-attempts/{attemptId}/confirmation:
    parameters:
      - { in: path, name: id, required: true, schema: { type: integer } }
      - { in: path, name: attemptId, required: true, schema: { type: integer, format: int64 } }
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Ověří nalezené P7S/P7M a bezpečně dokončí nejisté přímé podání
      description: |
        Vyžaduje session step-up. Produkční potvrzení musí mít platný podpis a
        řetězec EPO; sandboxové potvrzení platný podpis a přesnou testovací
        identitu GFŘ. Potvrzení se musí vztahovat k archivovanému odchozímu CMS.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, password]
              properties:
                file: { type: string, format: binary, description: Potvrzení EPO P7S/P7M, nejvýše 10 MiB }
                password: { type: string, format: password }
                totp_code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200':
          description: Kryptograficky ověřené a dokončené podání
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoDirectSubmissionResult' }
        '400': { description: Chybějící nebo neplatný soubor }
        '401': { description: Step-up autentizace selhala }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Pokus nelze tímto způsobem obnovit }
        '502': { description: Potvrzení nelze bezpečně ověřit }

  /api/v1/reports/submissions/{id}/epo-attempts/{attemptId}/resolve-not-submitted:
    parameters:
      - { in: path, name: id, required: true, schema: { type: integer } }
      - { in: path, name: attemptId, required: true, schema: { type: integer, format: int64 } }
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Auditovaně uvolní nejednoznačný pokus ověřený jako nepřijatý
      description: |
        Nikdy nekontaktuje submit endpoint. Je dostupné jen pro `uncertain` bez
        vzdáleného identifikátoru nebo pro `submitting` starší 15 minut, pokud
        nejsou dostupné údaje pro stavový dotaz. Vyžaduje session step-up,
        výslovné potvrzení a auditní poznámku.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [verified_not_submitted, note, password]
              properties:
                verified_not_submitted: { type: boolean, enum: [true] }
                note: { type: string, minLength: 10, maxLength: 500 }
                password: { type: string, format: password }
                totp_code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200':
          description: Pokus byl auditovaně uzavřen a snapshot lze znovu podat
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoDirectSubmissionResult' }
        '400': { description: Chybí výslovné potvrzení nebo auditní poznámka }
        '401': { description: Step-up autentizace selhala }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Pokus není bezpečně uvolnitelný }

  /api/v1/reports/submissions/{id}/artifacts:
    parameters:
      - { in: path, name: id, required: true, schema: { type: integer } }
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Nahraje potvrzení a další artefakty EPO do dokumentů
      description: |
        Přijímá nejvýše 20 souborů XML, P7S/P7M nebo PDF. Před uložením
        automaticky vytvoří nakonfigurovanou stromovou strukturu archivu podle
        druhu formuláře a období. U podepsaného potvrzení se ověří dostupné
        kryptografické a obsahové vlastnosti; podání uživatel doloží ručně po
        kontrole doručenky, dokud není k dispozici spravovaný allowlist EPO pečetí.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                attempt_id:
                  type: integer
                  format: int64
                  description: ID pokusu vrácené handoffem; bez něj zůstane artefakt bez vazby na konkrétní pokus.
                file:
                  type: array
                  maxItems: 20
                  items: { type: string, format: binary }
      responses:
        '200':
          description: Výsledek uložení a ověření artefaktů
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EpoArtifactUploadResult' }
        '400': { description: Chybějící, neplatné nebo nezpracovatelné soubory }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { description: Více než 20 souborů }
        '415': { description: Nepodporovaný typ artefaktu }

  /api/v1/reports/submissions/{id}/artifacts/{artifactId}/download:
    parameters:
      - { in: path, name: id, required: true, schema: { type: integer } }
      - { in: path, name: artifactId, required: true, schema: { type: integer } }
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Reports]
      summary: Stáhne důkazní artefakt EPO v rámci oprávnění k reportům
      responses:
        '200':
          description: Soubor jako bezpečná příloha
          content:
            application/octet-stream: {}
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ── Codebooks: cnb-rate + expense-categories + vat-classifications ──────
  /api/v1/codebooks/cnb-rate:
    get:
      tags: [Codebooks]
      summary: ČNB exchange rate pro currency + datum
      parameters:
        - { in: query, name: currency, required: true, schema: { type: string, example: "EUR" } }
        - { in: query, name: date, required: true, schema: { type: string, format: date } }
      responses:
        '200': { description: 'rate + rate_date + fallback_used + source' }

  /api/v1/expense-categories:
    get:
      tags: [Codebooks]
      summary: Seznam kategorií nákladů
      description: Per-tenant číselník pro rozpad nákladů přijatých faktur (CRM/dashboard).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: include_archived, schema: { type: boolean, default: false }, description: Zahrnout archivované }
      responses:
        '200':
          description: Seznam kategorií
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ExpenseCategory' }
    post:
      tags: [Codebooks]
      summary: Vytvoří kategorii (admin)
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, label]
              properties:
                code: { type: string, maxLength: 20, pattern: '^[a-z0-9_-]+$' }
                label: { type: string, maxLength: 100 }
                fixed_or_var: { type: string, enum: [fixed, variable] }
                display_order: { type: integer }
      responses:
        '201': { description: Created }

  /api/v1/expense-categories/{id}:
    put:
      tags: [Codebooks]
      summary: Update kategorie nákladů
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: OK }
    delete:
      tags: [Codebooks]
      summary: Smaže (nebo archivuje pokud má reference) kategorii
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: Deleted/archived }

  /api/v1/revenue-categories:
    get:
      tags: [Codebooks]
      summary: Seznam kategorií tržeb
      description: Per-tenant číselník pro rozpad tržeb vydaných faktur (CRM/dashboard/Grafy).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: include_archived, schema: { type: boolean, default: false }, description: Zahrnout archivované }
      responses:
        '200':
          description: Seznam kategorií
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/RevenueCategory' }
    post:
      tags: [Codebooks]
      summary: Vytvoří kategorii tržby (admin)
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, label]
              properties:
                code: { type: string, maxLength: 20, pattern: '^[a-z0-9_-]+$' }
                label: { type: string, maxLength: 100 }
                display_order: { type: integer }
      responses:
        '201': { description: Created }

  /api/v1/revenue-categories/{id}:
    put:
      tags: [Codebooks]
      summary: Update kategorie tržby
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: OK }
    delete:
      tags: [Codebooks]
      summary: Smaže (nebo archivuje pokud má reference) kategorii tržby
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: Deleted/archived }

  /api/v1/vat-classifications:
    get:
      tags: [Codebooks]
      summary: VAT klasifikační kódy (DPHDP3 / KH / SH)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: direction, schema: { type: string, enum: [sale, purchase] } }
        - { in: query, name: include_archived, schema: { type: boolean } }
      responses:
        '200':
          description: List kódů (globální seed + tenant overrides)
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/VatClassification' }
    post:
      tags: [Codebooks]
      summary: Vytvoří custom VAT kód (per tenant)
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VatClassificationCreate' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VatClassification' }

  /api/v1/vat-classifications/{id}:
    put:
      tags: [Codebooks]
      summary: Update VAT kódu (jen tenant rows, globální seed read-only)
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VatClassificationInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VatClassification' }
        '403': { description: Globální seed je read-only }
    delete:
      tags: [Codebooks]
      summary: Smaže (nebo archivuje pokud má reference) VAT kód
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: Deleted/archived }

  # ── Roční daňové konstanty (globální číselník) ──────────────────────────
  /api/v1/codebooks/tax-constants:
    get:
      tags: [Codebooks]
      summary: Roční daňové konstanty (sazby, limity, paušály) za roky
      description: |
        GLOBÁLNÍ číselník (ne per-supplier) — efektivní hodnoty per rok s příznakem
        `is_override` (přepis výchozích konstant z migrace) vs. výchozí seed.
        Slouží daňovému optimalizátoru a EPO výkazům (DPFO/DPPO).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  years:
                    type: array
                    items: { $ref: '#/components/schemas/TaxConstantsYear' }

  /api/v1/codebooks/tax-constants/{year}:
    parameters:
      - in: path
        name: year
        required: true
        schema: { type: integer, minimum: 2018, maximum: 2100 }
    put:
      tags: [Codebooks]
      summary: Uloží override daňových konstant pro rok (jen admin)
      description: |
        Přepíše výchozí konstanty pro daný rok. Tělo lze poslat buď jako plochý
        objekt, nebo zabalené v `{ "data": { ... } }`. Skalární klíče jsou povinné
        (sazby, limity), vnořené (`band_ceilings`, `expense_caps`, `child_credits`)
        musí být objekty/pole. Paušální daň se posílá jako rozvrh měsíčních záloh
        `pausal_monthly` (roční `pausal_annual` je odvozená a neukládá se; přijme se
        jen kvůli zpětné kompatibilitě, když rozvrh chybí).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TaxConstantsInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  year:        { type: integer }
                  is_override: { type: boolean, example: true }
                  data:        { type: object }
        '403': { description: Pouze admin }
        '422': { description: Neplatný rok / validace selhala }
    delete:
      tags: [Codebooks]
      summary: Reset roku na výchozí konstanty (smaže override, jen admin)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  year:        { type: integer }
                  is_override: { type: boolean, example: false }
                  data:        { type: object }
        '403': { description: Pouze admin }

  # ── Purchase invoices — chybějící endpointy ──────────────────────────────
  /api/v1/purchase-invoices/export:
    get:
      tags: [Purchase invoices]
      summary: Hromadný export přijatých faktur za měsíc nebo čtvrtletí (PDF ZIP / ISDOC ZIP / Pohoda dataPack)
      description: |
        `pdf-zip`: ZIP s jedním PDF na fakturu. Preferuje archivovaný **originál**
        od dodavatele (`Prijata-{vs}-{vendor}.pdf`); pokud originál chybí, doplní
        **naši rekonstrukci** z dat faktury (`Prijata-{vs}-{vendor}-rekonstrukce.pdf`).
        Počet rekonstrukcí je v hlavičce `X-Export-Reconstructed`.

        `isdoc` / `pohoda`: generují XML z dat faktury (PDF nepotřebují).
        ISDOC obsahuje také evidovaný bankovní účet dodavatele a platební
        variabilní symbol. Číslo dokladu dodavatele je v kořenovém `ID`, interní
        číslo MyInvoice v namespacovaném `Extensions/myi:InternalDocumentNumber`.

        Období lze zadat legacy parametrem `month=YYYY-MM`, nebo explicitně
        `period=monthly&year=YYYY&month=M` / `period=quarterly&year=YYYY&quarter=1..4`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: month, schema: { oneOf: [{ type: string, pattern: '^\d{4}-\d{2}$' }, { type: integer, minimum: 1, maximum: 12 }] } }
        - { in: query, name: period, schema: { type: string, enum: [monthly, quarterly] } }
        - { in: query, name: year, schema: { type: integer, minimum: 1900, maximum: 2100 } }
        - { in: query, name: quarter, schema: { type: integer, minimum: 1, maximum: 4 } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf-zip, isdoc, pohoda] } }
        - { in: query, name: date_by, schema: { type: string, enum: [tax, issue, received] } }
      responses:
        '200':
          description: ZIP nebo XML stream
          headers:
            X-Export-Warnings:
              schema: { type: string }
              description: Per-invoice warnings (poškozený originál, selhání rekonstrukce)
            X-Export-Reconstructed:
              schema: { type: integer }
              description: Počet faktur, u kterých se místo originálu vložila naše rekonstrukce (jen pdf-zip)

  /api/v1/purchase-invoices/{id}/activity:
    get:
      tags: [Purchase invoices]
      summary: Activity log pro přijatou fakturu
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: List events }

  # ── CRM dashboard endpoints ──────────────────────────────────────────────
  /api/v1/crm/overview:
    get:
      tags: [CRM]
      summary: KPI overview (current month + last + YTD + trailing 12m + prev year per currency)
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses: { '200': { description: KPI } }

  /api/v1/crm/monthly:
    get:
      tags: [CRM]
      summary: Monthly history (revenue/costs/profit per měsíc)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12, minimum: 1, maximum: 36 } }
        - { in: query, name: currency, schema: { type: string } }
      responses: { '200': { description: Monthly rows } }

  /api/v1/crm/top-clients:
    get:
      tags: [CRM]
      summary: Top klienti podle revenue
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
        - { in: query, name: limit, schema: { type: integer, default: 10 } }
        - { in: query, name: currency, schema: { type: string } }
      responses: { '200': { description: Top klienti } }

  /api/v1/crm/top-vendors:
    get:
      tags: [CRM]
      summary: Top dodavatelé podle costs
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
        - { in: query, name: limit, schema: { type: integer, default: 10 } }
      responses: { '200': { description: Top dodavatelé } }

  /api/v1/crm/aging-receivables:
    get:
      tags: [CRM]
      summary: Aging buckets pohledávek
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses: { '200': { description: Buckets not_due / overdue_30 / 60 / 90 / 90+ } }

  /api/v1/crm/aging-payables:
    get:
      tags: [CRM]
      summary: Aging buckets závazků
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses: { '200': { description: Buckets } }

  /api/v1/crm/dso:
    get:
      tags: [CRM]
      summary: Days Sales Outstanding (průměrná doba inkasa)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
      responses: { '200': { description: 'avg_days + sample_size' } }

  /api/v1/crm/payment-punctuality:
    get:
      tags: [CRM]
      summary: "% faktur zaplacených včas"
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
      responses: { '200': { description: 'on_time_pct + counts' } }

  /api/v1/crm/concentration:
    get:
      tags: [CRM]
      summary: Riziko koncentrace (Top 1/3/5 klient share + Pareto count)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
        - { in: query, name: currency, schema: { type: string } }
      responses: { '200': { description: 'top1_share + pareto_80_count + risk_level' } }

  /api/v1/crm/expense-breakdown:
    get:
      tags: [CRM]
      summary: Náklady per kategorie
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
      responses: { '200': { description: Per-category totals } }

  /api/v1/crm/churn-risk:
    get:
      tags: [CRM]
      summary: Klienti dlouho bez objednávky
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: days, schema: { type: integer, default: 60 } }
        - { in: query, name: limit, schema: { type: integer, default: 20 } }
      responses: { '200': { description: Churn risk klienti } }

  /api/v1/crm/action-items:
    get:
      tags: [CRM]
      summary: Daily TODO (overdue, bank unmatched, recurring due, payables, purchase drafts, unbooked, tax/SHV deadlines, churn)
      description: |
        Vrací položky `items[]` (typ `type`: `overdue_invoices`, `bank_unmatched`,
        `recurring_due`, `overdue_payables`, `purchase_drafts`, `unbooked_documents`,
        `journal_integrity`, `tax_deadline`, `shv_deadline`, `churn_risk`), každá s
        `severity`, `title`, `hint`, `link` a volitelně `count`/`days`. Položka
        `unbooked_documents` (jen firmy v podvojném účetnictví) navíc nese
        `breakdown[]` s rozpadem `{ key: invoices|purchase_invoices|bank, count, link }`.
        Položka `journal_integrity` (též jen podvojné účetnictví) nese `breakdown[]`
        s typy nálezů `{ key: orphan_entry|unbalanced_entry|booked_without_entry|entry_without_booked|amount_mismatch, count, link }`
        z posledního běhu nočního integrity jobu. Plus `total` a `dismissed_count`.
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses: { '200': { description: 'items + total + dismissed_count' } }

  /api/v1/crm/cash-flow-forecast:
    get:
      tags: [CRM]
      summary: Cash flow prognóza N týdnů dopředu
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: weeks, schema: { type: integer, default: 4, minimum: 1, maximum: 12 } }
        - { in: query, name: currency, schema: { type: string, default: CZK } }
      responses: { '200': { description: 'weeks[] + total_in/out/net' } }

  /api/v1/crm/late-risk:
    get:
      tags: [CRM]
      summary: Late payment risk score per klient
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: limit, schema: { type: integer, default: 10 } }
      responses: { '200': { description: Top risky klienti } }

  /api/v1/crm/reminder-effectiveness:
    get:
      tags: [CRM]
      summary: Funnel zaplaceno po N. upomínce
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
      responses: { '200': { description: 'no_reminder + after_first/_second/_third_plus + never_paid' } }

  /api/v1/crm/payment-time-histogram:
    get:
      tags: [CRM]
      summary: Distribuce doby inkasa (buckets 0-7, 8-14, 15-30, 31-60, 61+)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
      responses: { '200': { description: 'buckets[] + median + p90' } }

  /api/v1/crm/recompute:
    post:
      tags: [CRM]
      summary: Manuální přepočet pre-aggregated stats (admin)
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses:
        '200': { description: 'ok + elapsed_ms' }
        '403': { description: Pouze admin }

  /api/v1/crm/tax-calendar:
    get:
      tags: [CRM]
      summary: Daňový kalendář — DPH/KH/SH + zálohy (E9) + roční DPFO/DPPO
      description: |
        Fáze F (audit 2026-07 P2/S). Širší okno než action-items; každá položka
        (krom `tax_advance`) nese `submitted`/`submitted_at` odvozené z archivu
        `tax_submissions`. Typy: `tax_deadline` (DPH), `kh_deadline`, `shv_deadline`,
        `tax_advance` (zálohy E9, má `status` planned|paid a `amount` místo submitted),
        `income_tax_deadline` (roční DPFO/DPPO — standardní termíny 1.4./1.-2.5.,
        bez varianty "s poradcem"; vynecháno pro OSVČ v paušálním režimu).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: max_days_ahead, schema: { type: integer, default: 90, minimum: 1, maximum: 365 } }
      responses: { '200': { description: 'items[] + total' } }

  # ─────────────────────────────── Portfolio ───────────────────────────────
  /api/v1/portfolio/overview:
    get:
      tags: [Portfolio]
      summary: Přehled firem pro účetní kancelář (cross-supplier)
      description: |
        Fáze F (audit 2026-07 P2/M). NENÍ scoped na X-Supplier-Id — agreguje přes
        firmy z `user_suppliers` přihlášeného uživatele (admin/uživatel bez membership
        řádků vidí vše). Per firma: nejbližší daňový termín, počty nezaúčtovaných
        dokladů / nespárovaných bankovních plateb / konceptů PF, stav účetního období
        a datum posledního importu bankovního výpisu. Role client bez přístupu.
      responses: { '200': { description: 'companies[] + total + generated_at' } }

  # ─────────────────────────────── Dokumenty ───────────────────────────────
  /api/v1/documents:
    get:
      tags: [Documents]
      summary: Seznam dokumentů a podsložek v dané složce
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: folder_id, in: query, schema: { type: [integer, "null"] }, description: 'NULL/prázdné = kořen' }
        - { name: doc_type, in: query, schema: { type: string, enum: [pdf, docx, xlsx, xml, zfo, p7s, zip, image, other] } }
      responses:
        '200':
          description: Výpis složky
          content:
            application/json:
              schema:
                type: object
                properties:
                  breadcrumb: { type: array, items: { $ref: '#/components/schemas/DocumentBreadcrumb' } }
                  folders: { type: array, items: { $ref: '#/components/schemas/DocumentFolder' } }
                  documents: { type: array, items: { $ref: '#/components/schemas/Document' } }
                  max_file_bytes: { type: integer }
    post:
      tags: [Documents]
      summary: Nahrání souborů (multipart). ZFO se automaticky rozbalí; ZIP dle zip_mode.
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                'file[]': { type: array, items: { type: string, format: binary } }
                folder_id: { type: [integer, "null"] }
                zip_mode: { type: string, enum: [explode, keep], default: keep }
                'relpaths[]': { type: array, items: { type: string }, description: 'Relativní cesty složek pro upload adresáře' }
      responses:
        '200':
          description: Souhrn nahrání
          content:
            application/json:
              schema:
                type: object
                properties:
                  created: { type: integer }
                  skipped: { type: array, items: { type: object } }
                  errors: { type: array, items: { type: object } }
  /api/v1/documents/search:
    get:
      tags: [Documents]
      summary: Fulltextové vyhledávání v názvech, popisech a obsahu (MariaDB FULLTEXT)
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
      responses:
        '200':
          description: Výsledky
          content:
            application/json:
              schema:
                type: object
                properties:
                  documents: { type: array, items: { $ref: '#/components/schemas/Document' } }
                  query: { type: string }
  /api/v1/documents/link-search:
    get:
      tags: [Documents]
      summary: Našeptávač pro párování — hledá faktury, přijaté faktury, klienty a projekty
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: types, in: query, schema: { type: string }, description: 'CSV filtr: invoice,purchase_invoice,client,project' }
      responses:
        '200':
          description: Kandidáti pro vazbu
          content:
            application/json:
              schema:
                type: object
                properties:
                  results: { type: array, items: { $ref: '#/components/schemas/DocumentLinkCandidate' } }
                  query: { type: string }
  /api/v1/documents/{id}:
    get:
      tags: [Documents]
      summary: Detail dokumentu (vč. tagů, vazeb, příloh a ZFO metadat)
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Dokument, content: { application/json: { schema: { $ref: '#/components/schemas/DocumentDetail' } } } }
        '404': { description: Nenalezeno }
    patch:
      tags: [Documents]
      summary: Editace metadat (název, popis, tagy)
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                description: { type: [string, "null"] }
                tags: { type: array, items: { type: string } }
      responses:
        '200': { description: Aktualizovaný dokument, content: { application/json: { schema: { $ref: '#/components/schemas/DocumentDetail' } } } }
    delete:
      tags: [Documents]
      summary: Přesun do koše (soft-delete)
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: ok }
  /api/v1/documents/{id}/download:
    get:
      tags: [Documents]
      summary: Stažení souboru (Content-Disposition attachment)
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Binární obsah souboru, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
  /api/v1/documents/{id}/links:
    post:
      tags: [Documents]
      summary: Připojit dokument k entitě (faktura / přijatá faktura / klient / projekt)
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entity_type, entity_id]
              properties:
                entity_type: { type: string, enum: [client, invoice, purchase_invoice, project] }
                entity_id: { type: integer }
      responses:
        '200': { description: Aktuální vazby, content: { application/json: { schema: { type: object, properties: { links: { type: array, items: { $ref: '#/components/schemas/DocumentLink' } } } } } } }
    delete:
      tags: [Documents]
      summary: Odpojit vazbu dokumentu na entitu
      description: '`entity_type`/`entity_id` lze poslat v těle nebo jako query parametry.'
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                entity_type: { type: string, enum: [client, invoice, purchase_invoice, project] }
                entity_id: { type: integer }
      responses:
        '200': { description: Aktuální vazby, content: { application/json: { schema: { type: object, properties: { links: { type: array, items: { $ref: '#/components/schemas/DocumentLink' } } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/documents/by-entity/{type}/{id}:
    get:
      tags: [Documents]
      summary: Dokumenty navázané na entitu (oboustranné párování)
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: type, in: path, required: true, schema: { type: string, enum: [client, invoice, purchase_invoice, project] } }
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Navázané dokumenty, content: { application/json: { schema: { type: object, properties: { documents: { type: array, items: { $ref: '#/components/schemas/Document' } } } } } } }
  /api/v1/document-folders:
    get:
      tags: [Documents]
      summary: Strom složek (bez parametru celý plochý seznam, s parent_id přímé potomky)
      parameters:
        - { $ref: '#/components/parameters/SupplierIdHeader' }
        - { name: parent_id, in: query, schema: { type: [integer, "null"] } }
      responses:
        '200': { description: Složky, content: { application/json: { schema: { type: object, properties: { folders: { type: array, items: { $ref: '#/components/schemas/DocumentFolder' } } } } } } }
    post:
      tags: [Documents]
      summary: Vytvoření složky
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                parent_id: { type: [integer, "null"] }
      responses:
        '200': { description: ok }

  # ─────────────────────────── Pravidelné fakturace ────────────────────────
  /api/v1/recurring:
    get:
      tags: [Recurring]
      summary: Seznam šablon pravidelných fakturací
      description: |
        Stránkovaný seznam šablon aktuálního supplier-a. `meta.summary` obsahuje
        součet částek per měna + přepočet na CZK (dnešní ČNB kurz, pokud je víc měn).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: client_id, schema: { type: integer } }
        - { in: query, name: status, schema: { type: string, enum: [active, paused, expired] } }
        - { in: query, name: sort, schema: { type: string, enum: [client, next_run, amount_czk] } }
        - { in: query, name: page, schema: { type: integer, minimum: 1, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 5, maximum: 200, default: 50 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/RecurringTemplate' }
                  meta: { type: object }
    post:
      tags: [Recurring]
      summary: Vytvoří šablonu pravidelné fakturace
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecurringTemplateInput' }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecurringTemplate' }
        '400':
          $ref: '#/components/responses/ValidationFailed'

  /api/v1/recurring/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Recurring]
      summary: Detail šablony
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecurringTemplate' }
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Recurring]
      summary: Aktualizace šablony
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecurringTemplateInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecurringTemplate' }
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Recurring]
      summary: Smaže šablonu
      responses:
        '200': { description: 'OK ({ deleted: true })' }
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/recurring/{id}/invoices:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Recurring]
      summary: Faktury vygenerované z této šablony
      description: Flat list (poslední první) — id, varsymbol, typ, stav, data, částky, currency.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { type: object } }

  /api/v1/recurring/{id}/pause:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Recurring]
      summary: Pozastaví šablonu (status → paused)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecurringTemplate' }

  /api/v1/recurring/{id}/resume:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Recurring]
      summary: Obnoví pozastavenou šablonu (status → active, přepočet next_run)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecurringTemplate' }
        '409': { description: 'Šablona vypršela (next_run > end_date)' }

  /api/v1/recurring/{id}/run-now:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Recurring]
      summary: Manuální spuštění šablony (vygeneruje fakturu / koncept)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                issue_date: { type: string, format: date, description: Vynucené datum vystavení }
                draft: { type: boolean, description: 'true = vytvořit koncept i u šablony s auto_issue' }
      responses:
        '201':
          description: 'Vygenerováno (invoice_id, varsymbol, issued, sent_to, new_next_run_date, template_status)'
        '409': { description: Šablonu nelze spustit (vypršela / chyba generování) }

  # ── Vydané faktury — párování zálohy ─────────────────────────────────────
  /api/v1/invoices/{id}/advance-candidates:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [InvoiceOps]
      summary: Nespárované zálohové faktury (proforma) téhož odběratele
      description: Kandidáti pro propojení daňového dokladu se zálohou.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates: { type: array, items: { type: object } }

  /api/v1/invoices/{id}/final-candidates:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [InvoiceOps]
      summary: Daňové doklady, se kterými lze tuto zálohu (proforma) spárovat
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates: { type: array, items: { type: object } }

  /api/v1/invoices/{id}/link-advance:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [InvoiceOps]
      summary: Propojí daňový doklad se zálohovou fakturou
      description: Uloží `parent_invoice_id` a doplní `advance_paid_amount`, pokud na faktuře ještě žádná záloha není.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advance_id]
              properties:
                advance_id: { type: integer, format: int64 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }
        '409': { description: Propojení selhalo }
    delete:
      tags: [InvoiceOps]
      summary: Zruší propojení se zálohou
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }

  # ── Přijaté faktury — párování zálohy ────────────────────────────────────
  /api/v1/purchase-invoices/{id}/advance-candidates:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Purchase invoices]
      summary: Nespárované zálohové přijaté faktury téhož dodavatele
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates: { type: array, items: { type: object } }

  /api/v1/purchase-invoices/{id}/settlement-candidates:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Purchase invoices]
      summary: Vyúčtovací faktury, se kterými lze tuto zálohu spárovat (opačný směr)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates: { type: array, items: { type: object } }

  /api/v1/purchase-invoices/{id}/link-advance:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Purchase invoices]
      summary: Propojí finální přijatou fakturu se zálohovou (proti dvojímu nákladu)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advance_id]
              properties:
                advance_id: { type: integer, format: int64 }
      responses:
        '200': { description: 'OK (aktualizovaná faktura vč. linked_advance)' }
        '409': { description: Propojení selhalo }
    delete:
      tags: [Purchase invoices]
      summary: Zruší propojení se zálohou
      responses:
        '200': { description: OK }

  /api/v1/purchase-invoices/{id}/advance-suggestion:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    delete:
      tags: [Purchase invoices]
      summary: Zamítne automatický návrh na spárování zálohy
      responses:
        '200': { description: OK }

  /api/v1/purchase-invoices/{id}/dismiss-extraction-warning:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Purchase invoices]
      summary: Skryje varování z AI/automatické extrakce dokladu
      responses:
        '200': { description: OK }

  # ── CRM dashboard — doplňující endpointy ─────────────────────────────────
  /api/v1/crm/yearly:
    get:
      tags: [CRM]
      summary: Historie tržeb/nákladů/zisku po rocích
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: currency, schema: { type: string } }
      responses: { '200': { description: Roční řady } }

  /api/v1/crm/dpo:
    get:
      tags: [CRM]
      summary: Days Payable Outstanding (Ø doba úhrady dodavatelům)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
      responses: { '200': { description: 'avg_days + sample_size' } }

  /api/v1/crm/vendor-concentration:
    get:
      tags: [CRM]
      summary: Riziko koncentrace nákladů (Top dodavatelé share + Pareto)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
        - { in: query, name: currency, schema: { type: string } }
      responses: { '200': { description: 'top1_share + pareto_80_count + risk_level' } }

  /api/v1/crm/revenue-breakdown:
    get:
      tags: [CRM]
      summary: Tržby per kategorie tržeb
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: months, schema: { type: integer, default: 12 } }
        - { in: query, name: currency, schema: { type: string } }
      responses: { '200': { description: Per-category totals } }

  /api/v1/crm/action-items/dismiss:
    post:
      tags: [CRM]
      summary: Skryje položku action-items (den / týden / natrvalo)
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [item_type, mode]
              properties:
                item_type: { type: string }
                mode: { type: string, description: 'day | week | forever | historical' }
      responses:
        '200': { description: ok }
        '400': { description: Neplatný vstup }

  /api/v1/crm/action-items/restore:
    post:
      tags: [CRM]
      summary: Vrátí (un-dismiss) skrytou položku action-items
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [item_type]
              properties:
                item_type: { type: string }
      responses:
        '200': { description: ok }

  /api/v1/crm/action-items/restore-all:
    post:
      tags: [CRM]
      summary: Vrátí všechny skryté položky action-items aktuálního uživatele
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses:
        '200': { description: 'ok + restored (počet)' }

  # ── Dashboard náklady ────────────────────────────────────────────────────
  /api/v1/dashboard/purchase-summary:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Reports]
      summary: Dashboard nákladů (zrcadlo summary nad přijatými fakturami)
      description: |
        Agregace pro stránku **Náklady** nad `purchase_invoices`: KPI per měna
        (this/prev year + YoY), po splatnosti/nezaplacené závazky, top dodavatelé
        (YTD/loni/rolling 12m), náklady po měsících/rocích, rolling 12m, cash-outflow,
        aging závazků, forecast, rozpad DPH na vstupu (12m), rozpad nákladů po
        kategoriích, histogramy. VAT-aware (`is_vat_payer`). Zálohy se vyřazují
        z nákladů dle cash sémantiky (zaplaceno a nespárováno).
      responses:
        '200': { description: OK }

  # ── Daňový optimalizátor ─────────────────────────────────────────────────
  /api/v1/tax/analysis:
    get:
      tags: [Tax]
      summary: Daňová analýza za rok (retrospektiva nebo projekce)
      description: |
        Uzavřený rok → `mode: retrospective` (srovnání režimů na skutečném příjmu +
        YoY). Běžící rok → `mode: forecast` (YTD příjem + projekce a sledování limitů
        DPH/paušál). Vrací `profile`, `constants`, `available_years`, `is_vat_payer`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, schema: { type: integer }, description: 'Default = aktuální rok' }
      responses:
        '200': { description: OK }
        '400': { description: Žádný supplier scope }
        '422': { description: Neplatný rok }

  /api/v1/tax/profile:
    put:
      tags: [Tax]
      summary: Uloží daňový profil supplier-a pro rok
      description: |
        Profil pro optimalizátor — paušál/skutečné výdaje, sleva na manžela, počet
        dětí, úroky z hypotéky, penzijní/životní pojištění, dary, pásmo paušální daně.
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TaxProfileInput' }
      responses:
        '200': { description: 'OK ({ profile: {...} })' }
        '400': { description: Žádný supplier scope }
        '422': { description: Neplatný rok }

  # ── Klienti / vyhledávání — doplňující endpointy ─────────────────────────
  /api/v1/clients/{id}/vat-status:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Lookup]
      summary: Online ověření plátcovství DPH klienta (ARES dle IČO / VIES dle DIČ)
      description: |
        Ověří a uloží `clients.is_vat_payer`. Když registr nerozhodne, vrátí dosud
        uložený příznak. ARES/VIES cache (24 h) → opakované volání nezatěžuje registry.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer, format: int64 }
                  is_vat_payer: { type: boolean }
                  source: { type: string, enum: [ares, vies, unknown] }
                  ic: { type: [string, "null"] }
                  dic: { type: [string, "null"] }
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/clients/lookup-bank:
    post:
      tags: [Lookup]
      summary: Zveřejněné bankovní účty z registru plátců DPH (CRPDPH) dle DIČ
      description: |
        Vrátí účty zveřejněné v registru plátců DPH (MFČR) + příznak nespolehlivého
        plátce. Vhodné při zakládání dodavatele (předvyplnění účtu z DIČ).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [dic]
              properties:
                dic:
                  type: string
                  description: DIČ — 8–10 číslic (s i bez prefixu CZ)
                  example: "CZ27082440"
      responses:
        '200': { description: 'OK (účty + nespolehlivý plátce)' }
        '400': { description: Neplatné DIČ }

  /api/v1/search:
    get:
      tags: [Lookup]
      summary: Globální vyhledávač (klienti/dodavatelé + vydané/přijaté faktury)
      description: Min. 2 znaky, jinak prázdné výsledky. Tenant scope dle supplier-a.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: q, required: true, schema: { type: string, minLength: 2 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  q: { type: string }
                  clients: { type: array, items: { type: object } }
                  invoices: { type: array, items: { type: object } }
                  purchase_invoices: { type: array, items: { type: object } }

  # ── Reports — doplňující endpointy ───────────────────────────────────────
  /api/v1/reports/dphdp3/settings:
    get:
      tags: [Reports]
      summary: Nastavení relevantní pro DPH přiznání (perioda, kód FÚ, plátcovství)
      parameters: [{ $ref: '#/components/parameters/SupplierIdHeader' }]
      responses: { '200': { description: Settings } }

  /api/v1/reports/dphdp3/trend:
    get:
      tags: [Reports]
      summary: Trend daňové povinnosti DPH za více období
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, schema: { type: integer } }
      responses: { '200': { description: Časová řada } }

  /api/v1/reports/dphdp3/drafts-prediction:
    get:
      tags: [Reports]
      summary: Predikce dopadu konceptů na příští DPH přiznání
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, schema: { type: integer } }
        - { in: query, name: month, schema: { type: integer, minimum: 1, maximum: 12 } }
        - { in: query, name: quarter, schema: { type: integer, minimum: 1, maximum: 4 } }
      responses: { '200': { description: Predikce } }

  /api/v1/reports/dphdp3/post-filing-changes:
    get:
      tags: [Reports]
      summary: Doklady změněné po podání DPH přiznání (kandidáti na dodatečné přiznání)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer } }
        - { in: query, name: month, schema: { type: integer, minimum: 1, maximum: 12 } }
        - { in: query, name: period, schema: { type: string, enum: [monthly, quarterly] } }
      responses:
        '200': { description: "OK {has_filing, snapshot_available, submission, documents}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatné parametry období, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '403': { description: Pouze admin/accountant }

  /api/v1/reports/vat-coefficient:
    get:
      tags: [Reports]
      summary: § 76 koeficient krácení nároku na odpočet DPH
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer, minimum: 2020, maximum: 2050 } }
      responses:
        '200': { description: "OK {year, provisional_percent, resolved_provisional_percent, carried_forward, final_percent, numerator_czk, denominator_czk, settled_at}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    put:
      tags: [Reports]
      summary: Nastaví zálohový koeficient na rok
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [year, provisional_percent]
              properties:
                year: { type: integer }
                provisional_percent: { type: integer, minimum: 0, maximum: 100 }
      responses:
        '200': { description: "OK {year, provisional_percent}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/reports/vat-coefficient/settle:
    post:
      tags: [Reports]
      summary: Roční vypořádání koeficientu §76
      description: Admin-only.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [year]
              properties:
                year: { type: integer }
      responses:
        '200': { description: "OK {year, final_percent, numerator_czk, denominator_czk}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { description: Neplatný rok nebo chybí podklady, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/reports/cnb-rate-audit:
    get:
      tags: [Reports]
      summary: Audit kurzů vs. ČNB — cizoměnové doklady s odchylkou (§C / K4)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: threshold, schema: { type: number }, description: "Práh odchylky v %, default ~0.5" }
      responses:
        '200': { description: "OK {from, to, threshold_percent, items, missing_cnb_count, fixed_mode_skipped}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/reports/dph-book/preview:
    get:
      tags: [Reports]
      summary: Náhled Knihy DPH (interní VAT žurnál, vždy měsíční)
      description: Interní pomůcka — NENÍ EPO podání.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer } }
        - { in: query, name: month, required: true, schema: { type: integer, minimum: 1, maximum: 12 } }
      responses: { '200': { description: Summary řádků žurnálu } }

  /api/v1/reports/dph-book:
    get:
      tags: [Reports]
      summary: Stáhne PDF Knihy DPH za měsíc
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer } }
        - { in: query, name: month, required: true, schema: { type: integer, minimum: 1, maximum: 12 } }
      responses:
        '200':
          description: PDF download
          content:
            application/pdf:
              schema: { type: string, format: binary }

  /api/v1/reports/oss/preview:
    get:
      tags: [Reports]
      summary: Náhled OSS přiznání
      description: Kvartální podklad One Stop Shop z ručně označených řádků vydaných faktur. Vyžaduje zapnutý OSS režim v nastavení firmy.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer, minimum: 2020, maximum: 2050 } }
        - { in: query, name: quarter, required: true, schema: { type: integer, minimum: 1, maximum: 4 } }
      responses:
        '200':
          description: OSS souhrn podle zemí a sazeb
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OssPreview' }
        '409': { description: OSS režim není v nastavení firmy aktivní (`oss_disabled`) }

  /api/v1/reports/oss:
    get:
      tags: [Reports]
      summary: OSS XML export
      description: EPO XML pro OSS EU režim (OSSEI1) z ručně označených řádků vydaných faktur. Běžná plnění generuje jako VetaR a opravy minulých období jako VetaO. Vyžaduje zapnutý OSS režim v nastavení firmy.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer, minimum: 2020, maximum: 2050 } }
        - { in: query, name: quarter, required: true, schema: { type: integer, minimum: 1, maximum: 4 } }
      responses:
        '200':
          description: XML download
          content:
            application/xml: {}
        '409': { description: OSS režim není v nastavení firmy aktivní (`oss_disabled`) }

  /api/v1/logbook/cars:
    get:
      tags: [Logbook]
      summary: Seznam vozidel
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: include_archived, schema: { type: boolean, default: false } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/Car' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Logbook]
      summary: Vytvoří vozidlo
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CarInput' }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Car' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '403': { $ref: '#/components/responses/InsufficientScope' }

  /api/v1/logbook/cars/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [Logbook]
      summary: Detail vozidla
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Car' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Logbook]
      summary: Upraví vozidlo
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CarInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Car' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Logbook]
      summary: Smaže vozidlo (nebo archivuje, je-li použito)
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/logbook/trips:
    get:
      tags: [Logbook]
      summary: Seznam jízd
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: car_id, schema: { type: integer } }
        - { in: query, name: category_id, schema: { type: integer } }
        - { in: query, name: year, schema: { type: integer } }
        - { in: query, name: month, schema: { type: integer } }
        - { in: query, name: date_from, schema: { type: string, format: date } }
        - { in: query, name: date_to, schema: { type: string, format: date } }
        - { in: query, name: q, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/Trip' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Logbook]
      summary: Vytvoří jízdu
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TripInput' }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Trip' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '403': { $ref: '#/components/responses/InsufficientScope' }

  /api/v1/logbook/trips/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [Logbook]
      summary: Detail jízdy
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Trip' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Logbook]
      summary: Upraví jízdu
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TripInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Trip' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Logbook]
      summary: Smaže jízdu
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/logbook/fuelings:
    get:
      tags: [Logbook]
      summary: Seznam tankování
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: car_id, schema: { type: integer } }
        - { in: query, name: source, schema: { type: string } }
        - { in: query, name: year, schema: { type: integer } }
        - { in: query, name: month, schema: { type: integer } }
        - { in: query, name: date_from, schema: { type: string, format: date } }
        - { in: query, name: date_to, schema: { type: string, format: date } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/Fueling' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Logbook]
      summary: Vytvoří tankování (ruční)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FuelingInput' }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Fueling' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '403': { $ref: '#/components/responses/InsufficientScope' }

  /api/v1/logbook/fuelings/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [Logbook]
      summary: Detail tankování
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Fueling' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Logbook]
      summary: Upraví tankování
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FuelingInput' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Fueling' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Logbook]
      summary: Smaže tankování
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/logbook/trip-categories:
    get:
      tags: [Logbook]
      summary: Číselník kategorií cest
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: include_archived, schema: { type: boolean, default: false } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/TripCategory' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Logbook]
      summary: Vytvoří kategorii cesty
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, label]
              properties:
                code: { type: string, description: 'A-Z, a-z, 0-9, _, - (max 30 znaků)' }
                label: { type: string, description: 'Max 100 znaků' }
      responses:
        '201': { description: Vytvořeno }
        '409': { description: 'Kód už existuje' }
        '422':
          $ref: '#/components/responses/ValidationFailed'

  /api/v1/logbook/summary:
    get:
      tags: [Logbook]
      summary: Roční daňový/účetní souhrn (per vozidlo)
      description: |
        Souhrn za zvolený rok: ujeto km, rozpad služební/soukromé (poměr pro krácení),
        stav tachometru počítaný z jízd, tankování (litry, náklad, spotřeba),
        kontrola návaznosti tachometru a informativní srovnání s paušálem na dopravu.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, schema: { type: integer } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LogbookSummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ── Podvojné účetnictví (Epic F1) ─────────────────────────────────────────
  /api/v1/accounting/accounts:
    get:
      tags: [Accounting]
      summary: Účtová osnova (seznam nebo strom)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: tree, description: "1 = vrátí strom (children)", schema: { type: boolean } }
        - { in: query, name: include_inactive, description: "1 = i neaktivní účty", schema: { type: boolean } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ChartAccount' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Accounting]
      summary: Vytvoří analytický účet (dítě syntetiky)
      description: Účetní/admin. Typ i normální stranu dědí účet po rodiči.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [parent_id, account_code, name]
              properties:
                parent_id: { type: integer }
                account_code: { type: string, example: "518001" }
                name: { type: string, example: "Software — předplatné" }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChartAccount' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Účet s tímto kódem už existuje, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/accounts/{id}:
    patch:
      tags: [Accounting]
      summary: Přejmenování / deaktivace účtu
      description: Účetní/admin. Účty se nemažou, jen deaktivují (is_active=false).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                is_active: { type: boolean }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChartAccount' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods:
    get:
      tags: [Accounting]
      summary: Seznam účetních období
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AccountingPeriod' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Accounting]
      summary: Vytvoří účetní období
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fiscal_year, starts_on, ends_on]
              properties:
                fiscal_year: { type: integer, example: 2026 }
                starts_on: { type: string, format: date, example: "2026-01-01" }
                ends_on: { type: string, format: date, example: "2026-12-31" }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountingPeriod' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Období pro rok už existuje, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/status:
    post:
      tags: [Accounting]
      summary: Přechody stavu období (matice Epic F4)
      description: |
        Admin-only. Přes /status jdou pouze `closed→approved` (confirm=true),
        `closed→open` (reopen — povinný reason ≥ 10 znaků; blokováno 422
        `closing_entries_exist`, dokud existují závěrkové zápisy) a
        `approved→closed` (zrušení schválení — confirm + reason). Přechody
        open↔closing a closing→closed patří uzávěrkovému průvodci (422
        `use_closing_workflow`). Každá změna vyžaduje `row_version` (optimistická
        konkurence) — konflikt vrací 409 `version_conflict`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status, row_version]
              properties:
                status: { type: string, enum: [open, closing, closed, approved] }
                row_version: { type: integer, minimum: 1 }
                reason: { type: string, minLength: 10, description: "Povinný u closed→open a approved→closed (§17/7 ZoÚ)" }
                confirm: { type: boolean, description: "Povinný true u closed→approved a approved→closed" }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountingPeriod' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict — období mezitím změněno, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal:
    get:
      tags: [Accounting]
      summary: Stránkovaný seznam účetních zápisů
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 200 } }
        - { in: query, name: document_no, description: Částečná shoda čísla dokladu, schema: { type: string, maxLength: 50 } }
        - { in: query, name: period_id, schema: { type: integer } }
        - { in: query, name: date_from, schema: { type: string, format: date } }
        - { in: query, name: date_to, schema: { type: string, format: date } }
        - { in: query, name: source_type, schema: { type: string, enum: [invoice, purchase_invoice, bank, cash, asset, manual, closing, opening, stock, provision, income_tax, profit_distribution] } }
        - { in: query, name: source_id, schema: { type: integer } }
        - { in: query, name: posted, description: "true = jen zaúčtované, false = jen koncepty", schema: { type: boolean } }
        - { in: query, name: automation, description: Původ zápisu, schema: { type: string, enum: [auto, approved, manual] } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: '#/components/schemas/JournalEntry' } }
                  total: { type: integer }
                  page: { type: integer }
                  per_page: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Accounting]
      summary: Ruční účetní zápis
      description: |
        Účetní/admin. Zaúčtuje přes PostingService (source_type=manual). Σ MD musí
        rovnat Σ D, období musí být otevřené — jinak 422 se strojovým kódem.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ManualJournalEntry' }
      responses:
        '201':
          description: Vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JournalEntry' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422':
          description: Nevyvážený zápis / zavřené období / neznámý účet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/accounting/journal/{id}:
    get:
      tags: [Accounting]
      summary: Detail zápisu + řádky (kód/název účtu, měna)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200':
          description: OK
          headers:
            ETag:
              description: row_version zápisu — použij jako If-Match při PATCH .../description.
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JournalEntryDetail' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Accounting]
      summary: Smaže podporovaný účetní zápis z otevřeného období bez protizápisu
      description: >-
        Účetní/admin. Fyzicky odstraní ruční zápis, zápis vydané nebo přijaté
        faktury, bankovního pohybu nebo posledního odpisu bez protizápisu. Fakturu
        odemkne; bankovní pohyb vrátí do fronty nezaúčtovaných. U odpisu odstraní
        zápis i účetní a nepřerušený daňový řádek stejného roku; existující
        přerušení zachová. Odpisy lze mazat pouze odzadu a u nevyřazeného majetku.
        Vždy jen pro otevřené období, mimo uzamčené datum a u zápisu bez storna.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Zápis smazán a zdrojový doklad odúčtován
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok: { type: boolean }
                  asset_id: { type: [integer, "null"], description: ID majetku při smazání odpisu }
                  fiscal_year: { type: [integer, "null"], description: Smazaný rok odpisu }
                  pause_preserved: { type: [boolean, "null"], description: Zda bylo zachováno přerušení daňového odpisu }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Období není otevřené, datum je uzamčené, zápis je stornovaný, existuje pozdější odpis nebo typ zdroje nepodporuje přímé smazání
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/accounting/journal/post-invoice/{id}:
    post:
      tags: [Accounting]
      summary: Zaúčtuje vydanou fakturu (idempotentní)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Zaúčtováno (idempotentní — opakování vrací týž zápis)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JournalEntry' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal/post-purchase/{id}:
    post:
      tags: [Accounting]
      summary: Zaúčtuje přijatou fakturu (idempotentní)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Zaúčtováno (idempotentní)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JournalEntry' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal/post-invoices-bulk:
    post:
      tags: [Accounting]
      summary: Hromadně zaúčtuje vydané faktury z výběru
      description: >-
        Účetní/admin. Pro každé ID zaúčtuje vydanou fakturu (idempotentní).
        Jeden neúspěšný doklad nezablokuje zbytek dávky — vrací souhrnný report
        `posted` / `failed` (per-doklad strojový kód chyby).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  items: { type: integer }
      responses:
        '200':
          description: Souhrnný report zaúčtování
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BulkPostReport' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal/post-purchases-bulk:
    post:
      tags: [Accounting]
      summary: Hromadně zaúčtuje přijaté faktury z výběru
      description: >-
        Účetní/admin. Pro každé ID zaúčtuje přijatou fakturu (idempotentní).
        Jeden neúspěšný doklad nezablokuje zbytek dávky — vrací souhrnný report
        `posted` / `failed` (per-doklad strojový kód chyby).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  items: { type: integer }
      responses:
        '200':
          description: Souhrnný report zaúčtování
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BulkPostReport' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal/{id}/reverse:
    post:
      tags: [Accounting]
      summary: Storno zápisu (zrcadlový protizápis)
      description: Účetní/admin. Vytvoří opačný zápis a naváže reversed_by (§35).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                entry_date: { type: string, format: date }
                description: { type: string }
      responses:
        '201':
          description: Storno vytvořeno
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JournalEntry' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422':
          description: Zápis neexistuje / už stornován / koncept / zavřené období
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/accounting/posting-rules:
    get:
      tags: [Accounting]
      summary: Efektivní mapa kontačních pravidel (globální + override)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: OK (objekt rule_key → pravidlo)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: { $ref: '#/components/schemas/PostingRule' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/accounting/posting-rules/{rule_key}:
    put:
      tags: [Accounting]
      summary: Per-tenant override kontačního pravidla
      description: Účetní/admin. Účty (CODE) musí existovat v osnově firmy.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: rule_key, required: true, schema: { type: string }, example: "invoice.services.issued" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                debit_account_code: { type: [string, "null"] }
                credit_account_code: { type: [string, "null"] }
                description: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PostingRule' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal-templates:
    get:
      tags: [Accounting]
      summary: Seznam šablon ručních zápisů (mzdový můstek)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { items: { type: array, items: { type: object } } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Accounting]
      summary: Vytvoří šablonu ručního zápisu
      description: Účetní/admin. `lines` = 1–100 řádků MD/Dal.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, lines]
              properties:
                name: { type: string, minLength: 1, maxLength: 255 }
                description: { type: string, maxLength: 255 }
                lines:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [account_code, side]
                    properties:
                      account_code: { type: string, minLength: 1, maxLength: 20 }
                      side: { type: string, enum: [debit, credit] }
                      amount: { type: number, minimum: 0 }
                      label: { type: string }
                      cost_center: { type: string }
      responses:
        '201': { description: Vytvořená šablona, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal-templates/{id}:
    get:
      tags: [Accounting]
      summary: Detail šablony vč. řádků
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Accounting]
      summary: Upraví šablonu a její řádky
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, lines]
              properties:
                name: { type: string, minLength: 1, maxLength: 255 }
                description: { type: string, maxLength: 255 }
                lines:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [account_code, side]
                    properties:
                      account_code: { type: string }
                      side: { type: string, enum: [debit, credit] }
                      amount: { type: number, minimum: 0 }
                      label: { type: string }
                      cost_center: { type: string }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [Accounting]
      summary: Smaže šablonu
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { ok: { type: boolean } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/journal-templates/{id}/import-csv:
    post:
      tags: [Accounting]
      summary: Napáruje CSV rekapitulaci mezd na řádky šablony (náhled, nic nezapisuje)
      description: Účetní/admin. Multipart upload CSV/TXT, max 512 kB.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        '200': { description: "Náhled napárovaných řádků (částky z CSV)", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '415': { description: Nepodporovaný formát souboru, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/accounting/payroll/preview:
    post:
      tags: [Accounting]
      summary: Náhled rozpadu hrubé mzdy na účty
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [year, month, gross]
              properties:
                year: { type: integer, minimum: 2018, maximum: 2100 }
                month: { type: integer, minimum: 1, maximum: 12 }
                gross: { type: number, minimum: 0, maximum: 10000000 }
                taxpayer_type: { type: string, default: employee }
      responses:
        '200': { description: "Rozpad hrubé mzdy po účtech", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/payroll/post:
    post:
      tags: [Accounting]
      summary: Zaúčtuje rozpad mzdy (idempotentní na RRRRMM)
      description: Účetní/admin. Účtuje přes PostingService se source_id = RRRRMM.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [year, month, gross]
              properties:
                year: { type: integer, minimum: 2018, maximum: 2100 }
                month: { type: integer, minimum: 1, maximum: 12 }
                gross: { type: number, minimum: 0, maximum: 10000000 }
                taxpayer_type: { type: string, default: employee }
                employee_id: { type: [integer, "null"] }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Zápis za dané období už existuje / uzavřené období, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/payroll/employees:
    get:
      tags: [Accounting]
      summary: Seznam zaměstnanců pro mzdový list (§38j)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: active, schema: { type: boolean } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { items: { type: array, items: { type: object } } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Accounting]
      summary: Založí zaměstnance
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [full_name]
              properties:
                full_name: { type: string }
                birth_date: { type: [string, "null"], format: date }
                birth_number: { type: [string, "null"] }
                address: { type: [string, "null"] }
                taxpayer_type: { type: string, default: employee }
                tax_credit_taxpayer: { type: boolean, default: true }
                child_count: { type: integer, minimum: 0, maximum: 20 }
                is_active: { type: boolean, default: true }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object, properties: { employee: { type: object } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/payroll/employees/{id}:
    put:
      tags: [Accounting]
      summary: Upraví zaměstnance
      description: Účetní/admin. Všechna pole volitelná (patch-like).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { employee: { type: object } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [Accounting]
      summary: Smaže zaměstnance
      description: Účetní/admin. Jen bez mzdových záznamů.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Zaměstnanec má mzdové záznamy, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/accounting/cost-centers:
    get:
      tags: [Accounting]
      summary: Seznam firemních středisek
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: include_inactive, schema: { type: boolean } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: array, items: { type: object } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Accounting]
      summary: Vytvoří středisko
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, name]
              properties:
                code: { type: string, minLength: 1, maxLength: 50 }
                name: { type: string, minLength: 1, maxLength: 255 }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Středisko s tímto kódem už existuje, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/cost-centers/{id}:
    patch:
      tags: [Accounting]
      summary: Upraví středisko (název, aktivace)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                is_active: { type: boolean }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [Accounting]
      summary: Smaže nebo deaktivuje středisko
      description: Účetní/admin. Používané středisko se jen deaktivuje.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/expense-rules:
    get:
      tags: [Accounting]
      summary: Pravidla klasifikace druhu výdaje (§DM) — stránkovaně
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: expense_kind, schema: { type: string } }
        - { in: query, name: active, schema: { type: boolean } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 100 } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { items: { type: array, items: { type: object } }, total: { type: integer }, page: { type: integer }, per_page: { type: integer } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Accounting]
      summary: Vytvoří pravidlo klasifikace výdaje
      description: |
        Účetní/admin. READ-ONLY návrh — nic neúčtuje, jen předvyplňuje `expense_kind`
        na řádku PF. Vyžaduje alespoň jednu podmínku (`vendor_client_id`,
        `vendor_name_contains` nebo `description_contains`).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expense_kind]
              properties:
                expense_kind: { type: string }
                vendor_client_id: { type: [integer, "null"] }
                vendor_name_contains: { type: [string, "null"] }
                description_contains: { type: [string, "null"] }
                amount_min: { type: [number, "null"] }
                amount_max: { type: [number, "null"] }
                target_account_code: { type: [string, "null"] }
                recurring_prepaid: { type: boolean, default: false }
                priority: { type: integer, minimum: 0, maximum: 999, default: 100 }
                name: { type: [string, "null"] }
                is_active: { type: boolean, default: true }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object, properties: { rule: { type: object } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/expense-rules/{id}:
    put:
      tags: [Accounting]
      summary: Upraví pravidlo klasifikace (všechna pole volitelná)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        content: { application/json: { schema: { type: object } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { rule: { type: object } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [Accounting]
      summary: Smaže pravidlo klasifikace
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/purchase-invoices/{id}/expense-suggestions:
    get:
      tags: [Accounting]
      summary: Návrh druhu výdaje pro položky přijaté faktury (read-only)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {purchase_invoice_id, items (item_id→kind), recurring_prepaid}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/small-assets:
    get:
      tags: [Accounting]
      summary: Seznam karet drobného majetku (§DM)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: status, schema: { type: string, enum: [in_use, disposed, sold] } }
        - { in: query, name: q, schema: { type: string } }
        - { in: query, name: location, schema: { type: string } }
        - { in: query, name: year, schema: { type: integer } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 100 } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { items: { type: array, items: { type: object } }, total: { type: integer }, page: { type: integer }, per_page: { type: integer }, locations: { type: array, items: { type: string } }, years: { type: array, items: { type: integer } } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    post:
      tags: [Accounting]
      summary: Založí kartu drobného majetku
      description: |
        Účetní/admin. Karta NIC neúčtuje — náklad na 501 vzniká zaúčtováním
        dokladu podle `expense_kind`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, acquisition_date]
              properties:
                name: { type: string }
                acquisition_date: { type: string, format: date }
                quantity: { type: number, minimum: 1 }
                unit_price: { type: number }
                price: { type: number }
                purchase_invoice_id: { type: [integer, "null"] }
                inventory_number: { type: [string, "null"] }
                vendor_client_id: { type: [integer, "null"] }
                vendor_name: { type: [string, "null"] }
                put_into_use_date: { type: [string, "null"], format: date }
                location: { type: [string, "null"] }
                responsible_person: { type: [string, "null"] }
                notes: { type: [string, "null"] }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/small-assets/{id}:
    get:
      tags: [Accounting]
      summary: Detail karty drobného majetku
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Accounting]
      summary: Upraví kartu drobného majetku
      description: Účetní/admin. Všechna pole volitelná.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        content: { application/json: { schema: { type: object } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [Accounting]
      summary: Smaže kartu drobného majetku
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/small-assets/{id}/dispose:
    post:
      tags: [Accounting]
      summary: Vyřazení karty drobného majetku
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [disposed_at]
              properties:
                disposed_at: { type: string, format: date }
                disposal_reason: { type: [string, "null"] }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/small-assets/{id}/sell:
    post:
      tags: [Accounting]
      summary: Prodej karty drobného majetku
      description: Účetní/admin. Tržba se z karty neúčtuje, jen eviduje odkaz na vydanou fakturu.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sale_invoice_id, sold_at]
              properties:
                sale_invoice_id: { type: integer }
                sold_at: { type: string, format: date }
                sale_price: { type: [number, "null"] }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/small-assets/{id}/restore:
    post:
      tags: [Accounting]
      summary: Obnoví kartu z vyřazeného/prodaného stavu
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/purchase-invoices/{id}/small-assets:
    post:
      tags: [Accounting]
      summary: Vygeneruje karty drobného majetku z položek přijaté faktury
      description: Účetní/admin. Idempotentní — už vygenerované položky přeskočí (`skipped`).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '201': { description: "OK {purchase_invoice_id, created, skipped, cards}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/general-ledger:
    get:
      tags: [Accounting]
      summary: Hlavní kniha (PS, měsíční obraty, KS per účet)
      description: Jen zaúčtované zápisy; `draft_count` informuje o konceptech v rozsahu.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: from, description: "Zúžení rozsahu uvnitř období", schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: analytics, description: "1 = rozpad po analytikách", schema: { type: boolean } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/general-ledger/export:
    get:
      tags: [Accounting]
      summary: Export hlavní knihy (PDF / XLSX)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: analytics, schema: { type: boolean } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/trial-balance:
    get:
      tags: [Accounting]
      summary: Obratová předvaha (PS/obraty/KS + kontrolní vazby)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: analytics, schema: { type: boolean } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/trial-balance/export:
    get:
      tags: [Accounting]
      summary: Export obratové předvahy (PDF / XLSX)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: analytics, schema: { type: boolean } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/account-statement/{accountId}:
    get:
      tags: [Accounting]
      summary: Opis účtu (pohyby s běžícím zůstatkem, stránkované)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: accountId, required: true, schema: { type: integer } }
        - { in: query, name: from, required: true, schema: { type: string, format: date } }
        - { in: query, name: to, required: true, schema: { type: string, format: date } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 200 } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/account-statement/{accountId}/export:
    get:
      tags: [Accounting]
      summary: Export opisu účtu (PDF / XLSX)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: accountId, required: true, schema: { type: integer } }
        - { in: query, name: from, required: true, schema: { type: string, format: date } }
        - { in: query, name: to, required: true, schema: { type: string, format: date } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 200 } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/balance-sheet:
    get:
      tags: [Accounting]
      summary: Rozvaha (vyhl. 500/2002 Sb., brutto/korekce/netto + minulé období)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: as_of, description: "Rozvahový den (default min(konec období, dnešek))", schema: { type: string, format: date } }
        - { in: query, name: scope, schema: { type: string, enum: [auto, full, small, micro], default: auto } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/balance-sheet/export:
    get:
      tags: [Accounting]
      summary: Export rozvahy (PDF / XLSX)
      description: |
        PDF je oficiální výkaz — vždy v celých tisících Kč (§4 odst. 3 vyhl.
        500/2002 Sb.), zaokrouhlení po řádcích. XLSX je pracovní formát —
        jednotku volí `unit` (default Kč).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: as_of, schema: { type: string, format: date } }
        - { in: query, name: scope, schema: { type: string, enum: [auto, full, small, micro], default: auto } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
        - { in: query, name: unit, description: "Jen XLSX; PDF je vždy v tis. Kč", schema: { type: string, enum: [czk, thousands], default: czk } }
      responses:
        '200': { description: Soubor výkazu, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/income-statement:
    get:
      tags: [Accounting]
      summary: Výkaz zisku a ztráty (druhové členění)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: as_of, schema: { type: string, format: date } }
        - { in: query, name: scope, schema: { type: string, enum: [auto, full, small, micro], default: auto } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/income-statement/export:
    get:
      tags: [Accounting]
      summary: Export výsledovky (PDF / XLSX)
      description: |
        PDF je oficiální výkaz — vždy v celých tisících Kč (§4 odst. 3 vyhl.
        500/2002 Sb.), zaokrouhlení po řádcích. XLSX je pracovní formát —
        jednotku volí `unit` (default Kč).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: as_of, schema: { type: string, format: date } }
        - { in: query, name: scope, schema: { type: string, enum: [auto, full, small, micro], default: auto } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
        - { in: query, name: unit, description: "Jen XLSX; PDF je vždy v tis. Kč", schema: { type: string, enum: [czk, thousands], default: czk } }
      responses:
        '200': { description: Soubor výkazu, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/saldo:
    get:
      tags: [Accounting]
      summary: Saldokonto — otevřené položky per partner + konfrontace se zůstatkem účtu
      description: |
        Otevřené (nezaplacené/částečně zaplacené) faktury seskupené per partner na
        saldokontních účtech (default 311/321/314/324), s automatickou konfrontací
        Σ otevřených položek proti zůstatku účtu z hlavní knihy k rozvahovému dni.
        Rozdíl (inventarizační hodnota) odhalí ruční zápisy, storna či zapomenuté
        faktury nepokryté fakturačním zdrojem.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: as_of, description: "Rozvahový den (default min(konec období, dnešek))", schema: { type: string, format: date } }
        - { in: query, name: account, description: "Kód účtu (např. 311) nebo 'all' pro default sadu", schema: { type: string, default: all } }
        - { in: query, name: partner_id, description: "Volitelný filtr na jednoho partnera", schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/saldo/export:
    get:
      tags: [Accounting]
      summary: Export saldokonta / inventarizačního protokolu (PDF / XLSX)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: as_of, schema: { type: string, format: date } }
        - { in: query, name: account, schema: { type: string, default: all } }
        - { in: query, name: partner_id, schema: { type: integer } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/document-completeness:
    get:
      tags: [Accounting]
      summary: Kontrola úplnosti dokladů proti bance (§24/1)
      description: |
        Read-only report — bankovní pohyby bez napárovaného dokladu starší
        prahu X dní + doklady po splatnosti (REAL_data_followup_UX.md E).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: days, schema: { type: integer, minimum: 0, maximum: 3650, default: 30 } }
        - { in: query, name: direction, schema: { type: string, enum: [outgoing, incoming, all], default: all } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/balance-inventory:
    get:
      tags: [Accounting]
      summary: Inventarizace rozvahových účtů (§29–30 ZoÚ)
      description: Soupis účtů tříd 0–4 s koncovým zůstatkem k rozvahovému dni období.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/balance-inventory/export:
    get:
      tags: [Accounting]
      summary: Export inventarizace rozvahových účtů (PDF / XLSX)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/small-assets/inventory:
    get:
      tags: [Accounting]
      summary: Soupis karet drobného majetku k datu (§DM)
      description: Podklad k inventarizaci (§28/5 ZoÚ); rozpis 501 sedí na hlavní knihu.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: as_of, schema: { type: string, format: date, description: "Default dnešek" } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/small-assets/inventory/export:
    get:
      tags: [Accounting]
      summary: Export soupisu drobného majetku (PDF / XLSX)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: as_of, schema: { type: string, format: date } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/small-assets/movements:
    get:
      tags: [Accounting]
      summary: Pohyby drobného majetku v období (zařazení/vyřazení/prodej)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: from, required: true, schema: { type: string, format: date } }
        - { in: query, name: to, required: true, schema: { type: string, format: date } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/small-assets/movements/export:
    get:
      tags: [Accounting]
      summary: Export pohybů drobného majetku (PDF / XLSX)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: from, required: true, schema: { type: string, format: date } }
        - { in: query, name: to, required: true, schema: { type: string, format: date } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/small-assets/expense-breakdown:
    get:
      tags: [Accounting]
      summary: Rozpad nákladu 501 drobného majetku podle druhu za období
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: from, required: true, schema: { type: string, format: date } }
        - { in: query, name: to, required: true, schema: { type: string, format: date } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/small-assets/expense-breakdown/export:
    get:
      tags: [Accounting]
      summary: Export rozpadu nákladu 501 drobného majetku (PDF / XLSX)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: from, required: true, schema: { type: string, format: date } }
        - { in: query, name: to, required: true, schema: { type: string, format: date } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/monthly-report/preview:
    get:
      tags: [Accounting]
      summary: Měsíční přehled klientovi — náhled dat (JSON)
      description: |
        PDF balíček pro klienta sestavený NAD existujícími sestavami (výsledovka
        měsíc+YTD, rozvaha, top pohledávky/závazky po splatnosti ze saldokonta,
        DPH k úhradě a termín podání pro plátce DPH, nadcházející daňové termíny).
        Tento endpoint jen vrací data pro náhled; PDF viz /download.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer } }
        - { in: query, name: month, required: true, schema: { type: integer, minimum: 1, maximum: 12 } }
        - { in: query, name: comment, description: "Volitelný komentář účetní (zobrazí se i v PDF/e-mailu)", schema: { type: string } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/monthly-report/download:
    get:
      tags: [Accounting]
      summary: Měsíční přehled klientovi — PDF ke stažení
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer } }
        - { in: query, name: month, required: true, schema: { type: integer, minimum: 1, maximum: 12 } }
        - { in: query, name: comment, schema: { type: string } }
      responses:
        '200': { description: Soubor PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/monthly-report/send:
    post:
      tags: [Accounting]
      summary: Měsíční přehled klientovi — odeslání e-mailem + archivace do DMS
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [year, month, to]
              properties:
                year: { type: integer, minimum: 2000, maximum: 2100 }
                month: { type: integer, minimum: 1, maximum: 12 }
                to: { type: array, minItems: 1, items: { type: string, format: email } }
                cc: { type: array, items: { type: string, format: email } }
                comment: { type: string }
                subject_override: { type: string }
      responses:
        '200': { description: "OK {sent_to, cc, document_id, sent_at}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Chybí příjemci / neplatný e-mail, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '502': { description: Odeslání e-mailu selhalo, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/accounting/reports/monthly-report/history:
    get:
      tags: [Accounting]
      summary: Měsíční přehled klientovi — historie odeslání
      description: "Log odeslání (komu, kdy, kdo, archivovaná kopie v Dokumentech)."
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: limit, schema: { type: integer, default: 30, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { type: object } }

  /api/v1/accounting/reports/journal/export:
    get:
      tags: [Accounting]
      summary: Export účetního deníku (PDF / XLSX)
      description: |
        Deník je zákonná kniha (§13 ZoÚ). Export respektuje PŘESNĚ stejné filtry jako
        GET /accounting/journal (bez stránkování — vrací všechny odpovídající zápisy
        chronologicky). Řádky nesou kód/název účtu a částku MD/Dal, PDF má číslované
        strany. Strop 5000 zápisů na jeden export — nad limit vrací 422 `too_many_rows`
        (audit 2026-07).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, schema: { type: integer } }
        - { in: query, name: date_from, schema: { type: string, format: date } }
        - { in: query, name: date_to, schema: { type: string, format: date } }
        - { in: query, name: source_type, schema: { type: string, enum: [invoice, purchase_invoice, bank, cash, asset, manual, closing, opening, stock, provision, income_tax, profit_distribution] } }
        - { in: query, name: source_id, schema: { type: integer } }
        - { in: query, name: posted, schema: { type: boolean } }
        - { in: query, name: format, required: true, schema: { type: string, enum: [pdf, xlsx] } }
      responses:
        '200': { description: Soubor sestavy, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal/{id}/history:
    get:
      tags: [Accounting]
      summary: Auditní historie zápisu (SYSTEM VERSIONING timeline)
      description: |
        Všechny historické i aktuální verze hlavičky a řádků zápisu (MariaDB
        `FOR SYSTEM_TIME ALL`, viz migrace 1029) s diffem oproti předchozí verzi
        (změněná hlavičková pole, přidané/odebrané/změněné řádky dle `line_no`) a
        best-effort spárováním s activity_log (audit 2026-07). Verze jsou seřazené
        od nejnovější. Vyžaduje MariaDB — na jiné DB degraduje na jedinou verzi.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/reports/payroll-sheet:
    get:
      tags: [Accounting]
      summary: Mzdový list zaměstnance (§38j ZDP) — PDF
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: year, required: true, schema: { type: integer, minimum: 2018, maximum: 2100 } }
        - { in: query, name: employee_id, required: true, schema: { type: integer } }
        - { in: query, name: format, schema: { type: string, default: pdf } }
      responses:
        '200': { description: Soubor PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/entity-category:
    get:
      tags: [Accounting]
      summary: Kategorizace účetní jednotky (§1b–1d ZoÚ) + rozsah výkazů
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: period_id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK (kategorie, kritéria, prahy, scope)", content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reporting-settings:
    get:
      tags: [Accounting]
      summary: Nastavení výkaznictví firmy (zaměstnanci, override rozsahu)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { avg_employees: { type: [integer, "null"] }, statement_scope_override: { type: [string, "null"], enum: [full, small, micro, null] }, statutory_audit: { type: boolean }, manual_doc_series: { type: boolean }, fx_reversal_at_open: { type: boolean } } } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    put:
      tags: [Accounting]
      summary: Uloží nastavení výkaznictví
      description: |
        Účetní/admin. Přepínače (statutory_audit §3a vyhl. → výkazy vždy v plném
        rozsahu, manual_doc_series — řada ID pro ruční zápisy, fx_reversal_at_open
        — storno FX přecenění saldokonta k 1. dni) se ukládají jen jsou-li v body.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                avg_employees: { type: [integer, "null"], minimum: 0 }
                statement_scope_override: { type: [string, "null"], enum: [full, small, micro, null] }
                statutory_audit: { type: boolean, description: "Povinný audit §20 ZoÚ → plný rozsah výkazů (§3a vyhl. 500/2002)" }
                manual_doc_series: { type: boolean, description: "Auto číslo dokladu ID-{rok}-{NNNN} pro ruční zápisy" }
                fx_reversal_at_open: { type: boolean, description: "Storno přecenění saldokonta k 1. dni nového období (ČÚS 006)" }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/period-lock:
    get:
      tags: [Accounting]
      summary: Měkký zámek účtování k datu (B8)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { locked_until: { type: [string, "null"], format: date } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    put:
      tags: [Accounting]
      summary: Nastaví/posune měkký zámek účtování
      description: Admin-only. Důvod se ukládá do auditu.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                locked_until: { type: [string, "null"], format: date }
                reason: { type: string, minLength: 5 }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { locked_until: { type: [string, "null"], format: date } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/fx-rate-settings:
    get:
      tags: [Accounting]
      summary: Kurzový režim firmy (§24/7 ZoÚ)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: fiscal_year, schema: { type: integer } }
      responses:
        '200': { description: "OK {mode, rates}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    put:
      tags: [Accounting]
      summary: Přepne kurzový režim (denní / pevný měsíční / pevný roční)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mode]
              properties:
                mode: { type: string, enum: [daily, fixed_monthly, fixed_annual] }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { mode: { type: string } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/fx-rate-settings/cnb-prefill:
    get:
      tags: [Accounting]
      summary: Návrh pevného kurzu z ČNB (1. den období)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: currency, required: true, schema: { type: string, description: "ISO 4217, ne CZK" } }
        - { in: query, name: fiscal_year, required: true, schema: { type: integer, minimum: 2000, maximum: 2100 } }
        - { in: query, name: month, schema: { type: integer, minimum: 0, maximum: 12 } }
      responses:
        '200': { description: "OK {currency, rate, rate_date, fallback_used}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { description: Kurz od ČNB není k dispozici, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/fx-rate-settings/rates:
    put:
      tags: [Accounting]
      summary: Upsert pevného kurzu pro měsíc/rok
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currency, fiscal_year, month, rate]
              properties:
                currency: { type: string, description: "ISO 4217, ne CZK" }
                fiscal_year: { type: integer }
                month: { type: integer, minimum: 0, maximum: 12 }
                rate: { type: number, exclusiveMinimum: 0, maximum: 9999999.999999 }
      responses:
        '200': { description: "OK {rates}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/fx-rate-settings/rates/{id}:
    delete:
      tags: [Accounting]
      summary: Smaže pevný kurz
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/repo-rates:
    get:
      tags: [Accounting]
      summary: Repo sazba ČNB (číselník pro úrok z prodlení, NV 351/2013)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: "OK {rates: [{valid_from, rate, note}]}", content: { application/json: { schema: { type: object } } } }
    put:
      tags: [Accounting]
      summary: Upsert repo sazby k datu
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [valid_from, rate]
              properties:
                valid_from: { type: string, format: date }
                rate: { type: number, minimum: 0, maximum: 100 }
                note: { type: [string, "null"] }
      responses:
        '200': { description: "OK {rates}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/repo-rates/{date}:
    delete:
      tags: [Accounting]
      summary: Smaže repo sazbu k datu
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: date, required: true, schema: { type: string, format: date }, description: "Formát YYYY-MM-DD" }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/offsets:
    get:
      tags: [Accounting]
      summary: Historie dohod o vzájemném zápočtu
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: status, schema: { type: string } }
      responses:
        '200': { description: "OK {items}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Accounting]
      summary: Vytvoří dohodu o zápočtu (koncept)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [partner_id, agreement_date, items]
              properties:
                partner_id: { type: integer }
                agreement_date: { type: string, format: date }
                note: { type: [string, "null"] }
                items:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    properties:
                      doc_type: { type: string }
                      doc_id: { type: integer }
                      amount: { type: number }
      responses:
        '201': { description: "OK {agreement}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Neplatná kombinace dokladů, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/offsets/partners:
    get:
      tags: [Accounting]
      summary: Partneři s otevřenou pohledávkou i závazkem (kandidáti na zápočet)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: "OK {items}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/accounting/offsets/open:
    get:
      tags: [Accounting]
      summary: Otevřené faktury a přijaté faktury partnera pro sestavení zápočtu
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: partner_id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Partner nemá otevřené položky, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/offsets/{id}:
    get:
      tags: [Accounting]
      summary: Detail dohody o zápočtu
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Nekonzistentní stav dohody, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/accounting/offsets/{id}/confirm:
    post:
      tags: [Accounting]
      summary: Potvrdí dohodu — zaúčtuje a vyrovná saldokonto
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {agreement}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Dohoda už není v konceptu, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/offsets/{id}/cancel:
    post:
      tags: [Accounting]
      summary: Zruší dohodu (storno zápočtu + odvolání saldokonta)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {agreement}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Dohodu nelze zrušit v aktuálním stavu, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/offsets/{id}/pdf:
    get:
      tags: [Accounting]
      summary: PDF dohody o zápočtu ke stažení
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: Soubor PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/bank-accounts:
    get:
      tags: [Accounting]
      summary: Registr vlastních bankovních účtů firmy
      description: Vrací účty používané pro rozpoznání převodů mezi vlastními účty.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [accounts]
                properties:
                  accounts:
                    type: array
                    items: { $ref: '#/components/schemas/SupplierBankAccount' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/accounting/bank-accounts/{id}:
    patch:
      tags: [Accounting]
      summary: Upraví vlastní bankovní účet
      description: Účetní/admin. Mění klasifikaci a aktivaci účtu; cizí tenant vrací 404.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                kind: { type: string, enum: [current, savings, term_deposit] }
                label: { type: [string, "null"], maxLength: 120 }
                analytic_suffix: { type: [string, "null"], pattern: '^[0-9]{1,6}$' }
                is_active: { type: boolean }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/SupplierBankAccount' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets:
    get:
      tags: [Accounting]
      summary: Seznam karet dlouhodobého majetku (Epic F3)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: status, schema: { type: string, enum: [draft, in_use, disposed] } }
        - { in: query, name: q, description: "Fulltext (inventární číslo, název)", schema: { type: string } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 200 } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    post:
      tags: [Accounting]
      summary: Založí kartu majetku
      description: |
        Účetní/admin. Koncept; historický majetek (R23) lze založit rovnou `in_use`
        s opening poli. Validační matice druh × daňová metoda dle §26–33 ZDP;
        opening oprávky nesmějí překročit vstupní cenu a osobní automobil M1
        nesmí mít zvýšený odpis prvního roku. Neblokující upozornění vrací pole
        `warnings`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, description: "Sloupce karty majetku (inventory_number, name, kind, input_price, tax_method, tax_group, účty, účetní parametry…)" } } }
      responses:
        '201': { description: Vytvořená karta, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets/purchase-candidates:
    get:
      tags: [Accounting]
      summary: Přijaté faktury s příznakem majetku (kandidáti na kartu)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: "OK (id, varsymbol, vendor, částky, kurz, has_asset)", content: { application/json: { schema: { type: array, items: { type: object } } } } }

  /api/v1/accounting/assets/depreciations/book:
    post:
      tags: [Accounting]
      summary: Hromadné potvrzení a zaúčtování odpisů roku (R12)
      description: |
        Účetní/admin. Pro každý majetek v užívání (a vyřazený v daném roce) potvrdí
        daňový odpis a zaúčtuje účetní odpis (551 MD / oprávky D). Idempotentní —
        opakované spuštění přepíše in-place, dokud je období otevřené. Chyby per
        majetek vrací v poli `errors` (HTTP 200). Zaúčtovat lze pouze období, které
        už skončilo; probíhající nebo budoucí období vrací 409 `period_not_ended`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fiscal_year]
              properties:
                fiscal_year: { type: integer, minimum: 2000, maximum: 2100 }
      responses:
        '200': { description: "Souhrn {booked, skipped, total_accounting, total_tax, errors}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Období ještě neskončilo, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets/{id}:
    get:
      tags: [Accounting]
      summary: Detail karty majetku (vč. TZ a zámků editace R13)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Accounting]
      summary: Upraví kartu majetku
      description: Účetní/admin. Daňové parametry a VC zamčené po prvním potvrzeném odpisu (422 `asset_locked`).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [Accounting]
      summary: Smaže koncept nebo chybně zařazenou kartu majetku
      description: >-
        Účetní/admin. U karty v užívání atomicky odstraní i zápis zařazení.
        Operace je povolena jen bez potvrzených odpisů a technických zhodnocení;
        zápis zařazení musí být v otevřeném a neuzamčeném období.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Karta má účetní historii nebo je období uzavřené/uzamčené, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/accounting/assets/{id}/depreciation-plan:
    get:
      tags: [Accounting]
      summary: Plán odpisů (daňové i účetní, on-the-fly R11)
      description: >-
        Minulost z potvrzených řádků, budoucnost dopočtená strategií dle ZDP/ČÚS 013.
        Potvrzené řádky obsahují `depreciation_entry_id`; roky s aktivním účetním
        zápisem také `journal_entry_id`, přes které lze poslední odpis smazat.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {asset_summary, tax, accounting}", content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/assets/{id}/depreciation-card:
    get:
      tags: [Accounting]
      summary: Inventární karta majetku ke stažení (§29–30 ZoÚ) — PDF
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: Soubor PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/assets/{id}/put-into-use:
    post:
      tags: [Accounting]
      summary: Zařazení majetku do užívání (MD 02x / D 042)
      description: Účetní/admin. Jen koncept; `book_entry=false` pro historický majetek (R23).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [date]
              properties:
                date: { type: string, format: date }
                book_entry: { type: boolean, default: true }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets/{id}/improvements:
    post:
      tags: [Accounting]
      summary: Přidá technické zhodnocení (§33 ZDP)
      description: Účetní/admin. Jen majetek v užívání; u §30a vrací 422 (samostatná karta).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [completed_on, amount]
              properties:
                completed_on: { type: string, format: date }
                amount: { type: number, exclusiveMinimum: 0 }
                description: { type: [string, "null"] }
                purchase_invoice_id: { type: [integer, "null"] }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets/{id}/improvements/{impId}:
    delete:
      tags: [Accounting]
      summary: Smaže technické zhodnocení (jen rok bez potvrzeného odpisu)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: path, name: impId, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets/{id}/dispose:
    post:
      tags: [Accounting]
      summary: Vyřazení majetku (prodej / likvidace / dar / škoda)
      description: |
        Účetní/admin. Atomicky doúčtuje účetní odpis roku, potvrdí daňový půlodpis
        (§26/7) a zaúčtuje vyřazení (R19). Tržba z prodeje se z karty neúčtuje —
        výnos 641 vznikne vydanou fakturou (R20). Operace blokuje přeskočený
        předchozí rok i potvrzené odpisy pozdějších let. V `warnings` upozorní
        na DPH při darování a na doložení likvidace, manka či škody.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [date, type]
              properties:
                date: { type: string, format: date }
                type: { type: string, enum: [sold, liquidated, donated, damaged] }
                price: { type: [number, "null"], description: "Prodejní cena bez DPH, jen type=sold (evidenční)" }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets/{id}/dispose/revert:
    post:
      tags: [Accounting]
      summary: Vrácení vyřazení majetku (R24)
      description: Účetní/admin. Jen dokud je období data vyřazení otevřené; storna zůstávají v deníku a dříve založená pauza daňového odpisu zůstává zachována.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets/{id}/depreciation/pause:
    post:
      tags: [Accounting]
      summary: Přerušení daňového odpisování pro daný rok (§26/8 ZDP)
      description: Účetní/admin. Jen rovnoměrné/zrychlené odpisy; §30a přerušit nelze.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fiscal_year]
              properties:
                fiscal_year: { type: integer, minimum: 2000, maximum: 2100 }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/assets/{id}/depreciation/pause/{year}:
    delete:
      tags: [Accounting]
      summary: Zrušení přerušení odpisu roku
      description: Účetní/admin. Jen pokud neexistuje potvrzený řádek pozdějšího roku.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: path, name: year, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing:
    get:
      tags: [Accounting]
      summary: Stav uzávěrkového průvodce období (Epic F4)
      description: Období + kroky (precheck…open_next, stav pending/done/skipped, payload) + odvozené flagy can_*.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/periods/{id}/monthly-check:
    get:
      tags: [Accounting]
      summary: Měsíční kontrola (audit 2026-07 D8)
      description: |
        Reuse ClosingService::buildChecks nad libovolným rozsahem uvnitř období
        (měsíc/kvartál/vlastní od-do), kdykoli, BEZ zahájení uzávěrky. Read-only,
        nemění stav ani kroky. `date_from`/`date_to` volitelné (default = celé
        fiskální období); musí ležet uvnitř `starts_on`–`ends_on` období.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: query, name: date_from, schema: { type: string, format: date } }
        - { in: query, name: date_to, schema: { type: string, format: date } }
      responses:
        '200': { description: "OK {period, range_from, range_to, checks[], ran_at}", content: { application/json: { schema: { $ref: '#/components/schemas/MonthlyCheckResponse' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/start:
    post:
      tags: [Accounting]
      summary: Zahájení uzávěrky (open→closing)
      description: Účetní/admin. Předchozí období musí být closed/approved (422 `previous_period_open`).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [row_version], properties: { row_version: { type: integer, minimum: 1 } } } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/abort:
    post:
      tags: [Accounting]
      summary: Přerušení uzávěrky (closing→open)
      description: Účetní/admin. Blokováno 422 `closing_entries_exist`, existují-li zaúčtované závěrkové zápisy.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [row_version], properties: { row_version: { type: integer, minimum: 1 } } } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/fx-preview:
    get:
      tags: [Accounting]
      summary: Náhled kurzových rozdílů k rozvahovému dni (bez zápisu)
      description: |
        Saldokonto (open-item z dokladů, kurz ČNB k ends_on) + banka/pokladna dle
        volitelného `bank_rows` (JSON pole {account_code, currency_code, foreign_balance}).
        Návrhy devizových zůstatků vrací v `proposals`.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: query, name: bank_rows, schema: { type: string, description: "JSON pole bank řádků" } }
      responses:
        '200': { description: "OK {rate_info, saldo, bank, totals, proposals}", content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/provisions-preview:
    get:
      tags: [Accounting]
      summary: Náhled opravných položek k pohledávkám (D9, §8a/§8c ZoR)
      description: |
        Aging účtu 311 k rozvahovému dni (reuse saldokonta) s návrhem zákonné OP dle
        §8a (18 měs. 50 %, 30 měs. 100 %) a §8c (do 30 000 Kč nad 12 měs. 100 %).
        Od 36 měsíců se automatický návrh vynuluje a položka nese upozornění na možné promlčení.
        Systém návrh jen nabízí — účetní ho per pohledávka potvrdí/upraví v /steps/provisions/run.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {as_of, items, totals, rules}", content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/estimates-suggest:
    get:
      tags: [Accounting]
      summary: Návrh dohadných položek pasivních (K10, 389)
      description: |
        Heuristický, read-only návrh dohadů pasivních k rozvahovému dni: opakující se
        měsíční náklad (dodavatel s fakturou ve ≥3 měsících roku a kadencí do měsíce před
        posledním), jehož faktura za POSLEDNÍ měsíc období nedorazila. Návrh částky = průměr
        netto posledních 3 faktur, protiúčet = nejčastější 5xx MD účet dodavatele.
        Systém návrh jen nabízí — účetní ho per dodavatel potvrdí/upraví a zaúčtuje přes
        /closing/entries (estimate.liability → MD 5xx / D 389).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {as_of, items, totals, rules}", content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/income-tax-preview:
    get:
      tags: [Accounting]
      summary: Náhled podkladu splatné daně z příjmů (D11, 591/341)
      description: |
        Přednabídne částku z finalizovaného DPPO přiznání téhož roku a zůstatky 341/591.
        Zaúčtování přes /steps/income_tax/run (idempotence source income_tax/period_id).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {suggested_amount, has_finalized_return, taxpayer_type, applicable, balance_341, balance_591}; applicable=false pro FO", content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/steps/{step}/run:
    post:
      tags: [Accounting]
      summary: Provedení kroku uzávěrky 1–5
      description: |
        Účetní/admin. precheck = přepočet kontrol; fx_revaluation = zaúčtování
        kurzových rozdílů (body `bank_rows`); depreciation/estimates/deferrals =
        checklist potvrzení (body `status` done|skipped + `note`). Kroky
        close_books/open_next mají vlastní endpointy /close a /open-next.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: path, name: step, required: true, schema: { type: string, enum: [precheck, depreciation, fx_revaluation, estimates, deferrals, provisions, income_tax] } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [row_version]
              properties:
                row_version: { type: integer, minimum: 1 }
                status: { type: string, enum: [done, skipped], description: "Checklist kroky + skip u provisions/income_tax" }
                note: { type: string, maxLength: 500 }
                amount: { type: number, description: "Jen income_tax — splatná daň (591/341)" }
                items:
                  type: array
                  description: "Jen provisions — účetní-potvrzený seznam OP per pohledávka (§8a/§8c)"
                  items:
                    type: object
                    required: [invoice_id]
                    properties:
                      invoice_id: { type: integer }
                      legal_amount: { type: number, description: "Zákonná OP (558)" }
                      acct_amount: { type: number, description: "Účetní OP nad rámec (559)" }
                      note: { type: string }
                bank_rows:
                  type: array
                  description: "Jen fx_revaluation — devizové zůstatky bank/pokladen"
                  items:
                    type: object
                    required: [account_code, currency_code, foreign_balance]
                    properties:
                      account_code: { type: string }
                      currency_code: { type: string, minLength: 3, maxLength: 3 }
                      foreign_balance: { type: number }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/steps/{step}/revert:
    post:
      tags: [Accounting]
      summary: Revert kroku uzávěrky (R12 — hard delete závěrkových zápisů)
      description: |
        Admin-only. Pořadí vynuceno: open_next → close_books → fx_revaluation;
        mazané řádky jdou dumpem do activity_log. Asistované zápisy
        estimates/deferrals se nemažou — stornují se jednotlivě.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: path, name: step, required: true, schema: { type: string, enum: [precheck, depreciation, fx_revaluation, estimates, deferrals, provisions, income_tax, close_books, open_next] } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [row_version], properties: { row_version: { type: integer, minimum: 1 } } } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/entries:
    post:
      tags: [Accounting]
      summary: Asistovaný závěrkový zápis (dohady 388/389, časové rozlišení 381–385)
      description: |
        Účetní/admin, jen ve stavu closing. Kontace z posting_rules
        (estimates: estimate.asset|estimate.liability; deferrals:
        accrual.prepaid.expense|accrual.accrued.expense|accrual.deferred.revenue|
        accrual.accrued.revenue), entry_date = ends_on, číslo z řady ID.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [row_version, step, rule_key, amount, description]
              properties:
                row_version: { type: integer, minimum: 1 }
                step: { type: string, enum: [estimates, deferrals] }
                rule_key: { type: string }
                amount: { type: number, exclusiveMinimum: 0 }
                description: { type: string }
                counter_account: { type: string, description: "Povinný, nemá-li kontace stranu" }
      responses:
        '201': { description: Vytvořený zápis, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/closing/entries/{entryId}/reverse:
    post:
      tags: [Accounting]
      summary: Storno asistovaného závěrkového zápisu
      description: Účetní/admin. Standardní zrcadlový protizápis (zápisy z asistenta se nikdy nemažou).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: path, name: entryId, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [row_version], properties: { row_version: { type: integer, minimum: 1 } } } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/close:
    post:
      tags: [Accounting]
      summary: Uzavření účetních knih (krok 6, ČÚS 002)
      description: |
        Admin-only, stav closing, kroky 1–5 done/skipped, precheck bez errors.
        Zaúčtuje closing zápis (5xx/6xx→710, VH 710↔702, rozvahové↔702) k ends_on
        s číslem z řady UZ a přepne období na closed.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [row_version], properties: { row_version: { type: integer, minimum: 1 } } } } }
      responses:
        '200': { description: "OK {entry_id, profit, document_no…}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/open-next:
    post:
      tags: [Accounting]
      summary: Otevření nového roku přes 701 (krok 7)
      description: |
        Admin-only, stav closed. Následující období se dohledá/založí (musí přesně
        navazovat — 422 `period_gap`); opening zápis = zrcadlo rozvahové části
        closingu + VH na 431, číslo z řady OT; volitelné FX storno saldokonta.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [row_version], properties: { row_version: { type: integer, minimum: 1 } } } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/profit-distribution/preview:
    get:
      tags: [Accounting]
      summary: Náhled rozdělení výsledku hospodaření (D10)
      description: |
        Disponibilní zůstatek 431 v cílovém otevřeném období. Zdrojové období
        musí být schválené; odpověď obsahuje i limit rozdělitelných zdrojů z 431/428/429.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {approved_period, target_period, balance_431, available_profit, retained_profit, uncovered_loss, distributable_resources, is_loss, withholding_rate}", content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/periods/{id}/profit-distribution:
    post:
      tags: [Accounting]
      summary: Zaúčtování rozdělení VH (D10, 431 → 428/429/364…)
      description: |
        Účetní/admin. Zisk: MD 431 / D {428, fondy, 364}; ztráta: MD {429, 428} / D 431.
        Každý řádek kind=shares představuje jednoho společníka; §36 srážková daň se
        počítá a zaokrouhluje dolů per společník (MD účet podílů / D 342). Kontrola
        Σ přídělů = |zůstatek 431| a limitu rozdělitelných zdrojů. Účtuje se do OTEVŘENÉHO období; idempotence source
        profit_distribution/approved_period_id.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision_date, target_row_version, allocations]
              properties:
                decision_date: { type: string, format: date, description: "Datum rozhodnutí valné hromady" }
                target_row_version: { type: integer, minimum: 1, description: "row_version cílového otevřeného období (CAS)" }
                withholding_rate: { type: number, minimum: 0, maximum: 1, default: 0.15 }
                allocations:
                  type: array
                  items:
                    type: object
                    required: [account_code, amount]
                    properties:
                      account_code: { type: string, example: "428" }
                      amount: { type: number, exclusiveMinimum: 0 }
                      kind: { type: string, enum: [retained, fund, shares, loss_coverage] }
      responses:
        '200': { description: "OK {entry_id, distributed, withholding, target_period_id}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { description: "distribution_mismatch / validation_failed", content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/accounting/periods/{id}/profit-distribution/revert:
    post:
      tags: [Accounting]
      summary: Revert rozdělení VH (D10 — hard delete s dumpem)
      description: Admin-only. Smaže zápis rozdělení, dokud je cílové otevřené období open.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [target_row_version], properties: { target_row_version: { type: integer, minimum: 1 } } } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: version_conflict, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/journal/transfer:
    post:
      tags: [Accounting]
      summary: Dvě nohy převodu mezi peněžními účty přes 261 (R14)
      description: |
        Účetní/admin. Vytvoří DVA manual zápisy: k date_out MD 261 / D account_from,
        k date_in MD account_to / D 261; sdílené číslo dokladu z řady PP (sufix /1, /2).
        Storno standardně po jednotlivých nohách.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [date_out, date_in, amount, account_from, account_to]
              properties:
                date_out: { type: string, format: date }
                date_in: { type: string, format: date }
                amount: { type: number, exclusiveMinimum: 0 }
                account_from: { type: string, example: "221" }
                account_to: { type: string, example: "211" }
                description: { type: string }
                force: { type: boolean, default: false, description: "Vědomě povolí ruční zápis i při nalezené bankovní transakci převodu." }
      responses:
        '201': { description: "OK {document_no, entries[2]}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: Nalezeny bankovní transakce, jejichž ruční zápis by mohl převod zdvojit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BankTransferCandidatesError'
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/reports/tax-base-adjustments:
    get:
      tags: [Accounting]
      summary: Evidenční podklad DPPO (R19 — bez účtování, bez XML)
      description: |
        Rozdíl daňových a účetních odpisů roku, vyřazený majetek s daňovou/účetní
        ZC a klasifikací uznatelnosti (§24/2/b, §25/1/t, §24/2/l ZDP) a informativní
        zůstatky 388/389 + kurzové rozdíly 563/663 z přecenění.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: fiscal_year, required: true, schema: { type: integer } }
      responses:
        '200': { description: "OK {depreciation, disposals, info}", content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/document-series:
    get:
      tags: [Accounting]
      summary: Číselné řady deníku (UZ/OT/KR/PP/ID per rok)
      description: Řádek řady vzniká lazy při prvním výdeji čísla.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: array, items: { type: object } } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/accounting/document-series/{code}/{year}:
    put:
      tags: [Accounting]
      summary: Změna prefixu číselné řady
      description: Účetní/admin. Prefix 1–10 znaků A–Z/0–9; projeví se u dalšího vydaného čísla.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: code, required: true, schema: { type: string, enum: [closing, opening, fx, transfer, manual] } }
        - { in: path, name: year, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [prefix], properties: { prefix: { type: string, pattern: "^[A-Z0-9]{1,10}$" } } } } }
      responses:
        '200': { description: OK (aktuální seznam řad), content: { application/json: { schema: { type: array, items: { type: object } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/archive:
    get:
      tags: [Accounting]
      summary: Seznam per-firma archivů účetnictví (R15)
      description: Admin-only.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: array, items: { type: object } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/accounting/archive/export:
    post:
      tags: [Accounting]
      summary: Vytvoření archivu firmy (ZIP s manifestem)
      description: |
        Admin-only, synchronní. JSONL dump tenant dat + manifest.json (schema_version,
        sha256 per tabulka). PDF binárky nejsou součástí (kryjí je systémové zálohy).
        Obnova jen podpůrným CLI nástrojem (api/bin/archive-restore.php --dry-run).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: false
        content: { application/json: { schema: { type: object } } }
      responses:
        '201': { description: Metadata archivu, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/accounting/archive/{id}/download:
    get:
      tags: [Accounting]
      summary: Stažení ZIP archivu
      description: Admin-only. Streamuje soubor dohledaný přes id (tenant-scoped).
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: ZIP, content: { application/zip: { schema: { type: string, format: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/archive/{id}:
    delete:
      tags: [Accounting]
      summary: Smazání archivu (soubor + metadata)
      description: Admin-only, auditované.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # F5: per-user UI stav — ukládané filtry a preference tabulek (všechny role vč. readonly)
  /api/v1/user/filters:
    get:
      tags: [User]
      summary: Seznam uložených filtrů usera pro aktuální firmu
      description: page_key volitelný (R6); bez něj vrací všechny filtry usera pro aktuální firmu.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: page_key, required: false, schema: { type: string } }
      responses:
        '200': { description: Pole filtrů, content: { application/json: { schema: { type: array, items: { type: object } } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    post:
      tags: [User]
      summary: Vytvoření uloženého filtru
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [page_key, name, payload]
              properties:
                page_key: { type: string }
                name: { type: string, maxLength: 100 }
                payload: { type: object, additionalProperties: { type: string } }
                is_default: { type: boolean }
                sort_order: { type: integer }
      responses:
        '201': { description: Vytvořený filtr, content: { application/json: { schema: { type: object } } } }
        '409': { description: Filtr s tímto názvem už existuje (filter_name_exists) }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/user/filters/{id}:
    put:
      tags: [User]
      summary: Úprava uloženého filtru (page_key immutable)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 100 }
                payload: { type: object, additionalProperties: { type: string } }
                sort_order: { type: integer }
                is_default: { type: boolean }
      responses:
        '200': { description: Aktualizovaný filtr, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Filtr s tímto názvem už existuje (filter_name_exists) }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [User]
      summary: Smazání uloženého filtru
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/user/preferences:
    get:
      tags: [User]
      summary: Uživatelské preference UI (globální per user)
      description: 'Mapa pref_key → payload. Podporuje table.*, nav.order a keyboard.shortcuts; keys volitelný — výčtem omezí výsledek.'
      parameters:
        - { in: query, name: keys, required: false, schema: { type: string }, description: 'CSV výčet pref_key (např. table.invoices,nav.order,keyboard.shortcuts)' }
      responses:
        '200': { description: Mapa pref_key → payload, content: { application/json: { schema: { type: object } } } }

  /api/v1/user/preferences/{key}:
    put:
      tags: [User]
      summary: Upsert uživatelské preference
      description: 'Klíč je table.<page_key>, nav.order nebo keyboard.shortcuts. Body je přímo payload dané preference.'
      parameters:
        - { in: path, name: key, required: true, schema: { type: string, pattern: '^[a-z0-9_.]+$' } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object } } }
      responses:
        '200': { description: Uložený payload, content: { application/json: { schema: { type: object } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [User]
      summary: Reset preference klíče na default
      parameters:
        - { in: path, name: key, required: true, schema: { type: string, pattern: '^[a-z0-9_.]+$' } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  # F5: Excel export/import číselníků (osnova, kontace, majetek). Export = čtení
  # (readonly smí); import = requireWrite (admin|accountant), multipart file + dry_run.
  /api/v1/accounting/accounts/export:
    get:
      tags: [Accounting]
      summary: Export účtové osnovy do XLSX
      description: 'Round-trip: re-import beze změn skončí 100 % skip. List „Účtová osnova" + „Návod".'
      responses:
        '200':
          description: XLSX soubor
          content: { application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: { schema: { type: string, format: binary } } }
  /api/v1/accounting/posting-rules/export:
    get:
      tags: [Accounting]
      summary: Export kontačních pravidel do XLSX
      description: Efektivní pohled firmy (globální seed překrytý overridy).
      responses:
        '200':
          description: XLSX soubor
          content: { application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: { schema: { type: string, format: binary } } }
  /api/v1/accounting/assets/export:
    get:
      tags: [Accounting]
      summary: Export karet majetku do XLSX
      responses:
        '200':
          description: XLSX soubor
          content: { application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: { schema: { type: string, format: binary } } }
  /api/v1/accounting/accounts/import:
    post:
      tags: [Accounting]
      summary: Import účtové osnovy z XLSX/CSV
      description: 'Dry-run náhled (default dry_run=1). Ostrý běh (dry_run=0) all-or-nothing; ≥1 error → 422 import_has_errors.'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
                dry_run: { type: string, enum: ['0', '1'], default: '1' }
      responses:
        '200': { description: Report importu, content: { application/json: { schema: { $ref: '#/components/schemas/CodebookImportReport' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '415': { $ref: '#/components/responses/ValidationFailed' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/accounting/posting-rules/import:
    post:
      tags: [Accounting]
      summary: Import kontačních pravidel z XLSX/CSV
      description: 'Výhradně firemní overridy (priority=100, R11); neznámý klíč = error. Dry-run default.'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
                dry_run: { type: string, enum: ['0', '1'], default: '1' }
      responses:
        '200': { description: Report importu, content: { application/json: { schema: { $ref: '#/components/schemas/CodebookImportReport' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '415': { $ref: '#/components/responses/ValidationFailed' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/accounting/assets/import:
    post:
      tags: [Accounting]
      summary: Import karet majetku z XLSX/CSV
      description: 'Create vždy draft (stav ze souboru ignorován). Update jen draft; zaúčtovaná karta odlišná = error. Dry-run default.'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
                dry_run: { type: string, enum: ['0', '1'], default: '1' }
      responses:
        '200': { description: Report importu, content: { application/json: { schema: { $ref: '#/components/schemas/CodebookImportReport' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '415': { $ref: '#/components/responses/ValidationFailed' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/cash-registers:
    get:
      tags: [Accounting]
      summary: Seznam pokladen (analytiky 211) vč. zůstatku z ledgeru
      description: Pokladna = analytika 211; zůstatek počítán z ledgeru (R6). CZK-only v1.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: include_inactive, schema: { type: integer, enum: [0, 1] } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: array, items: { type: object } } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Accounting]
      summary: Nová pokladna (účetní|admin)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [name, account_code], properties: { name: { type: string }, account_code: { type: string }, currency_code: { type: string, default: CZK }, is_default: { type: boolean } } } } }
      responses:
        '201': { description: Detail nové pokladny, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/ValidationFailed' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/cash-registers/{id}:
    get:
      tags: [Accounting]
      summary: Detail pokladny (?date) + kontrolní Σ dokladů
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: query, name: date, schema: { type: string, format: date } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Accounting]
      summary: Úprava pokladny (název/analytika/výchozí/aktivní) — účetní|admin
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        content: { application/json: { schema: { type: object, properties: { name: { type: string }, account_code: { type: string }, is_default: { type: boolean }, is_active: { type: boolean } } } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/ValidationFailed' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [Accounting]
      summary: Smazání pokladny (jen bez dokladů) — účetní|admin
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/cash-registers/{id}/book:
    get:
      tags: [Accounting]
      summary: Pokladní kniha (opening + running balance + join na cash_documents, O8)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: page, schema: { type: integer } }
        - { in: query, name: per_page, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/accounting/cash-registers/{id}/book/pdf:
    get:
      tags: [Accounting]
      summary: PDF pokladní knihy (doplní PDF stage)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }

  /api/v1/accounting/cash-documents:
    get:
      tags: [Accounting]
      summary: Stránkovaný seznam pokladních dokladů (filtry)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: register_id, schema: { type: integer } }
        - { in: query, name: doc_type, schema: { type: string, enum: [in, out] } }
        - { in: query, name: purpose, schema: { type: string, enum: [sale, purchase, invoice_payment, purchase_payment, transfer, other] } }
        - { in: query, name: status, schema: { type: string, enum: [draft, posted, reversed] } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: q, schema: { type: string } }
        - { in: query, name: page, schema: { type: integer } }
        - { in: query, name: per_page, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
    post:
      tags: [Accounting]
      summary: Nový pokladní doklad (default post:true = create+post v 1 transakci) — účetní|admin
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [register_id, doc_type, purpose, issue_date, description, total_amount], properties: { register_id: { type: integer }, doc_type: { type: string, enum: [in, out] }, purpose: { type: string, enum: [sale, purchase, invoice_payment, purchase_payment, transfer, other] }, issue_date: { type: string, format: date }, tax_date: { type: string, format: date }, partner_name: { type: string }, partner_ic: { type: string }, partner_dic: { type: string }, description: { type: string }, vat_mode: { type: string, enum: [none, vat] }, total_amount: { type: number }, vat_lines: { type: array, items: { type: object, properties: { vat_rate: { type: number }, base_amount: { type: number }, vat_amount: { type: number } } } }, invoice_id: { type: integer }, purchase_invoice_id: { type: integer }, rule_key: { type: string }, counter_account_code: { type: string }, post: { type: boolean, default: true } } } } }
      responses:
        '201': { description: Vytvořeno, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/cash-documents/unpaid:
    get:
      tags: [Accounting]
      summary: Našeptávač nezaplacených FV/PF (jen CZK) pro úhradu hotově
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: kind, required: true, schema: { type: string, enum: [invoice, purchase_invoice] } }
        - { in: query, name: q, schema: { type: string } }
        - { in: query, name: limit, schema: { type: integer, default: 20 } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: array, items: { type: object } } } } }

  /api/v1/accounting/cash-documents/rule-presets:
    get:
      tags: [Accounting]
      summary: Nabídka kontačních pravidel pro pokladní doklad s purpose=other
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: doc_type, schema: { type: string, enum: [in, out] } }
      responses:
        '200': { description: "OK {items}", content: { application/json: { schema: { type: object } } } }

  /api/v1/accounting/cash-documents/{id}:
    get:
      tags: [Accounting]
      summary: Detail pokladního dokladu (vč. vat_lines, registru, vazeb)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Accounting]
      summary: Úprava draftu — účetní|admin
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    delete:
      tags: [Accounting]
      summary: Smazání draftu — účetní|admin
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/cash-documents/{id}/post:
    post:
      tags: [Accounting]
      summary: Zaúčtování draftu (idempotentní na dvojklik) — účetní|admin
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/cash-documents/{id}/reverse:
    post:
      tags: [Accounting]
      summary: Storno pokladního dokladu (povinný reason) — účetní|admin
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [reason], properties: { reason: { type: string, minLength: 3 }, entry_date: { type: string, format: date } } } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { reversal_entry_id: { type: integer } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/accounting/cash-documents/{id}/pdf:
    get:
      tags: [Accounting]
      summary: Tisk pokladního dokladu do PDF (doplní PDF stage)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }

  /api/v1/eshop/manufacturers:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Eshop]
      summary: Seznam výrobců
      parameters:
        - in: query
          name: active
          schema: { type: boolean }
          description: Jen aktivní (nearchivovaní) výrobci.
      responses:
        '200': { description: OK }
    post:
      tags: [Eshop]
      summary: Vytvoří výrobce
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, name]
              properties:
                code: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 150 }
                website: { type: [string, "null"] }
                display_order: { type: integer }
                export_eshop: { type: boolean, default: true }
                archived: { type: boolean, default: false }
      responses:
        '201': { description: Vytvořeno }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '409': { description: Výrobce s tímto kódem už existuje }
  /api/v1/eshop/manufacturers/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Detail výrobce
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Upraví výrobce
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 150 }
                website: { type: [string, "null"] }
                display_order: { type: integer }
                export_eshop: { type: boolean }
                archived: { type: boolean }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Výrobce s tímto kódem už existuje }
    delete:
      tags: [Eshop]
      summary: Smaže výrobce
      description: Odmítnuto (409), pokud je výrobce referencovaný u zboží — nabídne se archivace.
      responses:
        '200': { description: Smazáno }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Výrobce je použit u zboží — archivujte jej místo mazání }
  /api/v1/eshop/tags:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Eshop]
      summary: Seznam štítků zboží
      parameters:
        - in: query
          name: active
          schema: { type: boolean }
          description: Jen aktivní (nearchivované) štítky.
      responses:
        '200': { description: OK }
    post:
      tags: [Eshop]
      summary: Vytvoří štítek
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, name]
              properties:
                code: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 100 }
                color: { type: [string, "null"], pattern: '^#[0-9a-fA-F]{6}$', description: 'Formát #RRGGBB.' }
                archived: { type: boolean, default: false }
      responses:
        '201': { description: Vytvořeno }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '409': { description: Štítek s tímto kódem už existuje }
  /api/v1/eshop/tags/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Detail štítku
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Upraví štítek
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 100 }
                color: { type: [string, "null"], pattern: '^#[0-9a-fA-F]{6}$' }
                archived: { type: boolean }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Štítek s tímto kódem už existuje }
    delete:
      tags: [Eshop]
      summary: Smaže štítek
      responses:
        '200': { description: Smazáno }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/fee-types:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Eshop]
      summary: Seznam typů poplatků
      description: Číselník poplatků (autorský/recyklační/WEEE apod.).
      parameters:
        - in: query
          name: active
          schema: { type: boolean }
          description: Jen aktivní (nearchivované) poplatky.
      responses:
        '200': { description: OK }
    post:
      tags: [Eshop]
      summary: Vytvoří typ poplatku
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, name]
              properties:
                code: { type: string, maxLength: 30 }
                name: { type: string, maxLength: 120 }
                vat_rate_id: { type: [integer, "null"] }
                archived: { type: boolean, default: false }
      responses:
        '201': { description: Vytvořeno }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '409': { description: Poplatek s tímto kódem už existuje }
  /api/v1/eshop/fee-types/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Detail typu poplatku
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Upraví typ poplatku
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code: { type: string, maxLength: 30 }
                name: { type: string, maxLength: 120 }
                vat_rate_id: { type: [integer, "null"] }
                archived: { type: boolean }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Poplatek s tímto kódem už existuje }
    delete:
      tags: [Eshop]
      summary: Smaže typ poplatku
      description: Odmítnuto (409), pokud je poplatek referencovaný u zboží — nabídne se archivace.
      responses:
        '200': { description: Smazáno }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Poplatek je použit u zboží — archivujte jej místo mazání }
  /api/v1/eshop/attributes:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Eshop]
      summary: Seznam parametrů/atributů zboží
      description: U typu `enum` je v odpovědi navíc pole `options` s výčtem hodnot.
      parameters:
        - in: query
          name: active
          schema: { type: boolean }
          description: Jen aktivní (nearchivované) parametry.
      responses:
        '200': { description: OK }
    post:
      tags: [Eshop]
      summary: Vytvoří parametr
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, name]
              properties:
                code: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 120 }
                data_type: { type: string, enum: [text, number, bool, enum], default: text }
                unit: { type: [string, "null"] }
                is_filterable: { type: boolean, default: false }
                is_multivalue: { type: boolean, default: false }
                display_order: { type: integer }
                archived: { type: boolean, default: false }
      responses:
        '201': { description: Vytvořeno }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '409': { description: Parametr s tímto kódem už existuje }
  /api/v1/eshop/attributes/{id}/options:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
        description: Id parametru (musí být typu `enum`).
    get:
      tags: [Eshop]
      summary: Seznam hodnot (options) enum parametru
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Eshop]
      summary: Přidá hodnotu (option) enum parametru
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, label]
              properties:
                code: { type: string, maxLength: 50 }
                label: { type: string, maxLength: 120 }
                display_order: { type: integer }
      responses:
        '201': { description: Vytvořeno }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/attributes/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Detail parametru (vč. options u enum)
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Upraví parametr
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 120 }
                data_type: { type: string, enum: [text, number, bool, enum] }
                unit: { type: [string, "null"] }
                is_filterable: { type: boolean }
                is_multivalue: { type: boolean }
                display_order: { type: integer }
                archived: { type: boolean }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Parametr s tímto kódem už existuje }
    delete:
      tags: [Eshop]
      summary: Smaže parametr
      description: Odmítnuto (409), pokud je parametr použit u zboží — nabídne se archivace.
      responses:
        '200': { description: Smazáno }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Parametr je použit u zboží — archivujte jej místo mazání }
  /api/v1/eshop/attribute-options/{oid}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: oid
        required: true
        schema: { type: integer, format: int64 }
    put:
      tags: [Eshop]
      summary: Upraví hodnotu (option) enum parametru
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, label]
              properties:
                code: { type: string, maxLength: 50 }
                label: { type: string, maxLength: 120 }
                display_order: { type: integer }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Eshop]
      summary: Smaže hodnotu (option) enum parametru
      responses:
        '200': { description: Smazáno }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/categories:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Eshop]
      summary: Seznam kategorií (strom)
      description: Vrací celý strom kategorií tenanta (materialized path — `path`/`depth`).
      responses:
        '200': { description: OK }
    post:
      tags: [Eshop]
      summary: Vytvoří kategorii
      description: Zařadí novou kategorii pod `parent_id` (nebo do kořene); `path`/`depth` se dopočítají.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, name]
              properties:
                code: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 150 }
                parent_id: { type: [integer, "null"] }
                display_order: { type: integer }
                export_eshop: { type: boolean, default: true }
                archived: { type: boolean, default: false }
      responses:
        '201': { description: Vytvořeno }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '409': { description: Kategorie s tímto kódem už existuje }
  /api/v1/eshop/categories/{id}/i18n:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Seznam překladů kategorie
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Uloží překlady kategorie (upsert po jazycích)
      description: |
        Přijímá pole `translations` (nebo přímo pole objektů) — každý záznam
        `{locale, name, description?, seo_slug?}`. Neplatné/prázdné řádky (chybí
        `locale` nebo `name`, `locale` > 5 znaků) se přeskočí. Vrací aktuální
        seznam překladů po zápisu.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                translations:
                  type: array
                  items:
                    type: object
                    required: [locale, name]
                    properties:
                      locale: { type: string, maxLength: 5 }
                      name: { type: string }
                      description: { type: [string, "null"] }
                      seo_slug: { type: [string, "null"] }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/categories/{id}/move:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Eshop]
      summary: Přesune kategorii pod jiného rodiče
      description: |
        `parent_id=null` přesune uzel do kořene. Přepíše `path`/`depth` celého
        podstromu. Odmítne (422) cyklus — nový rodič nesmí být uzel sám ani
        jeho potomek.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                parent_id: { type: [integer, "null"] }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: Cyklus — kategorii nelze přesunout do vlastního podstromu }
  /api/v1/eshop/categories/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Detail kategorie (vč. i18n)
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Upraví kategorii
      description: Neřeší přeřazení rodiče — na to slouží `POST /{id}/move`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 150 }
                display_order: { type: integer }
                export_eshop: { type: boolean }
                archived: { type: boolean }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Kategorie s tímto kódem už existuje }
    delete:
      tags: [Eshop]
      summary: Smaže kategorii
      responses:
        '200': { description: Smazáno }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/products/import:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Eshop]
      summary: Import zboží z XLSX/CSV
      description: |
        Dry-run náhled (default `dry_run=1`). Ostrý běh (`dry_run=0`) je
        all-or-nothing — jakákoli chybová řádka vrátí 422 `import_has_errors`
        a nic se nezapíše. Soubor max 2 MB, whitelist přípon xlsx/csv, MIME se
        ověřuje z obsahu (ne jen z přípony).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
                dry_run: { type: string, enum: ['0', '1'], default: '1' }
      responses:
        '200': { description: Report importu, content: { application/json: { schema: { $ref: '#/components/schemas/CodebookImportReport' } } } }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '415': { description: Chybějící/nepodporovaný soubor nebo neshoda MIME s příponou }
        '422': { description: Import obsahuje chyby — nic nebylo zapsáno (import_has_errors) }
  /api/v1/eshop/products/{id}/i18n:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Seznam překladů karty zboží
      responses:
        '200': { description: OK }
  /api/v1/eshop/products/{id}/media:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Seznam médií karty zboží
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Eshop]
      summary: Nahraje média ke kartě zboží
      description: |
        Multipart pole `file` (jeden i více souborů, max 50 na request). Bajty
        se ukládají do content-addressed úložiště sdíleného s DMS (MIME z
        obsahu, blocklist spustitelných, dedup dle sha256). První nahraný
        obrázek se automaticky stane hlavním (`is_primary`), pokud karta ještě
        žádný hlavní obrázek nemá. Odpověď obsahuje `created` a `errors` (per
        soubor).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  oneOf:
                    - { type: string, format: binary }
                    - { type: array, items: { type: string, format: binary } }
      responses:
        '201': { description: Nahráno (aspoň jeden soubor uložen) }
        '400': { description: Žádný soubor odeslán nebo se nepodařilo uložit žádný soubor }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { description: Příliš mnoho souborů najednou (max 50) }
  /api/v1/eshop/products/{id}/media/reorder:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    put:
      tags: [Eshop]
      summary: Přeuspořádá média karty zboží
      description: Přijme pole `order` s id médií v novém pořadí; přijmou se jen id patřící této kartě.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                order:
                  type: array
                  items: { type: integer }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/products/{id}/prices:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Seznam cen karty zboží (per měna)
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Uloží ceny karty zboží (bulk) a přepočte je
      description: |
        Nahradí ceny (`prices`) — měny, které v payloadu nejsou, se smažou.
        Každý řádek: `currency_code` (ISO 4217), `price_mode` (markup/fixed),
        `markup_pct`, `fixed_price` (povinná u `fixed`), `rounding`,
        `is_manual_override`. Po zápisu se vždy spustí přepočet ceny.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                prices:
                  type: array
                  items:
                    type: object
                    required: [currency_code]
                    properties:
                      currency_code: { type: string, minLength: 3, maxLength: 3 }
                      price_mode: { type: string, enum: [markup, fixed], default: markup }
                      markup_pct: { type: [number, "null"] }
                      fixed_price: { type: [number, "null"] }
                      rounding: { type: string, enum: [none, '0.01', '0.10', '0.50', '1', '9_ending'], default: none }
                      is_manual_override: { type: boolean, default: false }
      responses:
        '200': { description: Výsledek přepočtu cen }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/products/{id}/prices/recompute:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Eshop]
      summary: Vynutí přepočet cen karty zboží
      responses:
        '200': { description: Výsledek přepočtu cen }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/products/{id}/vendors:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Seznam dodavatelů karty zboží
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Nahradí dodavatele karty zboží
      description: |
        Kompletní náhrada seznamu (`vendors`) — každý `client_id` musí patřit
        tenantovi a mít `is_vendor=1`, jinak 422 `vendor_invalid`. Nejvýše
        jeden dodavatel smí mít `is_preferred=true` (jinak 422
        `multiple_preferred_vendors`). Po zápisu se přepočte cena zboží
        (relevantní při `pricing_base=manual`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                vendors:
                  type: array
                  items:
                    type: object
                    required: [client_id]
                    properties:
                      client_id: { type: integer }
                      vendor_sku: { type: [string, "null"] }
                      purchase_price: { type: [number, "null"] }
                      currency_code: { type: string, minLength: 3, maxLength: 3, default: CZK }
                      delivery_days: { type: [integer, "null"] }
                      stock_qty: { type: [number, "null"] }
                      is_preferred: { type: boolean, default: false }
                      note: { type: [string, "null"] }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: Neplatný dodavatel nebo více preferovaných dodavatelů }
  /api/v1/eshop/products/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Karta zboží (agregát)
      description: |
        Vrací agregát nad `stock_items` (item_type=goods): eshopové sloupce +
        `i18n`, `categories`, `tag_ids`, `attributes`, `fees`, `media`.
        Základní skladová identita (sku/název/DPH/cena) se edituje přes
        `/api/stock/items`.
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Eshop]
      summary: Upraví eshopový obsah karty zboží (partial update agregátu)
      description: |
        Zapisují se jen přítomné sekce payloadu. `manufacturer_id` (pokud
        zadán) musí patřit tenantovi. `categories` — pole objektů
        `{category_id, is_primary?, display_order?}` (max 1 primary, jinak
        422). `tag_ids` — pole id štítků. `fees` — pole
        `{fee_type_id, amount, currency_code, vat_included}`. `attributes` —
        typované hodnoty parametrů. Změna `pricing_base` vyvolá přepočet cen.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                manufacturer_id: { type: [integer, "null"] }
                warranty_months: { type: [integer, "null"] }
                delivery_days: { type: [integer, "null"] }
                export_eshop: { type: boolean }
                is_stocked: { type: boolean }
                weight_g: { type: [integer, "null"] }
                pricing_base: { type: string }
                i18n:
                  type: array
                  items: { type: object }
                categories:
                  type: array
                  items:
                    type: object
                    properties:
                      category_id: { type: integer }
                      is_primary: { type: boolean }
                      display_order: { type: integer }
                tag_ids:
                  type: array
                  items: { type: integer }
                attributes:
                  type: array
                  items: { type: object }
                fees:
                  type: array
                  items:
                    type: object
                    properties:
                      fee_type_id: { type: integer }
                      amount: { type: number }
                      currency_code: { type: string }
                      vat_included: { type: boolean }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: Neplatná vazba (výrobce/kategorie/štítek) nebo více hlavních kategorií }
  /api/v1/eshop/media/{mid}/file:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: mid
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Eshop]
      summary: Stáhne bajty média
      description: |
        Servíruje soubor média. Skutečné obrázky inline (pro `<img>`), ostatní
        typy vždy jako attachment. Vždy `X-Content-Type-Options: nosniff` a
        restriktivní CSP (obrana proti záměně typu/vloženému skriptu).
      responses:
        '200':
          description: Bajty souboru
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/eshop/media/{mid}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: mid
        required: true
        schema: { type: integer, format: int64 }
    put:
      tags: [Eshop]
      summary: Upraví metadata média
      description: |
        `is_primary=true` nastaví médium jako hlavní obrázek karty (nejvýše
        jedno na kartu — předchozí hlavní se transakčně zruší).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: [string, "null"] }
                alt_text: { type: [string, "null"] }
                export_eshop: { type: boolean }
                display_order: { type: integer }
                is_primary: { type: boolean }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Eshop]
      summary: Smaže médium karty zboží
      description: Maže jen záznam vazby — sdílené bajty (content-addressed) se neuklízí zde, o to se stará údržbová GC úloha.
      responses:
        '200': { description: Smazáno }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/stock/warehouses:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Seznam skladů
      description: |
        Číselník skladů vč. aktuální hodnoty (`value`) každého skladu. Filtr `active=1`
        vrátí jen aktivní sklady.
      parameters:
        - in: query
          name: active
          schema: { type: boolean }
          description: Jen aktivní sklady.
      responses:
        '200': { description: OK }
    post:
      tags: [Stock]
      summary: Nový sklad
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, name]
              properties:
                code: { type: string, maxLength: 20, description: 'Unikátní kód skladu.' }
                name: { type: string, maxLength: 100 }
                is_default: { type: boolean }
                is_active: { type: boolean, default: true }
                note: { type: [string, "null"] }
      responses:
        '201': { description: Vytvořeno }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '409': { description: 'Sklad s tímto kódem už existuje.' }
  /api/v1/stock/warehouses/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Stock]
      summary: Detail skladu
      description: Vč. aktuální hodnoty skladu (`value`).
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Stock]
      summary: Úprava skladu
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code: { type: string, maxLength: 20 }
                name: { type: string, maxLength: 100 }
                is_default: { type: boolean }
                is_active: { type: boolean }
                note: { type: [string, "null"] }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Sklad s tímto kódem už existuje.' }
    delete:
      tags: [Stock]
      summary: Smazání skladu
      description: |
        Smaže sklad jen pokud nemá nenulový stav ani skladové pohyby. Jinak vrátí
        409 s `suggestion: deactivate` — místo mazání deaktivujte sklad (`is_active=false`).
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Sklad má nenulový stav nebo pohyby — nelze smazat.' }
  /api/v1/stock/items/search:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Našeptávač skladových karet (autocomplete)
      parameters:
        - in: query
          name: q
          schema: { type: string }
          description: Hledaný text (SKU, název, EAN).
        - in: query
          name: limit
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
      responses:
        '200': { description: OK }
  /api/v1/stock/items:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Seznam skladových karet
      parameters:
        - in: query
          name: type
          schema: { type: string, enum: [material, goods, product] }
        - in: query
          name: active
          schema: { type: boolean }
        - in: query
          name: q
          schema: { type: string }
          description: Fulltextové hledání (SKU, název, EAN).
        - in: query
          name: only_below_min
          schema: { type: boolean }
          description: Jen karty pod minimální zásobou (`min_qty`).
        - in: query
          name: limit
          schema: { type: integer, default: 200, minimum: 1, maximum: 500 }
        - in: query
          name: offset
          schema: { type: integer, default: 0, minimum: 0 }
      responses:
        '200': { description: OK }
    post:
      tags: [Stock]
      summary: Nová skladová karta
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                sku: { type: string, maxLength: 50, description: 'Nepovinné — bez zadání se odvodí slug z názvu.' }
                name: { type: string, maxLength: 255 }
                item_type: { type: string, enum: [material, goods, product], default: goods }
                unit: { type: string, default: ks }
                ean: { type: [string, "null"] }
                vat_rate_id: { type: [integer, "null"] }
                sale_price_without_vat: { type: [string, "null"] }
                min_qty: { type: [string, "null"] }
                is_active: { type: boolean, default: true }
                note: { type: [string, "null"] }
      responses:
        '201': { description: Vytvořeno }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '409': { description: 'Skladová karta s tímto SKU už existuje.' }
  /api/v1/stock/items/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Stock]
      summary: Detail skladové karty
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Stock]
      summary: Úprava skladové karty
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sku: { type: string, maxLength: 50 }
                name: { type: string, maxLength: 255 }
                item_type: { type: string, enum: [material, goods, product] }
                unit: { type: string }
                ean: { type: [string, "null"] }
                vat_rate_id: { type: [integer, "null"] }
                sale_price_without_vat: { type: [string, "null"] }
                min_qty: { type: [string, "null"] }
                is_active: { type: boolean }
                note: { type: [string, "null"] }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Skladová karta s tímto SKU už existuje.' }
    delete:
      tags: [Stock]
      summary: Smazání skladové karty
      description: |
        Smaže kartu jen pokud nemá žádné skladové pohyby. Jinak 409 s
        `suggestion: deactivate` — místo mazání kartu deaktivujte.
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Karta má skladové pohyby — nelze smazat.' }
  /api/v1/stock/items/{id}/movements:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Stock]
      summary: Skladová kniha karty (pohyby)
      description: |
        Stránkovaný výpis pohybů karty s běžnou bilancí (`balance_after` po každém
        řádku) a počáteční bilancí (`opening_balance`) k dané stránce.
      parameters:
        - in: query
          name: warehouse_id
          schema: { type: integer }
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: limit
          schema: { type: integer, default: 100, minimum: 1, maximum: 500 }
        - in: query
          name: offset
          schema: { type: integer, default: 0, minimum: 0 }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/stock/items/{id}/movements/export:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Stock]
      summary: Export skladové karty (PDF/XLSX)
      description: |
        Kompletní historie pohybů karty (bez stránkování, celý rozsah dle filtrů
        `warehouse_id`/`from`/`to`) jako soubor ke stažení.
      parameters:
        - in: query
          name: format
          required: true
          schema: { type: string, enum: [pdf, xlsx] }
        - in: query
          name: warehouse_id
          schema: { type: integer }
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
      responses:
        '200':
          description: 'Soubor (PDF nebo XLSX dle `format`).'
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/stock/levels:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Stav zásob
      description: Přehled aktuálních stavů skladu (skladová karta × sklad) s filtry.
      parameters:
        - in: query
          name: warehouse_id
          schema: { type: integer }
        - in: query
          name: item_type
          schema: { type: string, enum: [material, goods, product] }
        - in: query
          name: below_min
          schema: { type: boolean }
          description: Jen položky pod minimální zásobou.
        - in: query
          name: active
          schema: { type: boolean }
        - in: query
          name: q
          schema: { type: string }
      responses:
        '200': { description: OK }
  /api/v1/stock/availability:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Dávková dostupnost skladových karet
      description: |
        Použito pro badge dostupnosti v editoru faktury — vrátí dostupné množství
        pro zadané karty (volitelně na konkrétním skladu).
      parameters:
        - in: query
          name: item_ids
          required: true
          schema: { type: string }
          description: 'Seznam ID karet oddělený čárkou, např. `12,45,78`.'
        - in: query
          name: warehouse_id
          schema: { type: integer }
      responses:
        '200': { description: OK }
  /api/v1/stock/documents:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Seznam skladových dokladů
      description: Stránkovaný seznam dokladů PRI/VYD/PRE (příjemka/výdejka/převodka).
      parameters:
        - in: query
          name: doc_type
          schema: { type: string, enum: [receipt, issue, transfer] }
        - in: query
          name: status
          schema: { type: string, enum: [draft, posted, reversed] }
        - in: query
          name: origin
          schema: { type: string, enum: [manual, invoice, credit_note, purchase_invoice, inventory] }
        - in: query
          name: warehouse_id
          schema: { type: integer }
        - in: query
          name: q
          schema: { type: string }
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: limit
          schema: { type: integer, default: 100, minimum: 1, maximum: 500 }
        - in: query
          name: offset
          schema: { type: integer, default: 0, minimum: 0 }
      responses:
        '200': { description: 'OK — `{items, total, limit, offset}`.' }
    post:
      tags: [Stock]
      summary: Nový skladový doklad (draft)
      description: |
        Založí draft dokladu (hlavička + řádky). U `doc_type=transfer` je povinný i
        `warehouse_to_id` (odlišný od `warehouse_id`). U příjemky se ocenění řádků
        (`unit_cost`/`extra_cost`) počítá zadanou pořizovací cenou; u výdeje/převodky
        se doplní až při zaúčtování (`post`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [doc_type, doc_date, description, warehouse_id, lines]
              properties:
                doc_type: { type: string, enum: [receipt, issue, transfer] }
                origin: { type: string, enum: [manual, invoice, credit_note, purchase_invoice, inventory], default: manual }
                doc_date: { type: string, format: date }
                description: { type: string, maxLength: 255 }
                warehouse_id: { type: integer }
                warehouse_to_id: { type: [integer, "null"], description: 'Povinné jen u transfer.' }
                partner_name: { type: [string, "null"] }
                invoice_id: { type: [integer, "null"] }
                purchase_invoice_id: { type: [integer, "null"] }
                stock_take_id: { type: [integer, "null"] }
                lines:
                  type: array
                  items:
                    type: object
                    required: [stock_item_id, qty]
                    properties:
                      stock_item_id: { type: integer }
                      qty: { type: string }
                      unit_cost: { type: string, description: 'Jen u receipt.' }
                      extra_cost: { type: string, description: 'Vedlejší pořizovací náklady, jen u receipt.' }
                      invoice_item_id: { type: [integer, "null"] }
                      purchase_invoice_item_id: { type: [integer, "null"] }
                      source_description: { type: [string, "null"] }
                      source_qty: { type: [string, "null"] }
                      note: { type: [string, "null"] }
      responses:
        '201': { description: 'Vytvořeno (draft).' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/stock/documents/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Stock]
      summary: Detail skladového dokladu
      description: Vč. řádků.
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Stock]
      summary: Úprava skladového dokladu (draft)
      description: |
        Přepíše hlavičku a nahradí řádky. Lze upravovat jen doklad ve stavu `draft`
        (jinak 422 `not_draft`). Tělo stejné jako u vytvoření.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                doc_type: { type: string, enum: [receipt, issue, transfer] }
                origin: { type: string, enum: [manual, invoice, credit_note, purchase_invoice, inventory] }
                doc_date: { type: string, format: date }
                description: { type: string, maxLength: 255 }
                warehouse_id: { type: integer }
                warehouse_to_id: { type: [integer, "null"] }
                partner_name: { type: [string, "null"] }
                lines:
                  type: array
                  items: { type: object }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: 'Doklad není draft, nebo validace řádků selhala.' }
    delete:
      tags: [Stock]
      summary: Smazání skladového dokladu (draft)
      description: Smazat lze jen doklad ve stavu `draft`.
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: 'Doklad není draft.' }
  /api/v1/stock/documents/{id}/post:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Stock]
      summary: Zaúčtování skladového dokladu
      description: |
        Zaúčtuje draft: kontrola dostupnosti (výdej/převodka nesmí jít do minusu),
        aplikace pohybů na stavy skladu (naskladnění u receipt, vyskladnění u issue,
        výdej+příjem u transfer), přidělení čísla řady a přechod do stavu `posted`.
        Idempotentní — opakované zavolání na už zaúčtovaném dokladu vrátí doklad
        beze změny. Blokováno otevřenou inventurou (status `counting`) na dotčeném
        skladu a uzavřeným účetním obdobím.
      responses:
        '200': { description: 'Zaúčtováno — doklad vč. `warnings` (např. neaktivní karta na řádku).' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Nedostatek zásob, otevřená inventura na skladu, nebo uzavřené účetní období.' }
        '422': { description: 'Doklad není draft, nebo je neplatný (chybí sklad/řádky).' }
  /api/v1/stock/documents/{id}/reverse:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Stock]
      summary: Storno skladového dokladu
      description: |
        Vytvoří protidoklad v PŮVODNÍ ceně (hodnotově neutrální storno) — opačný
        `doc_type` (u převodky s prohozenými sklady), stejné řádky a `doc_date`
        původního dokladu; protidoklad se rovnou zaúčtuje a původní doklad se
        označí jako `reversed`. Storno lze provést jen na zaúčtovaném (`posted`)
        dokladu; storno příjemky po výdejích, které by způsobilo záporný stav,
        je odmítnuto (409).
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, description: 'Volitelný důvod storna (do popisu protidokladu).' }
      responses:
        '200': { description: 'Storno provedeno — `{original, reversal}`.' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Doklad už je stornovaný, není zaúčtovaný, nedostatek zásob, otevřená inventura, nebo uzavřené období.' }
  /api/v1/stock/documents/{id}/pdf:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Stock]
      summary: Tisk skladového dokladu (PDF)
      description: PDF skladového dokladu (PRI/VYD/PRE) vč. hlavičky, řádků a celkové hodnoty.
      responses:
        '200':
          description: PDF ke stažení (inline).
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/stock/takes:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Seznam inventur
      parameters:
        - in: query
          name: warehouse_id
          schema: { type: integer }
        - in: query
          name: status
          schema: { type: string, enum: [draft, counting, closed] }
      responses:
        '200': { description: OK }
    post:
      tags: [Stock]
      summary: Nová inventura (draft)
      description: |
        Založí novou inventuru ve stavu `draft`. Na skladu smí být rozpracovaná
        (draft/counting) jen jedna inventura zaráz — jinak 409.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [warehouse_id, take_date, counting_method, responsible_count_name, responsible_inventory_name]
              properties:
                warehouse_id: { type: integer }
                take_date: { type: string, format: date }
                note: { type: [string, "null"] }
                counting_method: { type: string, enum: [physical_count, measurement, weighing, other] }
                responsible_count_name: { type: string, maxLength: 255 }
                responsible_inventory_name: { type: string, maxLength: 255 }
      responses:
        '201': { description: Vytvořeno }
        '409': { description: 'Na skladu už běží rozpracovaná inventura.' }
        '422': { description: 'Sklad nenalezen nebo neaktivní, nebo chybí datum.' }
  /api/v1/stock/takes/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Stock]
      summary: Detail inventury
      description: Vč. řádků s `diff_qty` (rozdíl skutečnost−očekávání, `null` pokud nepočítáno).
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Stock]
      summary: Zadání skutečných počtů
      description: Zadání/úprava `counted_qty` na řádcích inventury — jen ve fázi `counting`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [lines]
              properties:
                lines:
                  type: array
                  items:
                    type: object
                    required: [id]
                    properties:
                      id: { type: integer, description: 'ID řádku inventury (stock_take_lines.id).' }
                      counted_qty: { type: [string, "null"], description: 'null = nepočítáno.' }
                      surplus_unit_cost: { type: [string, "null"], description: 'Reprodukční pořizovací cena za MJ pro kladný inventurní rozdíl.' }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: 'Inventura není ve fázi counting.' }
  /api/v1/stock/takes/{id}/start:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Stock]
      summary: Spuštění inventury (draft → counting)
      description: |
        Vytvoří snapshot očekávaných stavů (`expected_qty`/`expected_value`) všech
        aktivních karet skladu k okamžiku spuštění a přepne inventuru do fáze
        `counting`. Lze spustit jen inventuru ve stavu `draft`.
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: 'Inventura není draft.' }
  /api/v1/stock/takes/{id}/close:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Stock]
      summary: Uzavření inventury (counting → closed)
      description: |
        Spočítá rozdíly (skutečnost−očekávání) na počítaných řádcích a vygeneruje
        rozdílové skladové doklady (origin=`inventory`, rovnou zaúčtované) — souhrnnou
        příjemku za přebytky a/nebo souhrnnou výdejku za manka; nepočítané řádky
        (`counted_qty=null`) beze změny. Přepne inventuru do stavu `closed`. Lze
        uzavřít jen inventuru ve fázi `counting`.
      responses:
        '200': { description: 'Uzavřeno — vč. `receipt_document`/`issue_document` (může být `null`).' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Zaúčtování rozdílového dokladu selhalo (např. uzavřené období).' }
        '422': { description: 'Inventura není ve fázi counting.' }
  /api/v1/stock/takes/{id}/pdf:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Stock]
      summary: Inventurní soupis PDF
      responses:
        '200':
          description: Průkazný inventurní soupis s odpovědnými osobami, okamžiky a podpisovými řádky.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/stock/reports/status:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Sestava — stav zásob
      parameters:
        - in: query
          name: warehouse_id
          schema: { type: integer }
        - in: query
          name: item_type
          schema: { type: string, enum: [material, goods, product] }
        - in: query
          name: below_min
          schema: { type: boolean }
        - in: query
          name: active
          schema: { type: boolean }
        - in: query
          name: q
          schema: { type: string }
      responses:
        '200': { description: OK }
  /api/v1/stock/reports/valuation:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Stock]
      summary: Sestava — ocenění zásob k datu
      description: Ocenění stavu zásob k historickému datu (rekonstrukce z pohybů).
      parameters:
        - in: query
          name: date
          schema: { type: string, format: date }
          description: Výchozí dnešní datum.
        - in: query
          name: warehouse_id
          schema: { type: integer }
      responses:
        '200': { description: OK }
  /api/v1/stock/reports/{name}/export:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: name
        required: true
        schema: { type: string, enum: [status, valuation] }
    get:
      tags: [Stock]
      summary: Export sestavy (PDF/XLSX)
      description: Export sestavy `status` nebo `valuation` (stejné filtry jako u dat sestavy).
      parameters:
        - in: query
          name: format
          required: true
          schema: { type: string, enum: [pdf, xlsx] }
        - in: query
          name: warehouse_id
          schema: { type: integer }
        - in: query
          name: item_type
          schema: { type: string, enum: [material, goods, product] }
          description: Jen u sestavy status.
        - in: query
          name: below_min
          schema: { type: boolean }
          description: Jen u sestavy status.
        - in: query
          name: active
          schema: { type: boolean }
          description: Jen u sestavy status.
        - in: query
          name: q
          schema: { type: string }
          description: Jen u sestavy status.
        - in: query
          name: date
          schema: { type: string, format: date }
          description: Jen u sestavy valuation.
      responses:
        '200':
          description: 'Soubor (PDF nebo XLSX dle `format`).'
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
        '404': { description: 'Neznámá sestava.' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/documents/{entity_type}/{id}/signature-selection:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: entity_type
        required: true
        schema: { type: string, enum: [invoice, work_report] }
      - in: path
        name: id
        required: true
        schema: { type: integer }
        description: ID faktury (u `work_report` je to ID faktury, ke které výkaz patří).
    get:
      tags: [Documents]
      summary: Per-dokumentový výběr podpisového profilu — aktuální stav
      description: |
        Vrací efektivní i vlastní (override) nastavení výběru podpisového profilu pro
        PDF konkrétního dokladu: `selection_source` (`inherit`/`logged_in_user`/`admin_profile_settings`),
        zděděné nastavení z admin output settings a `effective_will_sign` (skutečně se
        doklad podepíše — zapnutý výstup + resolvovatelný profil s certifikátem).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  usage: { type: string, enum: [pdf] }
                  entity_type: { type: string }
                  entity_id: { type: integer }
                  selection_source: { type: string, enum: [inherit, logged_in_user, admin_profile_settings] }
                  admin_profile_id: { type: [integer, "null"] }
                  inherited_selection_source: { type: string }
                  inherited_admin_profile_id: { type: [integer, "null"] }
                  effective_selection_source: { type: string }
                  effective_admin_profile_id: { type: [integer, "null"] }
                  has_override: { type: boolean }
                  effective_will_sign: { type: boolean }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Documents]
      summary: Nastaví (nebo zdědí) výběr podpisového profilu pro doklad
      description: |
        `selection_source = inherit` (nebo prázdné) smaže override a doklad se vrátí
        ke zděděnému nastavení dodavatele. `admin_profile_id` smí nastavit jen admin.
        Po změně se u faktury invaliduje vygenerované PDF.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                selection_source: { type: string, enum: [inherit, logged_in_user, admin_profile_settings] }
                admin_profile_id: { type: [integer, "null"] }
      responses:
        '200': { description: Aktuální stav výběru }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Documents]
      summary: Zruší override výběru podpisového profilu (návrat ke zděděnému nastavení)
      responses:
        '200': { description: Aktuální stav výběru }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/document-folders/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    patch:
      tags: [Documents]
      summary: Přejmenování složky
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 255 }
      responses:
        '200': { description: ok }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Složka se stejným názvem už v cílovém rodiči existuje (duplicate)' }
    delete:
      tags: [Documents]
      summary: Přesun podstromu složky do koše (soft-delete)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  trashed_folders: { type: integer, description: Počet smazaných složek podstromu vč. této. }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/document-folders/{id}/move:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    post:
      tags: [Documents]
      summary: Přesun složky pod jinou nadřazenou složku
      description: Zakázán přesun do sebe sama nebo do vlastního potomka (cyklus).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                parent_id: { type: [integer, "null"], description: 'NULL = přesun do kořene' }
      responses:
        '200': { description: ok }
        '400': { description: 'Cyklus — přesun do vlastního potomka' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/document-folders/{id}/restore:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    post:
      tags: [Documents]
      summary: Obnovení složky z koše
      responses:
        '200': { description: ok }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/documents/zip-import:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Documents]
      summary: Nahraje ZIP a rozbalí ho na pozadí (async job)
      description: |
        Uloží ZIP a založí background job `document_zip_import`; sleduj přes
        `GET /documents/jobs/{id}`. Určeno pro velké archivy (obchází PHP limity uploadu).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary, description: ZIP archiv }
                folder_id: { type: [integer, "null"], description: 'Cílová složka; prázdné = kořen' }
      responses:
        '200':
          description: Job založen
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: integer }
                  status: { type: string, enum: [queued] }
        '400': { description: 'Chybí soubor / cílová složka nenalezena' }
        '415': { description: 'Soubor není ZIP' }
        '500': { description: 'Úložiště nezapisovatelné / chybí DB migrace pro job' }
  /api/v1/documents/export:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Documents]
      summary: Sestaví ZIP export vybraných dokumentů/složek na pozadí (async job)
      description: |
        Založí background job `document_zip_export` se zachovanou strukturou složek.
        Výsledek stáhneš přes `GET /documents/jobs/{id}/download` po dokončení.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids: { type: array, items: { type: integer }, description: ID dokumentů }
                folder_ids: { type: array, items: { type: integer }, description: ID složek (exportují se rekurzivně) }
      responses:
        '200':
          description: Job založen
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: integer }
                  status: { type: string, enum: [queued] }
        '400': { description: 'Nebyly vybrány žádné položky' }
  /api/v1/documents/upload/start:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Documents]
      summary: Zahájí chunkovaný upload — založí job a staging adresář
      description: |
        Krok 1 chunkovaného upload protokolu (obchází `post_max_size`/`upload_max_filesize`):
        `start` → 1..N × `chunk-bytes` (mode `zip-explode`/`single`) nebo `chunk-files`
        (mode `folder`) → `finish` (spustí worker). `mode=single` uloží `name` pro
        pozdější manifest.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mode]
              properties:
                mode: { type: string, enum: [zip-explode, folder, single] }
                folder_id: { type: [integer, "null"] }
                name: { type: string, description: 'Jen mode=single — název souboru' }
      responses:
        '200':
          description: Job/staging založen
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: integer }
                  mode: { type: string }
        '400': { description: 'Neplatný mode' }
        '404': { description: 'Cílová složka nenalezena' }
        '500': { description: 'Úložiště nezapisovatelné / chybí DB migrace pro job' }
  /api/v1/documents/upload/chunk-bytes:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: job_id
        required: true
        schema: { type: integer }
    post:
      tags: [Documents]
      summary: Nahraje jeden bytový chunk (mode zip-explode/single)
      description: Tělo požadavku je surový binární chunk, který se append-uje na disk k jobu.
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, format: binary }
      responses:
        '200':
          description: Aktuální velikost akumulovaného souboru
          content:
            application/json:
              schema:
                type: object
                properties:
                  size: { type: integer }
        '409': { description: 'Neplatný/již dokončený job nebo chybí staging' }
        '413': { description: 'Soubor přesáhl 1 GiB strop' }
        '500': { description: 'Zápis chunku selhal' }
  /api/v1/documents/upload/chunk-files:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: job_id
        required: true
        schema: { type: integer }
    post:
      tags: [Documents]
      summary: Nahraje dávku souborů s relativními cestami (mode folder)
      description: Určeno pro upload celého adresáře — zachovává relativní cestu z `relpaths[]`.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                'file[]': { type: array, items: { type: string, format: binary } }
                'relpaths[]': { type: array, items: { type: string }, description: 'Relativní cesta odpovídající stejnému indexu file[]' }
      responses:
        '200':
          description: Počet přidaných souborů do stagingu
          content:
            application/json:
              schema:
                type: object
                properties:
                  added: { type: integer }
        '409': { description: 'Neplatný/již dokončený job nebo chybí staging' }
  /api/v1/documents/upload/finish:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Documents]
      summary: Dokončí chunkovaný upload a spustí zpracování na pozadí
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [job_id]
              properties:
                job_id: { type: integer }
      responses:
        '200':
          description: Zpracování spuštěno
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: integer }
                  status: { type: string, enum: [queued] }
        '409': { description: 'Neplatný nebo již dokončený job' }
  /api/v1/documents/jobs:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Documents]
      summary: Seznam background jobů sekce Dokumenty (import/export)
      description: |
        Posledních 30 jobů dodavatele typu `document_zip_import`, `document_zip_export`
        nebo `document_folder_import`. Non-admin vidí jen vlastní joby (`created_by`),
        admin vidí všechny (vlastnická osa napříč uživateli dodavatele).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobs:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        source: { type: string, enum: [document_zip_import, document_zip_export, document_folder_import] }
                        status: { type: string, enum: [queued, running, completed, failed, cancelled] }
                        total_items: { type: [integer, "null"] }
                        processed: { type: integer }
                        created_count: { type: integer }
                        skipped_count: { type: integer }
                        failed_count: { type: integer }
                        current_step: { type: [string, "null"] }
                        last_error: { type: [string, "null"] }
                        cancel_requested: { type: boolean }
                        result_name: { type: [string, "null"] }
                        result_size: { type: [integer, "null"] }
                        created_at: { type: string, format: date-time }
                        finished_at: { type: [string, "null"], format: date-time }
  /api/v1/documents/jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    get:
      tags: [Documents]
      summary: Stav background jobu (polling)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  source: { type: string, enum: [document_zip_import, document_zip_export, document_folder_import] }
                  status: { type: string, enum: [queued, running, completed, failed, cancelled] }
                  total_items: { type: [integer, "null"] }
                  processed: { type: integer }
                  created_count: { type: integer }
                  skipped_count: { type: integer }
                  failed_count: { type: integer }
                  current_step: { type: [string, "null"] }
                  last_error: { type: [string, "null"] }
                  cancel_requested: { type: boolean }
                  result_name: { type: [string, "null"] }
                  result_size: { type: [integer, "null"] }
                  created_at: { type: string, format: date-time }
                  finished_at: { type: [string, "null"], format: date-time }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Documents]
      summary: Smaže job (vč. výsledného souboru na disku, pokud existuje)
      responses:
        '200': { description: ok }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/documents/jobs/{id}/download:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    get:
      tags: [Documents]
      summary: Stažení výsledku dokončeného jobu (typicky ZIP export)
      description: Vyžaduje job ve stavu `completed` s uloženým výsledkem; jinak `404`.
      responses:
        '200':
          description: Binární obsah výsledku (`Content-Type` dle `result_mime`, default `application/zip`)
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '404': { description: 'Job nenalezen, nedokončen nebo výsledek chybí' }
  /api/v1/documents/jobs/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    post:
      tags: [Documents]
      summary: Požádá o zrušení běžícího jobu (worker zrušení zkontroluje mezi kroky)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  cancel_requested: { type: boolean }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/documents/tags:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Documents]
      summary: Seznam tagů dodavatele s počtem použití
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  tags:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        name: { type: string }
                        usage_count: { type: integer }
  /api/v1/documents/trash:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Documents]
      summary: Obsah koše — smazané dokumenty a smazané složky
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  documents: { type: array, items: { $ref: '#/components/schemas/Document' } }
                  folders: { type: array, items: { $ref: '#/components/schemas/DocumentFolder' } }
  /api/v1/documents/trash/empty:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Documents]
      summary: Vysype koš — trvalé smazání (dedup-aware mazání souborů z disku)
      description: |
        Smaže DB řádky smazaných dokumentů i složek a následně fyzicky odstraní
        soubory, na které už po smazání neukazuje žádný jiný dokument/příloha
        (union ref-counting `documents` + `document_files`). Scope-aware: non-admin
        vysype jen firemní a vlastní osobní doklady, cizí osobní doklady v koši zůstanou.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  deleted: { type: integer }
  /api/v1/documents/bulk:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Documents]
      summary: Hromadná akce nad dokumenty a/nebo složkami
      description: |
        `action=move` — přesun `ids`/`folder_ids` do `folder_id` (zákaz cyklu u složek).
        `action=delete` — soft-delete (do koše). `action=tag` — přiřadí `tags[]`
        k dokumentům `ids` (složky tag nepodporují). Cizí osobní doklady jsou
        scope-guardem tiše přeskočeny (nezapočítají se do `affected`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action: { type: string, enum: [move, delete, tag] }
                ids: { type: array, items: { type: integer } }
                folder_ids: { type: array, items: { type: integer } }
                folder_id: { type: [integer, "null"], description: 'Jen action=move' }
                tags: { type: array, items: { type: string }, description: 'Jen action=tag' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  affected: { type: integer }
        '400': { description: 'Nebyly vybrány žádné položky / neznámá akce' }
        '404': { description: 'Cílová složka nenalezena (action=move)' }
  /api/v1/documents/bulk-download:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: ids
        required: true
        schema: { type: string }
        description: 'CSV seznam ID dokumentů, např. "1,2,3"'
    get:
      tags: [Documents]
      summary: Stáhne vybrané dokumenty jako jeden ZIP (dokumenty.zip)
      description: Názvy v ZIPu jsou deduplikovány (`-2`, `-3`, …) při shodě `original_name`.
      responses:
        '200':
          description: ZIP archiv
          content:
            application/zip:
              schema: { type: string, format: binary }
        '400': { description: 'Nebyly vybrány žádné dokumenty' }
        '404': { description: 'Žádný soubor k zabalení' }
        '500': { description: 'ZIP není na serveru dostupné / vytvoření selhalo' }
  /api/v1/documents/{id}/move:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    post:
      tags: [Documents]
      summary: Přesun dokumentu do jiné složky
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                folder_id: { type: [integer, "null"], description: 'NULL = přesun do kořene' }
      responses:
        '200': { description: ok }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/documents/{id}/restore:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    post:
      tags: [Documents]
      summary: Obnovení dokumentu z koše
      responses:
        '200': { description: ok }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/documents/{id}/preview:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    get:
      tags: [Documents]
      summary: Inline náhled souboru (jen PDF/rastrové obrázky, jinak attachment fallback)
      description: |
        Bezpečné servírování: inline `Content-Disposition` jen pro `pdf`/`image`
        doc_type s whitelistovaným MIME (PDF, JPEG, PNG, GIF, WEBP); ostatní typy
        (vč. HTML/SVG) se vrátí jako `attachment` s `application/octet-stream`
        (anti stored-XSS), plus `Content-Security-Policy: sandbox`.
      responses:
        '200':
          description: Binární obsah souboru
          content:
            application/pdf: { schema: { type: string, format: binary } }
            image/jpeg: { schema: { type: string, format: binary } }
            application/octet-stream: { schema: { type: string, format: binary } }
        '404': { description: 'Dokument nebo soubor na disku nenalezen' }
  /api/v1/documents/{id}/thumb:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    get:
      tags: [Documents]
      summary: Náhled dokumentu (JPEG)
      description: Vrátí `404`, dokud není náhled vygenerován (`thumb_status = generated`).
      responses:
        '200':
          description: JPEG náhled
          content:
            image/jpeg: { schema: { type: string, format: binary } }
        '404': { description: 'Náhled není k dispozici / nenalezen na disku' }
  /api/v1/documents/{id}/files:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
    get:
      tags: [Documents]
      summary: Seznam souborů dokumentu (primary + přílohy)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        document_id: { type: integer }
                        role: { type: string, enum: [primary, attachment] }
                        original_name: { type: string }
                        mime_type: { type: string }
                        size_bytes: { type: integer }
                        doc_type: { type: string, enum: [pdf, docx, xlsx, xml, zfo, p7s, zip, image, other] }
                        sort_order: { type: integer }
                        uploaded_by: { type: [integer, "null"] }
                        created_at: { type: string, format: date-time }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Documents]
      summary: Přidá přílohu/přílohy k dokumentu (role vždy attachment)
      description: |
        Multipart upload jednoho i více souborů (`file`/`file[]`, max 100 na request).
        Bajty jdou přes hardened storage (MIME z obsahu, blocklist, size cap, dedup).
        Shodný obsah (stejné `sha256`) už u dokumentu → daný soubor se přeskočí
        s `reason: duplicate` v `errors[]`, ostatní se zpracují dál.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                'file[]': { type: array, items: { type: string, format: binary } }
      responses:
        '200':
          description: 'Výsledek (i částečný — viz errors[])'
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        document_id: { type: integer }
                        role: { type: string, enum: [primary, attachment] }
                        original_name: { type: string }
                        mime_type: { type: string }
                        size_bytes: { type: integer }
                        doc_type: { type: string, enum: [pdf, docx, xlsx, xml, zfo, p7s, zip, image, other] }
                        sort_order: { type: integer }
                        uploaded_by: { type: [integer, "null"] }
                        created_at: { type: string, format: date-time }
                  errors: { type: array, items: { type: object, properties: { name: { type: string }, reason: { type: string } } } }
        '400': { description: 'Žádný soubor odeslán / žádný se nepodařilo přidat (viz errors[])' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { description: 'Příliš mnoho souborů najednou (> 100)' }
  /api/v1/documents/{id}/files/{fileId}/download:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
      - in: path
        name: fileId
        required: true
        schema: { type: integer }
    get:
      tags: [Documents]
      summary: Stažení konkrétního souboru dokumentu (vždy attachment + nosniff)
      responses:
        '200':
          description: Binární obsah souboru
          content:
            application/octet-stream: { schema: { type: string, format: binary } }
        '404': { description: 'Dokument, soubor nebo bajty na disku nenalezeny' }
  /api/v1/documents/{id}/files/{fileId}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer }
      - in: path
        name: fileId
        required: true
        schema: { type: integer }
    patch:
      tags: [Documents]
      summary: Nastaví soubor jako primární a/nebo změní pořadí
      description: |
        `role=primary` atomicky degraduje stávající primary a povýší cíl (právě
        jeden primary na dokument). `role=attachment` na aktuálně primárním souboru
        je odmítnuto — dokument musí mít vždy primární soubor.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                role: { type: string, enum: [primary, attachment] }
                sort_order: { type: integer }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        document_id: { type: integer }
                        role: { type: string, enum: [primary, attachment] }
                        original_name: { type: string }
                        mime_type: { type: string }
                        size_bytes: { type: integer }
                        doc_type: { type: string, enum: [pdf, docx, xlsx, xml, zfo, p7s, zip, image, other] }
                        sort_order: { type: integer }
                        uploaded_by: { type: [integer, "null"] }
                        created_at: { type: string, format: date-time }
        '400': { description: 'Neplatná role' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Nelze degradovat/smazat poslední primární soubor' }
    delete:
      tags: [Documents]
      summary: Odebere soubor od dokumentu (nelze poslední primary; orphan-aware mazání bajtů)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        document_id: { type: integer }
                        role: { type: string, enum: [primary, attachment] }
                        original_name: { type: string }
                        mime_type: { type: string }
                        size_bytes: { type: integer }
                        doc_type: { type: string, enum: [pdf, docx, xlsx, xml, zfo, p7s, zip, image, other] }
                        sort_order: { type: integer }
                        uploaded_by: { type: [integer, "null"] }
                        created_at: { type: string, format: date-time }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Nelze smazat poslední primární soubor' }
  /api/v1/accounting/journal/{id}/description:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    patch:
      tags: [Accounting]
      summary: Upraví popis účetního zápisu (§35)
      description: |
        Inline editace popisu zápisu v deníku. Audit before/after se zapisuje ATOMICKY
        v téže transakci (§35 requirement). Vyžaduje roli účetní/admin (requireWrite).
        `description: null` nebo prázdný řetězec popis smaže.

        Optimistická konkurence: pošli aktuální `row_version` v hlavičce `If-Match`
        (hodnota z ETag u GET). Při neshodě 409 `version_conflict` (mezitím editoval
        jiný uživatel). Vynechání If-Match i body `row_version` = bez CAS (zpětně kompat.).
      parameters:
        - in: header
          name: If-Match
          required: false
          description: |
            Očekávaná verze zápisu (row_version) pro optimistickou konkurenci; při neshodě
            409 version_conflict. Formát ETag validátoru (`"5"`). Vynechání = bez CAS.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [description]
              properties:
                description: { type: [string, "null"], maxLength: 255 }
                row_version:
                  type: integer
                  description: Fallback k If-Match, pokud klient neumí posílat hlavičku.
      responses:
        '200':
          description: OK
          headers:
            ETag:
              description: Aktuální row_version zápisu jako validátor pro další If-Match.
              schema: { type: string }
        '400':
          description: invalid_if_match — hlavička If-Match není platný validátor (row_version)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: version_conflict — zápis mezitím změnil jiný uživatel
          headers:
            ETag:
              description: Aktuální row_version zápisu (pro resync klienta).
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/accounting/journal/{id}/attachments:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Accounting]
      summary: Seznam příloh účetního zápisu (§33a)
      description: Vrací `items` (seznam příloh) a `total_size` (součet bajtů příloh zápisu). Čtení (readonly+).
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Accounting]
      summary: Nahraje jednu nebo více příloh k účetnímu zápisu
      description: |
        Multipart upload, pole `file` nebo `file[]` (víc souborů najednou). MIME se
        detekuje z OBSAHU (magic-byte + finfo, klient-side Content-Type se ignoruje) +
        blocklist nebezpečných přípon/MIME. Content-addressed SHA-256 s dedupem —
        stejný obsah u téhož zápisu vrací `duplicate` (409). Limity: max 20 MiB na
        soubor, strop na celkovou velikost příloh zápisu.

        Dávkový upload nesmí spadnout kvůli jedné vadné příloze — odpověď obsahuje
        `results` (created/error per soubor). Pokud NIC nevzniklo a nastala chyba,
        vrací se klasický chybový status (409 duplicate, 413 file_too_large, …) —
        REST parita pro single-file upload. Vyžaduje roli účetní/admin.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Jeden soubor, nebo pole souborů (file[]).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  created: { type: array, items: { type: integer }, description: ID úspěšně vytvořených příloh. }
                  results:
                    type: array
                    description: Per-soubor výsledek (status created/error, original_name, error, message).
                    items: { type: object }
                  items: { type: array, description: Aktuální seznam příloh zápisu po uploadu. }
                  total_size: { type: integer, description: Součet bajtů příloh zápisu po uploadu. }
        '400': { description: "Žádný/vadný soubor (no_file, validation_failed)" }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Stejný obsah (sha256) už je u zápisu evidován (duplicate)' }
        '413': { description: Soubor nebo součet příloh zápisu přesahuje limit }
  /api/v1/accounting/journal/{id}/attachments/{attId}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: path
        name: attId
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Accounting]
      summary: Stáhne přílohu účetního zápisu
      description: |
        Vždy `Content-Disposition: attachment` (nikdy inline render) + `X-Content-Type-Options: nosniff`.
        Cesta k souboru prochází realpath-inside-base kontrolou před streamem (path traversal guard).
      responses:
        '200':
          description: Binární obsah přílohy
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Accounting]
      summary: Smaže přílohu účetního zápisu
      description: |
        Odstraní DB řádek; bajt na disku se smaže jen pokud je orphan (dedup-aware,
        počítá se jen v rámci journal_entry_attachments, nekříží se s DMS). requireWrite.
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/accounting/journal/{id}/attachments/{attId}/description:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
      - in: path
        name: attId
        required: true
        schema: { type: integer, format: int64 }
    patch:
      tags: [Accounting]
      summary: Upraví popisek přílohy zápisu
      description: 'Neměnný before/after audit log. requireWrite = účetní/admin. `description: null`/prázdný řetězec popis smaže (max 255 znaků).'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [description]
              properties:
                description: { type: [string, "null"], maxLength: 255 }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/bank-statements/scan:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Bank]
      summary: Naskenuje nakonfigurovaný adresář a naimportuje nové bankovní výpisy
      description: |
        Pouze role admin. Adresář se čte z `cfg.bank_import.scan_root` — pokud není
        nastaven nebo neexistuje, vrací `config_missing` (400). Používá se místo
        ručního uploadu, když banka ukládá výpisy na sdílený disk.
      responses:
        '200': { description: 'OK — souhrn naskenovaných/naimportovaných souborů' }
        '400': { description: cfg.bank_import.scan_root není nastaveno nebo adresář neexistuje }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/v1/bank-statements/account-balances:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Bank]
      summary: Přehled zůstatků na bankovních účtech dodavatele
      description: |
        Pro každý měnový účet (currencies) vrací aktuální zůstatek, měsíční vývoj
        (carry-forward) a přepočet do CZK kurzem ČNB ke konci každého měsíce
        (`exchange_rates` cache). Zdroj zůstatku: GPC výpisy (`source=gpc`,
        autoritativní) nebo e-mailová avíza (`bank_transactions.balance`) — novější
        datum vyhrává, při shodě GPC. Měny bez dostupného kurzu jsou v `missing_rates`.
        Grafy jsou omezené na posledních 36 měsíců.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  base_currency: { type: string, example: CZK }
                  accounts:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        code: { type: string }
                        label: { type: string }
                        account_number: { type: string }
                        bank_code: { type: [string, "null"] }
                        is_default: { type: boolean }
                        current_balance: { type: number }
                        current_balance_czk: { type: [number, "null"] }
                        statement_date: { type: [string, "null"], format: date }
                        current_source: { type: string, enum: [gpc, email_notice] }
                        statement_count: { type: integer }
                        months:
                          type: array
                          items:
                            type: object
                            properties:
                              month: { type: string, example: '2026-06' }
                              balance: { type: [number, "null"] }
                  total_czk:
                    type: object
                    properties:
                      current: { type: number }
                      months:
                        type: array
                        items:
                          type: object
                          properties:
                            month: { type: string }
                            balance_czk: { type: [number, "null"] }
                  missing_rates:
                    type: array
                    items: { type: string }
                    description: Kódy měn bez dostupného kurzu (chybí v přepočtu do CZK).
  /api/v1/bank-statements/{id}/pdf:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Přiloží k výpisu oficiální PDF z banky
      description: |
        Multipart upload (max 10 MiB). Validace: přípona `.pdf`, magic bytes `%PDF-`,
        MIME `application/pdf`. Ukládá se jako MEDIUMBLOB do `bank_statements.pdf_content`.
        Avízo-výpis (`source=email_notice`, virtuální, složený z e-mailových avíz) PDF
        nemá — vrací `unsupported` (400). Role admin nebo účetní.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary, description: 'PDF výpis (max 10 MiB)' }
      responses:
        '200': { description: OK }
        '400': { description: 'Soubor chybí, není PDF, nebo je avízo-výpis (unsupported)' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { description: Soubor je příliš velký (> 10 MiB) }
    get:
      tags: [Bank]
      summary: Stáhne přiložené PDF výpisu
      description: 'Číslo účtu se prepend­uje do názvu souboru, pokud v něm ještě není (odlišení výpisů z různých účtů).'
      responses:
        '200':
          description: PDF stream
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '404': { description: "K výpisu není nahrané PDF, nebo výpis nenalezen" }
    delete:
      tags: [Bank]
      summary: Smaže přiložené PDF výpisu
      description: GPC data i transakce zůstávají zachované, maže se jen PDF. Role admin nebo účetní.
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/bank-transactions/{id}/match-candidates:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Bank]
      summary: Navrhne faktury ke spárování s bankovní transakcí
      description: |
        Použije se, když transakce nemá VS nebo VS nesedí. Prohledává vystavené i
        přijaté faktury (± 14 dní kolem data splatnosti/vystavení), znaménko se
        nefiltruje (dobropisy). Cizoměnová faktura placená z CZK účtu se porovnává
        přes kurz faktury s relativní tolerancí (4 %). Zahrnuje i zaplacené faktury
        (druhá platba/doplatek). Vrací max 25 kandidátů, nejlepší shoda první.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates:
                    type: array
                    items:
                      type: object
                      properties:
                        type: { type: string, enum: [invoice, purchase_invoice] }
                        id: { type: integer }
                        ref: { type: [string, "null"] }
                        amount: { type: number }
                        currency: { type: string }
                        converted_amount: { type: [number, "null"] }
                        converted_currency: { type: [string, "null"] }
                        issue_date: { type: string, format: date }
                        due_date: { type: [string, "null"], format: date }
                        party: { type: [string, "null"] }
                        paid: { type: boolean }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/bank-transactions/{id}/split-suggestions:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Bank]
      summary: Navrhne sloučenou úhradu — jedna platba pokrývající více vystavených faktur
      description: |
        Použije se, když klient zaplatil 2+ faktur jednou platbou a VS nesedí (jen
        příchozí platby). Kombinace se hledají výhradně v rámci jednoho klienta;
        default okno ± 7 dní kolem data platby (rozšiřitelné parametrem `window`).
        Volitelná kotva `invoice_id` — uživatel už jednu fakturu vybral a částka
        nesedí, dohledají se další faktury téhož klienta.
      parameters:
        - in: query
          name: invoice_id
          schema: { type: integer }
          description: Kotva — omezí návrh na klienta této již vybrané faktury.
        - in: query
          name: window
          schema: { type: integer, default: 7, minimum: 1, maximum: 60 }
          description: Okno ve dnech kolem data platby (výchozí 7, max 60).
        - in: query
          name: max
          schema: { type: integer, default: 6, minimum: 2, maximum: 6 }
          description: Max počet faktur v jedné navržené kombinaci.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  suggestions:
                    type: array
                    items:
                      type: object
                      properties:
                        client_id: { type: integer }
                        client_name: { type: [string, "null"] }
                        total: { type: number }
                        currency: { type: string }
                        count: { type: integer }
                        invoices: { type: array, items: { type: object } }
                  window: { type: integer }
                  max: { type: integer }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/bank-transactions/{id}/create-purchase-invoice:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Bank]
      summary: Založí koncept přijaté faktury z odchozí bankovní transakce
      description: |
        Jen pro ODCHOZÍ (zápornou) platbu — jinak `not_outgoing` (400). Dodavatel se
        NEzakládá automaticky, musí existovat a být vybraný přes `vendor_id` (uživatel
        ho vybere ve VendorPickeru, případně přes tlačítko „nový dodavatel"). Předvyplní
        fakturu (částka, datum, VS, měna, popis) jako DRAFT s jednou položkou v sazbě
        0 % — uživatel po nahrání PDF doplní skutečný rozpad DPH. `vendor_invoice_number`
        se nastaví na placeholder `BANK-{txId}` (VS jde do `varsymbol`, nekoliduje s
        unikátním klíčem). Zápis se spáruje s transakcí (`payment_matches`, typ manual),
        faktura ale NENÍ označena jako paid. Opakované volání na téže transakci vrací
        `already_exists` (409) s ID existujícího konceptu. Role admin nebo účetní.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [vendor_id]
              properties:
                vendor_id: { type: integer, description: 'ID dodavatele (clients.id, musí patřit tenantovi).' }
      responses:
        '201':
          description: Koncept vytvořen
          content:
            application/json:
              schema:
                type: object
                properties:
                  purchase_invoice_id: { type: integer }
                  vendor_id: { type: integer }
                  currency: { type: string }
        '400': { description: 'Chybí/neplatný vendor_id, nebo transakce není odchozí (not_outgoing)' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Z této transakce už koncept přijaté faktury existuje (already_exists)' }
  /api/v1/bank-transactions/{id}/document-request:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    post:
      tags: [Bank]
      summary: Založí požadavek na doklad z bankovní transakce (Fáze F)
      description: Výchozí popis/částka/datum se předvyplní z transakce.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                description: { type: string, maxLength: 500 }
                deadline: { type: string, format: date }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/invoices/{id}/stock-documents:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Invoices, Stock]
      summary: Skladové doklady vázané na vydanou fakturu
      description: Vrací výdejky/doklady vytvořené k této faktuře (např. výdej ze skladu při fakturaci). 400, pokud sklad není u dodavatele zapnutý.
      responses:
        '200': { description: OK }
        '400': { description: Skladový modul není u tohoto dodavatele zapnutý }
  /api/v1/invoices/{id}/book:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Invoices]
      summary: Ručně označí vydanou fakturu jako zaúčtovanou
      description: |
        Epic F6 (§4.6) — pro tax_evidence firmy bez journalu/období je `booked_at`
        jediný mechanismus zámku pro roli client. Idempotentní — existující
        `booked_at`/`booked_by` se nepřepisuje. Koncept (`status=draft`) nelze
        zaúčtovat (`not_draft`, 409).
      responses:
        '200': { description: 'OK — vrací fakturu vč. stavu zámku' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Koncept nelze označit jako zaúčtovaný (not_draft)' }
    delete:
      tags: [Invoices]
      summary: Zruší ruční zaúčtování vydané faktury
      description: |
        Smaže `booked_at`/`booked_by`. Pokud má doklad aktivní (nestornovaný) posted
        zápis v deníku, vrací `still_posted` (409) — nejdřív je nutné zápis stornovat.
      responses:
        '200': { description: 'OK — vrací fakturu vč. stavu zámku' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Doklad má aktivní účetní zápis — nejdřív stornovat v deníku (still_posted)' }
  /api/v1/purchase-invoices/{id}/source:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Purchase invoices]
      summary: Stáhne archivovaný strojově čitelný zdrojový doklad (ISDOC/ISDOCX/Pohoda XML/…)
      description: |
        Vrací bajty AS-IS (`.isdocx` se nerozbaluje). Soubor leží mimo webroot, cesta
        prochází realpath-inside-archive-root kontrolou (path traversal guard). Vždy
        `Content-Disposition: attachment`. 404, pokud faktura nemá archivovaný zdroj.
      responses:
        '200':
          description: Binární obsah zdrojového dokladu
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '404': { description: 'K faktuře není archivovaný zdroj, nebo faktura nenalezena' }
  /api/v1/purchase-invoices/{id}/documents:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Purchase invoices]
      summary: Seznam DMS dokumentů provázaných s přijatou fakturou
      description: |
        Epic F7 (§6) — vazba přes `document_links(entity_type='purchase_invoice')`,
        nezávislá na fixních `pdf_`/`source_` sloupcích PF. Výsledek prochází DMS
        scope guardem (user-scoped doklady cizích uživatelů se nevrací).
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Purchase invoices]
      summary: Přivěsí existující DMS dokument k přijaté faktuře
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [document_id]
              properties:
                document_id: { type: integer }
      responses:
        '200': { description: 'OK — vrací aktuální seznam provázaných dokumentů' }
        '400': { description: Chybí document_id }
        '404': { description: 'Faktura nebo dokument nenalezen (mimo DMS scope viewera)' }
    delete:
      tags: [Purchase invoices]
      summary: Odpojí DMS dokument od přijaté faktury
      description: '`document_id` se čte z těla požadavku, případně z query parametru.'
      parameters:
        - in: query
          name: document_id
          schema: { type: integer }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                document_id: { type: integer }
      responses:
        '200': { description: 'OK — vrací aktuální seznam provázaných dokumentů' }
        '404': { description: 'Faktura nebo dokument nenalezen (mimo DMS scope viewera)' }
  /api/v1/purchase-invoices/{id}/stock-receipt:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Purchase invoices, Stock]
      summary: Návrh příjemky ke skladovým položkám přijaté faktury
      description: Vrací, co zbývá přijmout na sklad, navrhované pořizovací ceny a kandidáty vedlejších nákladů. 400, pokud sklad není u dodavatele zapnutý.
      responses:
        '200': { description: OK }
        '400': { description: Skladový modul není u tohoto dodavatele zapnutý }
    post:
      tags: [Purchase invoices, Stock]
      summary: Založí skladovou příjemku (DRAFT) z přijaté faktury
      description: Vytvoří skladový doklad typu příjemka navázaný na fakturu. Tělo odpovídá návrhu z GET (položky/množství/ceny), lze upravit. requireWrite.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Položky příjemky (podle návrhu z GET stock-receipt, upravitelné).
      responses:
        '201': { description: Příjemka vytvořena }
        '400': { description: Skladový modul není u tohoto dodavatele zapnutý }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/purchase-invoices/{id}/stock-receipts:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Purchase invoices, Stock]
      summary: Přehled skladových příjmů vytvořených z této přijaté faktury
      responses:
        '200': { description: OK }
        '400': { description: Skladový modul není u tohoto dodavatele zapnutý }
  /api/v1/logbook/trip-categories/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    put:
      tags: [Logbook]
      summary: Upraví kategorii cesty
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, label]
              properties:
                code: { type: string }
                label: { type: string }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Kód už existuje' }
        '422':
          $ref: '#/components/responses/ValidationFailed'
    delete:
      tags: [Logbook]
      summary: Smaže kategorii cesty
      description: |
        Selže s 409, pokud jsou na kategorii navázané jízdy — v takovém případě
        ji lze místo smazání archivovat přes PUT.
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Kategorie má navázané jízdy' }
  /api/v1/logbook/trips/import:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Logbook]
      summary: Import knihy jízd z CSV/XLSX
      description: |
        Multipart upload souboru (max 10 MB). Vrací report o počtu vytvořených
        a neúspěšných řádků.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        '200': { description: 'Report importu (created/failed)' }
        '400': { description: 'Chybí soubor nebo import selhal' }
        '413': { description: 'Soubor příliš velký (max 10 MB)' }
  /api/v1/logbook/trips/export:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: format
        schema: { type: string, enum: [xlsx, pdf], default: xlsx }
      - in: query
        name: date_from
        schema: { type: string, format: date }
      - in: query
        name: date_to
        schema: { type: string, format: date }
      - in: query
        name: car_id
        schema: { type: integer }
      - in: query
        name: category_id
        schema: { type: integer }
    get:
      tags: [Logbook]
      summary: Export knihy jízd (XLSX/PDF)
      description: |
        Binární download souboru za zvolené období/filtry.
      responses:
        '200':
          description: Soubor exportu
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
            application/pdf:
              schema: { type: string, format: binary }
        '400': { description: 'Chybí supplier kontext nebo neplatný format' }
  /api/v1/logbook/trips/purposes:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Logbook]
      summary: Našeptávač účelů cest
      description: Distinct dříve zadané účely cest daného tenanta.
      responses:
        '200': { description: OK }
  /api/v1/logbook/trips/places:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Logbook]
      summary: Našeptávač míst cest
      description: Distinct dříve zadaná místa (odkud/kam) daného tenanta.
      responses:
        '200': { description: OK }
  /api/v1/logbook/fuelings/export:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: format
        schema: { type: string, enum: [xlsx, pdf], default: xlsx }
      - in: query
        name: date_from
        schema: { type: string, format: date }
      - in: query
        name: date_to
        schema: { type: string, format: date }
      - in: query
        name: car_id
        schema: { type: integer }
      - in: query
        name: source
        schema: { type: string }
      - in: query
        name: vendor_id
        schema: { type: integer }
    get:
      tags: [Logbook]
      summary: Export tankování (XLSX/PDF)
      description: |
        Binární download souboru za zvolené období/filtry.
      responses:
        '200':
          description: Soubor exportu
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
            application/pdf:
              schema: { type: string, format: binary }
        '400': { description: 'Chybí supplier kontext nebo neplatný format' }
  /api/v1/logbook/summary/export:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: year
        schema: { type: integer }
      - in: query
        name: format
        schema: { type: string, enum: [xlsx, pdf], default: xlsx }
    get:
      tags: [Logbook]
      summary: Export ročního souhrnu knihy jízd (XLSX/PDF)
      description: |
        Binární download souhrnu za rok (per vozidlo + totály + měsíční km).
        Bez `year` se použije nejnovější dostupný rok.
      responses:
        '200':
          description: Soubor exportu
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
            application/pdf:
              schema: { type: string, format: binary }
        '400': { description: 'Chybí supplier kontext nebo neplatný format' }
  /api/v1/logbook/fuel-invoices/backfill:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Logbook]
      summary: Zpětné dávkové vytěžení historie tankování z faktur
      description: |
        Projde historické přijaté faktury od benzínek (is_fuel_station) a vytěží
        z nich tankování. `limit` omezuje počet zpracovaných faktur na jedno volání
        (1–100, default 25).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                limit: { type: integer, minimum: 1, maximum: 100, default: 25 }
      responses:
        '200': { description: 'Report (processed/created/remaining)' }
  /api/v1/logbook/fuel-invoices:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Logbook]
      summary: Přehled faktur od benzínek
      description: |
        Nespárované faktury vrací všechny, spárované jen posledních `scanned_limit`
        (default 10, 0 = žádné). `only_unscanned=1` zobrazí jen dosud nevytěžené.
      parameters:
        - in: query
          name: year
          schema: { type: integer }
        - in: query
          name: only_unscanned
          schema: { type: boolean }
        - in: query
          name: scanned_limit
          schema: { type: integer, default: 10 }
      responses:
        '200': { description: 'Faktury + seznam aut tenanta + has_cars' }
  /api/v1/logbook/fuel-invoices/{id}/items:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Logbook]
      summary: Položky faktury od benzínky (detail pro vytěžení)
      description: |
        Detail položek faktury s příznakem `is_fuel` (rozpoznání paliva dle klíčových slov).
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/logbook/fuel-invoices/{id}/assign:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Logbook]
      summary: Vytěží fakturu a přiřadí ji autu
      description: |
        Vytěží položky faktury (i opakovaně — force režim doplní dříve chybějící
        litry) a přiřadí vzniklá tankování zvolenému autu. Reassign přepíše auto
        u všech tankování navázaných na tuto fakturu. `car_id: null` = zruší přiřazení.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                car_id: { type: [integer, "null"] }
      responses:
        '200': { description: 'Výsledek vytěžení + reassigned' }
        '400': { description: 'Vytěžení selhalo' }
        '404': { description: 'Auto neexistuje' }
  /api/v1/tax-evidence/cash-journal:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: year
        schema: { type: integer }
      - in: query
        name: from
        schema: { type: string, format: date }
      - in: query
        name: to
        schema: { type: string, format: date }
    get:
      tags: [Tax]
      summary: Peněžní deník daňové evidence (§7b, kasová báze)
      description: |
        Read-only report. Období zadej buď `year` (celý rok), nebo `from`+`to`
        (YYYY-MM-DD). Bez parametrů = aktuální rok. Vrací deník + totály +
        checks{} + warnings[].
      responses:
        '200': { description: OK }
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /api/v1/tax-evidence/cash-journal/export:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: year
        schema: { type: integer }
      - in: query
        name: from
        schema: { type: string, format: date }
      - in: query
        name: to
        schema: { type: string, format: date }
      - in: query
        name: format
        required: true
        schema: { type: string, enum: [pdf, xlsx] }
    get:
      tags: [Tax]
      summary: Export peněžního deníku (PDF/XLSX)
      responses:
        '200':
          description: Soubor exportu
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /api/v1/tax-evidence/receivables-payables:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Tax]
      summary: Pohledávky a závazky daňové evidence (aging)
      description: |
        Read-only report — aging per měna + KPI (DSO/DPO/punktualita). Reuse
        CRM agregací, bez vlastních parametrů (aktuální stav).
      responses:
        '200': { description: OK }
  /api/v1/tax-evidence/receivables-payables/export:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: format
        required: true
        schema: { type: string, enum: [pdf, xlsx] }
    get:
      tags: [Tax]
      summary: Export pohledávek a závazků (PDF/XLSX)
      responses:
        '200':
          description: Soubor exportu
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /api/v1/tax-evidence/classification:
    post:
      tags: [Tax]
      summary: Ruční klasifikační override pohybu peněžního deníku (G2)
      description: Účetní/admin. Předvyplněnou automatickou klasifikaci lze ručně přebít.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [source_type, source_id, tax_bucket]
              properties:
                source_type: { type: string, enum: [bank, cash] }
                source_id: { type: integer }
                tax_bucket: { type: string }
                note: { type: [string, "null"], maxLength: 255 }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/tax-evidence/classification/{source_type}/{source_id}:
    delete:
      tags: [Tax]
      summary: Smaže klasifikační override (vrátí pohyb k automatické klasifikaci)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: source_type, required: true, schema: { type: string, enum: [bank, cash] } }
        - { in: path, name: source_id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/tax-evidence/transition-report:
    get:
      tags: [Tax]
      summary: Podklady přechodového můstku §7b→§24 ZDP (příloha č. 3, G7)
      description: Read-only report.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: as_of, schema: { type: string, format: date } }
        - { in: query, name: direction, schema: { type: string, enum: [tax_to_accounting, accounting_to_tax], default: tax_to_accounting } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { description: Přechod se na firmu nevztahuje, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/tax-evidence/closing/{year}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: year, required: true, schema: { type: integer, minimum: 2018 } }
    get:
      tags: [Tax]
      summary: Roční uzávěrka daňové evidence podle § 7b ZDP
      description: Vrátí nebo založí rozpracovanou uzávěrku včetně checklistu, stavů a nepeněžních úprav.
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TaxEvidenceAnnualClosing' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
    put:
      tags: [Tax]
      summary: Uloží rozpracovanou roční uzávěrku daňové evidence
      description: Účetní/admin. `row_version` chrání proti souběžné úpravě.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TaxEvidenceAnnualClosingWrite' }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TaxEvidenceAnnualClosing' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/tax-evidence/closing/{year}/finalize:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: year, required: true, schema: { type: integer, minimum: 2018 } }
    post:
      tags: [Tax]
      summary: Finalizuje roční uzávěrku a uloží neměnný zdrojový snapshot
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [row_version], properties: { row_version: { type: integer, minimum: 1 } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TaxEvidenceAnnualClosing' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/tax-evidence/closing/{year}/reopen:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: year, required: true, schema: { type: integer, minimum: 2018 } }
    post:
      tags: [Tax]
      summary: Vrátí finální uzávěrku daňové evidence do rozpracovaného stavu
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [row_version], properties: { row_version: { type: integer, minimum: 1 } } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TaxEvidenceAnnualClosing' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /api/v1/reports/monthly-export/preview:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: query
        name: period
        schema: { type: string, enum: [monthly, quarterly] }
      - in: query
        name: year
        schema: { type: integer }
      - in: query
        name: month
        schema: { type: string, description: 'Číslo měsíce (1-12) nebo YYYY-MM' }
      - in: query
        name: quarter
        schema: { type: integer, minimum: 1, maximum: 4 }
    get:
      tags: [Reports]
      summary: Náhled měsíčního/kvartálního exportu — počty položek
      description: |
        Bez validního `period` se `month` bere jako legacy zápis YYYY-MM.
        Přístup jen role admin/accountant/readonly.
      responses:
        '200': { description: 'Počty per část (sales_pdf, sales_isdoc, purchase_pdf, purchase_isdoc, bank_pdf, bank_gpc, dph_book)' }
        '400': { description: 'Neplatné parametry období' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/v1/reports/monthly-export/start:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Spustí měsíční/kvartální export (background job)
      description: |
        Vytvoří job (import_jobs, source=monthly_export) a spustí worker na
        pozadí (ZIP se sales/purchase PDF+ISDOC, bankovními výpisy PDF+GPC a
        Knihou DPH). Vrátí 409, pokud už export běží. Období se čte z těla,
        přebíjí query (`period`/`year`/`month`/`quarter`).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                period: { type: string, enum: [monthly, quarterly] }
                year: { type: integer }
                month: { type: string }
                quarter: { type: integer, minimum: 1, maximum: 4 }
                parts:
                  type: array
                  items:
                    type: string
                    enum: [sales_pdf, sales_isdoc, purchase_pdf, purchase_isdoc, bank_pdf, bank_gpc, dph_book]
                  description: 'Prázdné/chybí = všechny části'
      responses:
        '201': { description: 'job_id, status=queued, params' }
        '400': { description: 'Chybí supplier kontext nebo neplatné období' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: 'Export už běží (existing_job_id)' }
  /api/v1/reports/monthly-export/jobs:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Reports]
      summary: Historie jobů měsíčního exportu
      description: Posledních 15 jobů (zůstávají ke stažení dokud nejsou uklizené).
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/v1/reports/monthly-export/jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Reports]
      summary: Stav jobu měsíčního exportu (polling)
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Reports]
      summary: Smaže job měsíčního exportu i jeho soubor
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/reports/monthly-export/jobs/{id}/download:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [Reports]
      summary: Stáhne hotový ZIP měsíčního exportu
      responses:
        '200':
          description: ZIP archiv exportu
          content:
            application/zip:
              schema: { type: string, format: binary }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Export ještě není připravený ke stažení' }
        '410': { description: 'Soubor exportu už není k dispozici' }
  /api/v1/reports/monthly-export/jobs/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - in: path
        name: id
        required: true
        schema: { type: integer, format: int64 }
    post:
      tags: [Reports]
      summary: Zruší běžící/čekající job měsíčního exportu
      responses:
        '200': { description: 'ok, cancel_requested' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/reports/submissions/{id}/submit:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer } }
    post:
      tags: [Reports]
      summary: Zaznamená podání archivovaného EPO XML jako prokazatelně podané
      description: Zkušební asistovaný pokus nelze touto operací potvrdit jako právně účinné podání.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                submitted_at: { type: string, description: "ISO datum/čas nebo YYYY-MM-DD, default teď" }
                submission_ref: { type: [string, "null"] }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Poslední potvrditelný asistovaný pokus patří do zkušebního prostředí EPO }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  # ── Uzávěrkový balíček (ZIP se všemi sestavami uzávěrky) ────────────────────
  /api/v1/reports/closing-package/preview:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: query, name: period_id, required: true, schema: { type: integer } }
    get:
      tags: [Reports]
      summary: Náhled uzávěrkového balíčku — počty dostupných sestav
      description: Jen podvojné účetnictví (guard v Action).
      responses:
        '200': { description: "OK {period_id, fiscal_year, counts}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/reports/closing-package/start:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    post:
      tags: [Reports]
      summary: Spustí uzávěrkový balíček (background job)
      description: Vytvoří job (import_jobs) a spustí worker na pozadí. Vrátí 409, pokud už běží.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [period_id]
              properties:
                period_id: { type: integer }
                parts: { type: array, items: { type: string } }
                include_xlsx: { type: boolean, default: false }
      responses:
        '201': { description: "job_id, status=queued, params" }
        '400': { description: Neplatné parametry }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Export už běží (existing_job_id) }
  /api/v1/reports/closing-package/jobs:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Reports]
      summary: Historie jobů uzávěrkového balíčku
      description: Posledních 15 jobů.
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/v1/reports/closing-package/jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [Reports]
      summary: Stav jobu uzávěrkového balíčku (polling)
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Reports]
      summary: Smaže job uzávěrkového balíčku i jeho soubor
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/reports/closing-package/jobs/{id}/download:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [Reports]
      summary: Stáhne hotový ZIP uzávěrkového balíčku
      responses:
        '200':
          description: ZIP archiv
          content:
            application/zip:
              schema: { type: string, format: binary }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Balíček ještě není připravený ke stažení' }
        '410': { description: 'Soubor balíčku už není k dispozici' }
  /api/v1/reports/closing-package/jobs/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: id, required: true, schema: { type: integer, format: int64 } }
    post:
      tags: [Reports]
      summary: Zruší běžící/čekající job uzávěrkového balíčku
      responses:
        '200': { description: 'ok, cancel_requested' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/portal/summary:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [Portal]
      summary: Agregovaný přehled hospodaření firmy (klientský portál)
      description: |
        Epic F6 — živě agregovaný přehled přes CrmAggregationService (žádný
        cache). Bezpečnostní invariant: response nikdy neobsahuje jména
        klientů/odběratelů ani čísla dokladů, jen agregáty (KPI per období,
        měsíční historie, cashflow aging + forecast, stav DPH a termíny).
      responses:
        '200': { description: 'company, kpi, monthly, cashflow, vat' }
        '403': { description: 'Žádná firma není dostupná' }
        '404': { description: 'Firma nenalezena' }

  /api/v1/document-requests:
    get:
      tags: [DocumentRequests]
      summary: Výčet požadavků na chybějící doklady (účetní pohled)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: query, name: status, schema: { type: string }, description: "Čárkou oddělené stavy, např. requested,uploaded,resolved" }
      responses:
        '200': { description: "OK {items}", content: { application/json: { schema: { type: object } } } }
    post:
      tags: [DocumentRequests]
      summary: Ruční založení požadavku na doklad
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [description]
              properties:
                description: { type: string, maxLength: 500 }
                amount: { type: [number, "null"] }
                context_date: { type: [string, "null"], format: date }
                deadline: { type: [string, "null"], format: date }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/document-requests/{id}:
    get:
      tags: [DocumentRequests]
      summary: Detail požadavku na doklad
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [DocumentRequests]
      summary: Zruší požadavek na doklad
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean } } } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/document-requests/{id}/resolve:
    post:
      tags: [DocumentRequests]
      summary: Uzavře požadavek na doklad (bez uploadu — vyřešeno jinak)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/document-requests/{id}/reopen:
    post:
      tags: [DocumentRequests]
      summary: Vrátí požadavek zpět na "requested" (chybný/neúplný upload)
      description: Účetní/admin.
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/portal/document-requests:
    get:
      tags: [DocumentRequests]
      summary: Vlastní požadavky na doklady (klientský portál)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
      responses:
        '200': { description: "OK {items}", content: { application/json: { schema: { type: object } } } }
        '403': { description: Klient bez přiřazené firmy, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/portal/document-requests/{id}/upload:
    post:
      tags: [DocumentRequests]
      summary: Nahraje doklad k požadavku (reuse AI extrakce → koncept přijaté faktury)
      parameters:
        - $ref: '#/components/parameters/SupplierIdHeader'
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary, description: "PDF/obrázek, max 32 MiB" }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Chybí soubor, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '403': { description: Klient bez přiřazené firmy, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Požadavek už je vyřešený, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '413': { description: Soubor je příliš velký, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { description: Extrakce dat ze souboru selhala, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  # ── Přiznání k dani z příjmů (Epic DP) ──────────────────────────────────────
  /api/v1/tax-return/{type}/{year}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    get:
      tags: [TaxReturn]
      summary: Řádky, ruční vstupy a upozornění přiznání
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/tax-return/{type}/{year}/inputs:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    put:
      tags: [TaxReturn]
      summary: Uloží ruční vstupy přiznání (draft)
      description: Účetní/admin. `row_version` chrání proti souběžné úpravě (optimistic lock).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [inputs, row_version]
              properties:
                inputs: { type: object }
                row_version: { type: integer }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Přiznání bylo mezitím změněno (row_version nesedí), content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/finalize:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    post:
      tags: [TaxReturn]
      summary: Finalizuje přiznání (zmrazí draft)
      description: Vyžaduje oprávnění `reports.finalize`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [row_version]
              properties:
                row_version: { type: integer }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Přiznání není v konceptu / row_version nesedí, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/reopen:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    post:
      tags: [TaxReturn]
      summary: Vrátí finalizované přiznání zpět do konceptu
      description: Vyžaduje oprávnění `reports.reopen`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [row_version]
              properties:
                row_version: { type: integer }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Přiznání není finalizované / row_version nesedí, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/prefinalize-check:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    get:
      tags: [TaxReturn]
      summary: Předfinalizační kontrolní checklist (E10, audit 2026-07)
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/tax-return/{type}/{year}/xml/preview:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    get:
      tags: [TaxReturn]
      summary: Stáhne nearchivovaný pracovní náhled EPO XML DPFO
      description: Náhled lze vytvořit z draftu; není určen k podání a nemění finální snapshot ani archiv podání.
      responses:
        '200': { description: Pracovní XML DPFO, content: { application/xml: {} } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '500': { description: Build XML selhal, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/xml:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    get:
      tags: [TaxReturn]
      summary: Stáhne validované EPO XML přiznání (DPFO/DPPO)
      description: Vyžaduje oprávnění `reports.export`. Vrací hlavičku `X-Validation-Status`.
      responses:
        '200': { description: XML download, content: { application/xml: {} } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { description: Build XML selhal, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/reconcile:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    post:
      tags: [TaxReturn]
      summary: Rekonciliace proti podanému přiznání (upload EPO XML od účetní)
      description: Jen pro `type=po` (DPPO); pro `fo` vrací 400 `invalid_type`.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary, description: "Podané EPO XML (DPPDP9), max 10 MB" }
      responses:
        '200': { description: "OK {differences}", content: { application/json: { schema: { type: object } } } }
        '400': { description: "Neplatný typ (jen po) / chybí soubor" }
        '403': { $ref: '#/components/responses/Forbidden' }
        '415': { description: Nepodporovaný formát souboru }

  /api/v1/tax-return/{type}/{year}/insurance:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    get:
      tags: [TaxReturn]
      summary: Přehled pojistného a záloh OSVČ (JSON)
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/tax-return/{type}/{year}/insurance/pdf:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    get:
      tags: [TaxReturn]
      summary: Přehled OSVČ o příjmech a výdajích — PDF pro sociální pojištění
      description: Vyžaduje oprávnění `reports.export`.
      responses:
        '200': { description: Soubor PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { description: Build PDF selhal }

  /api/v1/tax-return/{type}/{year}/insurance/pdf/health:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    get:
      tags: [TaxReturn]
      summary: Přehled OSVČ pro zdravotní pojišťovnu — PDF (E11)
      description: Jen `type=fo`. Vyžaduje oprávnění `reports.export`.
      responses:
        '200': { description: Soubor PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
        '400': { description: Neplatný typ (jen fo) / rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { description: Build PDF selhal }

  /api/v1/tax-return/{type}/{year}/insurance/xml/cssz:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
    get:
      tags: [TaxReturn]
      summary: XML přehledu OSVČ pro ČSSZ (sociální pojištění)
      description: Vyžaduje oprávnění `reports.export`. Vrací hlavičku `X-Validation-Status`.
      responses:
        '200': { description: XML download, content: { application/xml: {} } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { description: Build XML selhal }

  /api/v1/tax-return/advances/upcoming:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
    get:
      tags: [TaxReturn]
      summary: Nadcházející zálohy na daň a pojistné napříč roky (E9)
      responses:
        '200': { description: "OK {schedules}", content: { application/json: { schema: { type: object } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/tax-return/{type}/{year}/advances:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: query, name: variant, schema: { type: string, enum: [radne, opravne, dodatecne], default: radne } }
      - { in: query, name: seq, schema: { type: integer, minimum: 0, default: 0 } }
    get:
      tags: [TaxReturn]
      summary: Předpisy záloh na daň a pojistné za rok
      responses:
        '200': { description: "OK {schedules}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok/variant }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/tax-return/{type}/{year}/advances/generate:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    post:
      tags: [TaxReturn]
      summary: Vygeneruje předpisy záloh z finalizovaného přiznání
      responses:
        '200': { description: "OK {generated_count}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Přiznání není finalizované / předpisy už existují, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/advances/match:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    post:
      tags: [TaxReturn]
      summary: Spáruje bankovní platby s předpisy záloh
      responses:
        '200': { description: "OK {matched_count}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Nelze spárovat (chybí předpisy), content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/advances/generate-period:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    post:
      tags: [TaxReturn]
      summary: Vygeneruje předpisy záloh pro tento rok (z draftu min. roku / rozhodnutí FÚ)
      description: "#42 — použije se, dokud není přiznání předchozího roku finalizované."
      responses:
        '200': { description: "OK {generated_count}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Předpisy pro rok už existují, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/advances/override:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    get:
      tags: [TaxReturn]
      summary: Aktivní rozhodnutí FÚ (§174) platné pro tento rok
      responses:
        '200': { description: "OK {amount, valid_from, valid_to}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
    put:
      tags: [TaxReturn]
      summary: Uloží rozhodnutí FÚ (§174) a přepočítá předpisy
      description: "#43 — ruční potvrzení rozhodnutí o výši záloh."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, valid_from, valid_to]
              properties:
                amount: { type: number }
                valid_from: { type: string, format: date }
                valid_to: { type: string, format: date }
                reason: { type: [string, "null"] }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Kolize s existujícím rozhodnutím, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    delete:
      tags: [TaxReturn]
      summary: Smaže rozhodnutí FÚ (§174), vrátí zálohy na predikci
      responses:
        '200': { description: "OK {deleted: true}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/tax-return/{type}/{year}/advances/overrides:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    get:
      tags: [TaxReturn]
      summary: Globální přehled rozhodnutí FÚ (§174) napříč roky
      description: "#46 — id-based CRUD s rozsahem OD-DO, globální tabulka nezávislá na `year` v cestě."
      responses:
        '200': { description: "OK {overrides}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [TaxReturn]
      summary: Vytvoří rozhodnutí FÚ (§174) s rozsahem OD-DO
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [valid_from, valid_to, amount]
              properties:
                valid_from: { type: string, format: date }
                valid_to: { type: string, format: date }
                amount: { type: number }
                reason: { type: [string, "null"] }
      responses:
        '201': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/tax-return/{type}/{year}/advances/overrides/{overrideId}:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: path, name: overrideId, required: true, schema: { type: integer } }
    put:
      tags: [TaxReturn]
      summary: Upraví rozhodnutí FÚ (§174) podle ID
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                valid_from: { type: string, format: date }
                valid_to: { type: string, format: date }
                amount: { type: number }
                reason: { type: [string, "null"] }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [TaxReturn]
      summary: Smaže rozhodnutí FÚ (§174) podle ID
      responses:
        '200': { description: "OK {deleted: true}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/tax-return/{type}/{year}/advances/confirm-all:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
    post:
      tags: [TaxReturn]
      summary: Hromadně potvrdí úhradu všech záloh roku
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                kind: { type: [string, "null"], description: "Volitelný filtr druhu zálohy (daň / pojistné)" }
      responses:
        '200': { description: "OK {confirmed_count}", content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Některé zálohy nelze potvrdit, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/advances/{scheduleId}/amount:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: path, name: scheduleId, required: true, schema: { type: integer } }
    post:
      tags: [TaxReturn]
      summary: Ručně upraví předepsanou výši zálohy (jen nezaplacené)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: number }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Záloha je už potvrzená, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/advances/{scheduleId}/confirm:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: path, name: scheduleId, required: true, schema: { type: integer } }
    post:
      tags: [TaxReturn]
      summary: Ručně potvrdí úhradu zálohy (bez párování s bankou)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount: { type: [number, "null"] }
                paid_on: { type: [string, "null"], format: date }
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Záloha je už potvrzená, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /api/v1/tax-return/{type}/{year}/advances/{scheduleId}/unconfirm:
    parameters:
      - $ref: '#/components/parameters/SupplierIdHeader'
      - { in: path, name: type, required: true, schema: { type: string, enum: [fo, po] } }
      - { in: path, name: year, required: true, schema: { type: integer } }
      - { in: path, name: scheduleId, required: true, schema: { type: integer } }
    post:
      tags: [TaxReturn]
      summary: Vrátí potvrzenou zálohu zpět do stavu "planned"
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object } } } }
        '400': { description: Neplatný typ/rok }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Záloha není ve stavu confirmed, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: mi_pat_
      description: Personal Access Token získaný v Nastavení → API tokeny.

  parameters:
    SupplierIdHeader:
      in: header
      name: X-Supplier-Id
      required: false
      description: |
        ID supplier-a (firmy), v jejíž kontextu má operace běžet. Vyžadováno
        jen u **multi-supplier účtů s tokenem bez bind**. Pokud je token bound
        na konkrétního supplier-a, hlavička se ignoruje.
      schema: { type: integer }

  responses:
    Unauthorized:
      description: Neplatný nebo chybějící token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Zdroj nenalezen
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Nedostatečné oprávnění (role)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationFailed:
      description: Validace selhala
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    InsufficientScope:
      description: Token nemá dostatečný scope (např. `read` na write operaci)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Překročen rate limit
      headers:
        Retry-After:
          schema: { type: integer }
          description: Sekundy do uvolnění
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    OssPreview:
      type: object
      required: [period, settings, summary, countries, corrections, warnings]
      properties:
        period:
          type: object
          required: [year, quarter, start, end, label, submission_deadline]
          properties:
            year: { type: integer }
            quarter: { type: integer, minimum: 1, maximum: 4 }
            start: { type: string, format: date }
            end: { type: string, format: date }
            label: { type: string }
            submission_deadline: { type: string, format: date }
        settings: { type: object, additionalProperties: true }
        summary:
          type: object
          properties:
            return_currency: { type: string, example: EUR }
            total_base: { type: number }
            total_vat: { type: number }
            total_corrections: { type: number }
            total_payable: { type: number }
            invoice_count: { type: integer }
            row_count: { type: integer }
            correction_row_count: { type: integer }
            invalid_correction_count: { type: integer }
            conversion_missing_count: { type: integer }
        countries:
          type: array
          items: { type: object, additionalProperties: true }
        corrections:
          type: array
          items:
            type: object
            required: [period, year, quarter, state_consumption, correction, count, rows]
            properties:
              period: { type: string, pattern: '^[0-9]{4}Q[1-4]$' }
              year: { type: integer }
              quarter: { type: integer, minimum: 1, maximum: 4 }
              state_consumption: { type: string, minLength: 2, maxLength: 2 }
              correction: { type: number, description: "Rozdíl DPH v EUR, může být záporný." }
              count: { type: integer }
              rows:
                type: array
                items: { type: object, additionalProperties: true }
        warnings:
          type: array
          items: { type: string }

    TaxEvidenceNonCashAdjustment:
      type: object
      required: [adjustment_on, kind, direction, amount, description]
      properties:
        id: { type: integer, readOnly: true }
        adjustment_on: { type: string, format: date }
        kind: { type: string, enum: [setoff, barter, in_kind_income, debt_forgiveness, private_use, shortage, damage, inventory, receivable, payable, section23_other] }
        direction: { type: string, enum: [increase, decrease, neutral] }
        amount: { type: number, minimum: 0 }
        description: { type: string, maxLength: 500 }
        evidence_ref: { type: [string, "null"], maxLength: 190 }

    TaxEvidenceAnnualClosingWrite:
      type: object
      required: [row_version, checklist, opening_balances, closing_balances]
      properties:
        row_version: { type: integer, minimum: 1 }
        checklist: { type: object, additionalProperties: { type: boolean } }
        opening_balances: { type: object, additionalProperties: { type: number } }
        closing_balances: { type: object, additionalProperties: { type: number } }
        unsupported_cases: { type: array, items: { type: string, maxLength: 500 } }
        adjustments: { type: array, items: { $ref: '#/components/schemas/TaxEvidenceNonCashAdjustment' } }

    TaxEvidenceAnnualClosing:
      allOf:
        - $ref: '#/components/schemas/TaxEvidenceAnnualClosingWrite'
        - type: object
          required: [id, supplier_id, year, status, adjustments]
          properties:
            id: { type: integer }
            supplier_id: { type: integer }
            year: { type: integer }
            status: { type: string, enum: [draft, final] }
            source_snapshot: { type: [object, "null"], readOnly: true }
            source_hash: { type: [string, "null"], readOnly: true }
            finalized_at: { type: [string, "null"], format: date-time, readOnly: true }
            finalized_by: { type: [integer, "null"], readOnly: true }

    AutoPostingPolicyRow:
      type: object
      required: [operation_type, level, is_default, effective_level]
      properties:
        operation_type: { type: string }
        level: { type: string, enum: [off, suggest, auto] }
        is_default: { type: boolean }
        effective_level: { type: string, enum: [off, suggest, auto] }

    AutoPostingPolicy:
      type: object
      required: [automation_level, automation_daily_limit_czk, automation_digest_enabled, automation_digest_hour, rows]
      properties:
        automation_level: { type: string, enum: [off, suggest, assisted, full] }
        automation_daily_limit_czk: { type: [number, "null"], minimum: 0 }
        automation_digest_enabled: { type: boolean }
        automation_digest_hour: { type: integer, minimum: 0, maximum: 23 }
        rows:
          type: array
          items: { $ref: '#/components/schemas/AutoPostingPolicyRow' }

    AutoPostingPolicyWrite:
      type: object
      properties:
        automation_level: { type: string, enum: [off, suggest, assisted, full] }
        automation_daily_limit_czk: { type: [number, "null"], minimum: 0 }
        automation_digest_enabled: { type: boolean }
        automation_digest_hour: { type: integer, minimum: 0, maximum: 23 }
        rows:
          type: array
          items:
            type: object
            required: [operation_type, level]
            properties:
              operation_type: { type: string }
              level: { type: string, enum: [off, suggest, auto] }

    BankPostingRuleWrite:
      type: object
      properties:
        name: { type: string }
        direction: { type: string, enum: [incoming, outgoing] }
        counterparty_account: { type: [string, "null"] }
        counterparty_bank: { type: [string, "null"] }
        counterparty_prefix: { type: [string, "null"] }
        variable_symbol: { type: [string, "null"] }
        message_contains: { type: [string, "null"] }
        amount_min: { type: [number, "null"], minimum: 0 }
        amount_max: { type: [number, "null"], minimum: 0 }
        debit_account_code: { type: string }
        credit_account_code: { type: string }
        description: { type: [string, "null"] }
        is_active: { type: boolean }
        priority: { type: integer, minimum: 0, maximum: 999 }
        operation_type: { type: [string, "null"] }
        auto_amount_cap: { type: [number, "null"], minimum: 0 }
        applies_currency: { type: string, pattern: '^[A-Z]{3}$' }
        backfill_suggestions: { type: boolean }

    BankPostingRule:
      allOf:
        - $ref: '#/components/schemas/BankPostingRuleWrite'
        - type: object
          required: [id, supplier_id, mode, is_active, hit_count, priority, applies_currency]
          properties:
            id: { type: integer }
            supplier_id: { type: integer }
            system_template_key: { type: [string, "null"] }
            hit_count: { type: integer }
            approved_streak: { type: integer }
            promotion_candidate: { type: boolean }
            rejected_streak: { type: integer }
            mode: { type: string, enum: [suggest, auto] }
            last_hit_at: { type: [string, "null"], format: date-time }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }

    BankPostingRulePage:
      type: object
      required: [items, total, page, per_page]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/BankPostingRule' } }
        total: { type: integer, minimum: 0 }
        page: { type: integer, minimum: 1 }
        per_page: { type: integer, minimum: 1, maximum: 100 }

    BankPostingRuleDryRun:
      type: object
      required: [matched_count, already_posted_count, shadowed_by_own_transfer, sample]
      properties:
        matched_count: { type: integer }
        already_posted_count: { type: integer }
        shadowed_by_own_transfer: { type: boolean }
        sample:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              posted_at: { type: string, format: date }
              amount: { type: number }
              description: { type: [string, "null"] }
              already_posted: { type: boolean }

    BankPostingRuleHistory:
      type: object
      required: [events, corrections, stats, total, page, per_page]
      properties:
        total: { type: integer, minimum: 0 }
        page: { type: integer, minimum: 1 }
        per_page: { type: integer, minimum: 1, maximum: 100 }
        events:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              event_type: { type: string }
              reason: { type: [string, "null"] }
              created_by_name: { type: [string, "null"] }
              created_at: { type: string, format: date-time }
        corrections:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              event_type: { type: string }
              suggested: { type: [string, "null"] }
              final: { type: [string, "null"] }
              amount: { type: [number, "null"] }
              created_by_name: { type: [string, "null"] }
              created_at: { type: string, format: date-time }
        stats:
          type: object
          properties:
            hit_count: { type: integer }
            approved_streak: { type: integer }
            rejected_streak: { type: integer }
            override_count: { type: integer }
            success_rate: { type: number }

    BankRuleTemplate:
      type: object
      required: [template_key, name, direction, operation_type, rule_key, default_priority, already_instantiated]
      properties:
        template_key: { type: string }
        name: { type: string }
        direction: { type: string, enum: [incoming, outgoing] }
        operation_type: { type: string }
        counterparty_bank: { type: [string, "null"] }
        counterparty_prefix: { type: [string, "null"] }
        vs_placeholder: { type: [string, "null"] }
        vs_value: { type: [string, "null"] }
        message_contains: { type: [string, "null"] }
        rule_key: { type: string }
        debit_account_code: { type: [string, "null"] }
        credit_account_code: { type: [string, "null"] }
        default_priority: { type: integer }
        already_instantiated: { type: boolean }
        rule_id: { type: [integer, "null"] }

    AdminBankRuleTemplateWrite:
      type: object
      required: [template_key, name_cs, name_en, direction, operation_type, rule_key, default_priority, sort_order, is_active]
      properties:
        template_key: { type: string, maxLength: 64, pattern: '^[a-z0-9][a-z0-9._-]{0,63}$' }
        name_cs: { type: string, maxLength: 120 }
        name_en: { type: string, maxLength: 120 }
        direction: { type: string, enum: [incoming, outgoing] }
        operation_type: { type: string, maxLength: 40 }
        counterparty_bank: { type: [string, "null"], pattern: '^\d{2,10}$' }
        counterparty_prefix: { type: [string, "null"], pattern: '^\d{1,6}$' }
        vs_placeholder:
          type: [string, "null"]
          enum: ['{cssz_vsdp}', '{health_insurance_number}', '{dic_kmen}', null]
        message_contains: { type: [string, "null"], maxLength: 120 }
        rule_key: { type: string, maxLength: 64 }
        default_priority: { type: integer, minimum: 0, maximum: 999 }
        sort_order: { type: integer, minimum: 0, maximum: 65535 }
        is_active: { type: boolean }

    AdminBankRuleTemplate:
      allOf:
        - $ref: '#/components/schemas/AdminBankRuleTemplateWrite'
        - type: object
          required: [id, usage_count]
          properties:
            id: { type: integer }
            debit_account_code: { type: [string, "null"] }
            credit_account_code: { type: [string, "null"] }
            usage_count: { type: integer, minimum: 0 }

    AdminBankRuleTemplateCatalog:
      type: object
      required: [templates, operation_types, posting_rules]
      properties:
        templates:
          type: array
          items: { $ref: '#/components/schemas/AdminBankRuleTemplate' }
        operation_types:
          type: array
          items: { type: string }
        posting_rules:
          type: array
          items:
            type: object
            required: [rule_key, description]
            properties:
              rule_key: { type: string }
              description: { type: string }
              debit_account_code: { type: [string, "null"] }
              credit_account_code: { type: [string, "null"] }

    BankPostingSuggestion:
      type: object
      required: [id, source, transaction, debit_account_code, credit_account_code, amount, status, period_closed, created_at]
      properties:
        id: { type: integer }
        source: { type: string, enum: [rule, learned, payment_match, transfer, detector, schedule, knn, llm] }
        rule_id: { type: [integer, "null"] }
        rule_name: { type: [string, "null"] }
        rule_hit_count: { type: [integer, "null"] }
        rule_approved_streak: { type: [integer, "null"] }
        transaction: { type: object, additionalProperties: true }
        debit_account_code: { type: string }
        credit_account_code: { type: string }
        amount: { type: number }
        status: { type: string, enum: [pending, needs_input, blocked, approved, rejected, auto_posted, superseded] }
        note: { type: [string, "null"] }
        batch_id: { type: [string, "null"], pattern: '^[a-f0-9]{32}$' }
        snoozed_until: { type: [string, "null"], format: date-time }
        snooze_reason: { type: [string, "null"] }
        snoozed_by: { type: [integer, "null"] }
        confidence: { type: [number, "null"], minimum: 0, maximum: 1 }
        detector: { type: [string, "null"] }
        operation_type: { type: [string, "null"] }
        tax_advance_schedule_id: { type: [integer, "null"] }
        journal_entry_id: { type: [integer, "null"] }
        document_no: { type: [string, "null"] }
        period_closed: { type: boolean }
        correction:
          type: [object, "null"]
          properties:
            created_at: { type: string, format: date-time }
            suggested_debit: { type: [string, "null"] }
            suggested_credit: { type: [string, "null"] }
            final_debit: { type: [string, "null"] }
            final_credit: { type: [string, "null"] }
        created_at: { type: string, format: date-time }

    BankPostingSuggestionPage:
      type: object
      required: [items, total, page, per_page]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/BankPostingSuggestion' } }
        total: { type: integer, minimum: 0 }
        page: { type: integer, minimum: 1 }
        per_page: { type: integer, minimum: 1, maximum: 100 }

    BankPostingBulkResult:
      type: object
      required: [approved, approved_ids, failed, batch_id]
      properties:
        approved: { type: integer, minimum: 0 }
        approved_ids: { type: array, items: { type: integer, format: int64 } }
        failed: { type: array, items: { type: object, additionalProperties: true } }
        batch_id: { type: [string, "null"], pattern: '^[a-f0-9]{32}$' }

    BankPostingBulkPreview:
      type: object
      required: [count, items, accounts, failed]
      properties:
        count: { type: integer, minimum: 0 }
        items: { type: array, items: { type: object, additionalProperties: true } }
        accounts:
          type: array
          items:
            type: object
            required: [currency, account_code, debit, credit]
            properties:
              currency: { type: string }
              account_code: { type: string }
              debit: { type: number }
              credit: { type: number }
        failed: { type: array, items: { type: object, additionalProperties: true } }

    AutomationStats:
      type: object
      additionalProperties: true
      properties:
        period: { type: object, additionalProperties: true }
        range: { type: object, additionalProperties: true }
        automation_rate: { type: number }
        sources:
          type: array
          items: { type: object, additionalProperties: true }
        auto: { type: object, additionalProperties: true }
        rules: { type: object, additionalProperties: true }
        corrections: { type: object, additionalProperties: true }

    AutomationFeedItem:
      type: object
      required: [id, kind, tab, supplier_id, supplier_name, date, amount, currency, description, source, period_closed, can_write, refs]
      properties:
        id: { type: string }
        kind: { type: string, enum: [bank_suggestion, unbooked_invoice, unbooked_purchase, rule_disabled] }
        tab: { type: string, enum: [auto, pending, needs_input] }
        supplier_id: { type: integer }
        supplier_name: { type: string }
        date: { type: string, format: date }
        amount: { type: number }
        currency: { type: string }
        description: { type: string }
        counterparty: { type: [string, "null"] }
        debit_account_code: { type: [string, "null"] }
        credit_account_code: { type: [string, "null"] }
        source: { type: string }
        confidence: { type: [number, "null"], minimum: 0, maximum: 1 }
        detector: { type: [string, "null"] }
        operation_type: { type: [string, "null"] }
        rule_id: { type: [integer, "null"] }
        rule_name: { type: [string, "null"] }
        note: { type: [string, "null"] }
        snoozed_until: { type: [string, "null"], format: date-time }
        snooze_reason: { type: [string, "null"] }
        journal_entry_id: { type: [integer, "null"] }
        document_no: { type: [string, "null"] }
        period_closed: { type: boolean }
        can_write: { type: boolean }
        refs:
          type: object
          additionalProperties: true
        source_details:
          type: [object, "null"]
          additionalProperties: true
        conflict_rules:
          type: [array, "null"]
          items:
            type: object
            required: [id, name, debit_account_code, credit_account_code, hit_count]
            properties:
              id: { type: integer }
              name: { type: string }
              debit_account_code: { type: string }
              credit_account_code: { type: string }
              hit_count: { type: integer }
        duplicate_entry:
          type: [object, "null"]
          additionalProperties: true

    AutomationFeedPage:
      type: object
      required: [items, total, page, per_page]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/AutomationFeedItem' } }
        total: { type: integer }
        page: { type: integer }
        per_page: { type: integer }

    AutomationHistoryPage:
      type: object
      required: [items, total, page, per_page]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/AutomationHistoryItem' }
        total: { type: integer, minimum: 0 }
        page: { type: integer, minimum: 1 }
        per_page: { type: integer, minimum: 1, maximum: 200 }

    AutomationHistoryItem:
      type: object
      additionalProperties: false
      required: [id, supplier_id, supplier_name, event, source, amount, currency, debit_account_code, credit_account_code, description, journal_entry_id, bank_transaction_id, statement_id, transaction_date, occurred_at]
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        supplier_name: { type: string }
        event: { type: string, enum: [auto_posted, approved, rejected, superseded] }
        source: { type: string }
        amount: { type: number, format: double }
        currency: { type: string }
        debit_account_code: { type: string }
        credit_account_code: { type: string }
        description: { type: string }
        counterparty: { type: [string, "null"] }
        variable_symbol: { type: [string, "null"] }
        document_no: { type: [string, "null"] }
        journal_entry_id: { type: [integer, "null"] }
        bank_transaction_id: { type: integer }
        statement_id: { type: integer }
        transaction_date: { type: string, format: date }
        note: { type: [string, "null"] }
        occurred_at: { type: string, format: date-time }
        decided_by: { type: [string, "null"] }

    AccountingBackfillJob:
      type: object
      additionalProperties: true
      required: [id, supplier_id, kind, status, processed, cancel_requested, created_at]
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        kind: { type: string, enum: [dry_run, execute] }
        status: { type: string, enum: [queued, running, completed, failed, cancelled] }
        phase: { type: [string, "null"], enum: [opening, documents, cash, bank, null] }
        total_items: { type: [integer, "null"] }
        processed: { type: integer }
        cancel_requested: { type: boolean }
        created_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
        report_json:
          type: [object, "null"]
          properties:
            document_issues:
              type: array
              items:
                type: object
                required: [source_type, source_id, document_no, entry_date, severity, error_code, message]
                properties:
                  source_type: { type: string, enum: [invoice, purchase_invoice] }
                  source_id: { type: integer }
                  document_no: { type: string }
                  entry_date: { type: string, format: date }
                  severity: { type: string, enum: [skipped, failed] }
                  error_code: { type: string }
                  message: { type: string }
            document_coverage:
              type: object
              additionalProperties:
                type: object
                required: [expected, handled, missing, unexpected, complete]
                properties:
                  expected: { type: integer }
                  handled: { type: integer }
                  missing: { type: integer }
                  unexpected: { type: integer }
                  complete: { type: boolean }

    AccountingBackfillJobPage:
      type: object
      required: [items, total, page, per_page]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/AccountingBackfillJob' } }
        total: { type: integer, minimum: 0 }
        page: { type: integer, minimum: 1 }
        per_page: { type: integer, minimum: 1, maximum: 100 }

    AutomationCounts:
      type: object
      required: [auto_today, pending, needs_input, per_supplier]
      properties:
        auto_today: { type: integer }
        pending: { type: integer }
        needs_input: { type: integer }
        per_supplier:
          type: array
          items:
            type: object
            required: [supplier_id, supplier_name, auto_today, pending, needs_input]
            properties:
              supplier_id: { type: integer }
              supplier_name: { type: string }
              auto_today: { type: integer }
              pending: { type: integer }
              needs_input: { type: integer }

    CodebookImportReport:
      type: object
      description: Report importu číselníku (dry-run i ostrý běh mají identický tvar).
      properties:
        ok: { type: boolean }
        dry_run: { type: boolean }
        created: { type: integer }
        updated: { type: integer }
        skipped: { type: integer }
        failed: { type: integer }
        rows:
          type: array
          items:
            type: object
            properties:
              line: { type: integer }
              key: { type: string }
              status: { type: string, enum: [create, update, skip, error] }
              changes: { type: object, additionalProperties: { type: object, properties: { from: {}, to: {} } } }
              message: { type: string }
    ChartAccount:
      type: object
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        account_code: { type: string, example: "518" }
        name: { type: string }
        account_type: { type: string, enum: [asset, liability, equity, revenue, expense, offbalance] }
        normal_side: { type: [string, "null"], enum: [debit, credit, null] }
        is_synthetic: { type: boolean }
        parent_id: { type: [integer, "null"] }
        is_active: { type: boolean }
        children:
          type: array
          description: Přítomno jen v režimu ?tree=1
          items: { $ref: '#/components/schemas/ChartAccount' }

    AccountingPeriod:
      type: object
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        fiscal_year: { type: integer }
        starts_on: { type: string, format: date }
        ends_on: { type: string, format: date }
        status: { type: string, enum: [open, closing, closed, approved] }
        closed_at: { type: [string, "null"] }
        closed_by: { type: [integer, "null"] }
        approved_at: { type: [string, "null"] }
        approved_by: { type: [integer, "null"] }
        row_version: { type: integer, description: "Optimistická konkurence — posílá se zpět v mutacích" }

    JournalEntry:
      type: object
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        period_id: { type: integer }
        entry_date: { type: string, format: date }
        document_date: { type: [string, "null"], format: date }
        document_no: { type: [string, "null"] }
        description: { type: [string, "null"] }
        source_type: { type: string, enum: [invoice, purchase_invoice, bank, cash, asset, manual, closing, opening, stock] }
        source_id: { type: [integer, "null"], description: Drill-down na zdrojový doklad }
        posted_at: { type: [string, "null"], description: "NULL = koncept" }
        posted_by: { type: [integer, "null"] }
        reversed_by: { type: [integer, "null"] }
        row_version: { type: integer }
        amount: { type: number, format: float, description: "Σ MD řádků zápisu (jen v listu, audit 2026-07)." }
        source_statement_id: { type: [integer, "null"], description: "Jen source_type=bank — ID bankovního výpisu pro drill-down (audit 2026-07)." }
        source_doc_number: { type: [string, "null"], description: "Jen source_type=cash — číslo pokladního dokladu pro drill-down (audit 2026-07)." }
        source_register_id: { type: [integer, "null"], description: "Jen source_type=cash — ID pokladny (audit 2026-07)." }
        automation:
          type: [object, "null"]
          allOf:
            - $ref: '#/components/schemas/AutomationProvenance'
        _warnings:
          type: array
          description: "Volitelná varování při zaúčtování; `entry_date_outside_document_year` znamená, že explicitní datum zápisu leží v jiném kalendářním roce než DUZP/datum dokladu."
          items: { type: string, enum: [entry_date_outside_document_year] }
        lines:
          type: array
          items: { $ref: '#/components/schemas/JournalLine' }

    AutomationProvenance:
      type: object
      required: [source, mode, confidence, detector, rule_id, rule_name, suggestion_id, decided_at, decided_by]
      properties:
        source: { type: string, enum: [matched, rule, detector, learned, schedule, ai] }
        mode: { type: string, enum: [auto, approved] }
        confidence: { type: [number, "null"], format: float, minimum: 0, maximum: 1 }
        detector: { type: [string, "null"] }
        rule_id: { type: [integer, "null"] }
        rule_name: { type: [string, "null"] }
        suggestion_id: { type: [integer, "null"] }
        decided_at: { type: string, format: date-time }
        decided_by: { type: [string, "null"] }

    DphCrossCheckDocument:
      type: object
      required: [invoice_id, doc_number, source, declared, counter, difference]
      properties:
        invoice_id: { type: integer }
        doc_number: { type: [string, "null"] }
        source: { type: string, enum: [sale, purchase, cash, manual] }
        declared: { type: number, format: float }
        counter: { type: number, format: float }
        difference: { type: number, format: float }
        reason: { type: [string, "null"], enum: [timing_73, value_mismatch, missing_entry, extra_entry, null] }
        claim_period: { type: [string, "null"], pattern: '^\d{4}-\d{2}$' }
        entry_date: { type: [string, "null"], format: date }
        received_at: { type: [string, "null"], format: date }

    DphCrossCheckFinding:
      type: object
      required: [check, label, severity, blocking, declared, counter, difference, documents, note]
      properties:
        check: { type: string }
        label: { type: string }
        severity: { type: string, enum: [mismatch, info] }
        blocking: { type: boolean }
        declared: { type: [number, "null"], format: float }
        counter: { type: [number, "null"], format: float }
        difference: { type: number, format: float }
        explained: { type: number, format: float }
        documents: { type: array, items: { $ref: '#/components/schemas/DphCrossCheckDocument' } }
        note: { type: string }

    DphPriznaniPreview:
      type: object
      additionalProperties: true
      properties:
        cross_check: { type: array, items: { $ref: '#/components/schemas/DphCrossCheckFinding' } }

    MonthlyCheckResponse:
      type: object
      required: [period, range_from, range_to, checks, ran_at]
      properties:
        period: { $ref: '#/components/schemas/AccountingPeriod' }
        range_from: { type: string, format: date }
        range_to: { type: string, format: date }
        checks:
          type: array
          items:
            type: object
            required: [key, severity, ok]
            properties:
              key: { type: string }
              severity: { type: string }
              ok: { type: boolean }
              value: { type: [object, "null"], additionalProperties: true }
        ran_at: { type: string, format: date-time }

    BulkPostReport:
      type: object
      description: Souhrnný report hromadného zaúčtování (A2) — ok/fail per doklad.
      properties:
        posted:
          type: array
          description: ID dokladů, které se úspěšně zaúčtovaly.
          items: { type: integer }
        failed:
          type: array
          description: Doklady, které se nezaúčtovaly, se strojovým kódem chyby.
          items:
            type: object
            properties:
              id: { type: integer }
              error_code: { type: string }
              message: { type: string }

    JournalEntryDetail:
      allOf:
        - $ref: '#/components/schemas/JournalEntry'
        - type: object
          properties:
            lines:
              type: array
              items: { $ref: '#/components/schemas/JournalLineDetail' }

    JournalLine:
      type: object
      properties:
        id: { type: integer }
        account_id: { type: integer }
        side: { type: string, enum: [debit, credit] }
        amount: { type: number, format: float }
        currency_code: { type: [string, "null"] }
        fx_rate: { type: [number, "null"], format: float }
        amount_foreign: { type: [number, "null"], format: float }
        cost_center: { type: [string, "null"] }
        line_no: { type: integer }

    JournalLineDetail:
      allOf:
        - $ref: '#/components/schemas/JournalLine'
        - type: object
          properties:
            account_code: { type: [string, "null"] }
            account_name: { type: [string, "null"] }

    ManualJournalEntry:
      type: object
      required: [entry_date, lines]
      properties:
        entry_date: { type: string, format: date }
        description: { type: [string, "null"] }
        document_no: { type: [string, "null"] }
        lines:
          type: array
          minItems: 1
          items:
            type: object
            required: [account_code, side, amount]
            properties:
              account_code: { type: string, example: "518" }
              side: { type: string, enum: [debit, credit] }
              amount: { type: number, format: float, example: 1000.00 }
              cost_center: { type: [string, "null"] }

    PostingRule:
      type: object
      properties:
        id: { type: integer }
        supplier_id: { type: [integer, "null"], description: "NULL = globální šablona" }
        rule_key: { type: string, example: "invoice.services.issued" }
        description: { type: string }
        debit_account_code: { type: [string, "null"] }
        credit_account_code: { type: [string, "null"] }
        priority: { type: integer }
        is_active: { type: boolean }

    VatClassificationInput:
      type: object
      properties:
        code: { type: string, maxLength: 8 }
        label: { type: string, maxLength: 150 }
        direction: { type: string, enum: [sale, purchase, both] }
        dphdp3_line: { type: [string, 'null'] }
        kh_section: { type: [string, 'null'] }
        vat_rate: { type: [number, 'null'] }
        is_reverse_charge: { type: boolean }
        kh_regime_code:
          type: [string, 'null']
          enum: ['0', '1', '2', null]
          description: VetaA4.kod_rezim_pl — běžné plnění, cestovní služba §89 nebo použité zboží §90.
        kh_bad_debt:
          type: [string, 'null']
          enum: [N, P, null]
          description: VetaA4/VetaB2.zdph_44 — běžné plnění nebo oprava nedobytné pohledávky.
        display_order: { type: integer }
        archived: { type: boolean }
    VatClassificationCreate:
      allOf:
        - $ref: '#/components/schemas/VatClassificationInput'
        - type: object
          required: [code, label, direction]
    VatClassification:
      allOf:
        - $ref: '#/components/schemas/VatClassificationInput'
        - type: object
          required: [id, supplier_id, archived, created_at]
          properties:
            id: { type: integer }
            supplier_id: { type: [integer, 'null'] }
            archived: { type: boolean }
            created_at: { type: string, format: date-time }
    RevenueCategory:
      type: object
      properties:
        id: { type: integer }
        code: { type: string, maxLength: 20 }
        label: { type: string, maxLength: 100 }
        display_order: { type: integer }
        archived: { type: boolean }
        created_at: { type: string }
        invoices_count:
          type: integer
          description: Počet vydaných faktur v této kategorii (jen v listu)
    ExpenseCategory:
      type: object
      properties:
        id: { type: integer }
        code: { type: string, maxLength: 20 }
        label: { type: string, maxLength: 100 }
        fixed_or_var: { type: string, enum: [fixed, variable] }
        display_order: { type: integer }
        archived: { type: boolean }
        created_at: { type: string }
        purchases_count:
          type: integer
          description: Počet přijatých faktur v této kategorii (jen v listu)
    TaxConstantsYear:
      type: object
      description: Efektivní daňové konstanty pro rok (výchozí seed nebo override).
      additionalProperties: true
      properties:
        year: { type: integer }
        is_override:
          type: boolean
          description: true = přepsáno administrátorem, false = výchozí konstanty
    TaxConstantsInput:
      type: object
      description: |
        Daňové konstanty pro rok. Lze poslat ploše nebo zabaleně v `{ "data": {...} }`.
        Všechny skalární klíče jsou povinné; vnořené struktury musí být objekty/pole.
      required:
        - credit_taxpayer
        - credit_spouse
        - tax_rate_low
        - tax_rate_high
        - tax_high_threshold
        - social_rate
        - health_rate
        - social_assessment_pct
        - health_assessment_pct
        - social_min_base_main
        - social_min_base_secondary
        - social_secondary_participation_threshold
        - health_min_base
        - mortgage_cap
        - pension_cap
        - vat_limit_low
        - vat_limit_high
        - vat_rate_standard
        - vat_rate_reduced
        - kh_item_threshold
        - band_ceilings
        - expense_caps
        - child_credits
        - minimum_wage
        - child_bonus_min_income
        - spouse_income_limit
        - tax_loss_carry_years
        - m1_depreciation_limit
        - extraordinary_depreciation
        - depreciation_straight_rates
        - depreciation_accelerated_coefficients
        - entity_category_thresholds
        - filing_deadlines
      properties:
        credit_taxpayer: { type: number, description: Sleva na poplatníka (Kč/rok) }
        credit_spouse: { type: number, description: Sleva na manžela/ku }
        tax_rate_low: { type: number, description: Nižší sazba daně z příjmu (%) }
        tax_rate_high: { type: number, description: Vyšší sazba daně z příjmu (%) }
        tax_high_threshold: { type: number, description: Práh pro vyšší sazbu }
        social_rate: { type: number }
        health_rate: { type: number }
        social_assessment_pct: { type: number }
        health_assessment_pct: { type: number }
        social_min_base_main: { type: number }
        social_min_base_secondary: { type: number }
        social_secondary_participation_threshold: { type: number, description: Rozhodná částka (zisk) pro povinnou účast na důch. pojištění u vedlejší SVČ }
        health_min_base: { type: number }
        mortgage_cap: { type: number, description: Strop odpočtu úroků z hypotéky }
        pension_cap: { type: number }
        vat_limit_low: { type: number, description: Limit obratu pro povinnou registraci DPH }
        vat_limit_high: { type: number }
        vat_rate_standard: { type: number, description: "Základní sazba DPH (%) — sloupce DPH výkazů, samovyměření RC, auto-klasifikace" }
        vat_rate_reduced: { type: number, description: Snížená sazba DPH (%) }
        kh_item_threshold: { type: number, description: "Limit KH (Kč vč. DPH) — nad limit doklad jednotlivě (A.4/B.2), do limitu sumace (A.5/B.3)" }
        pausal_monthly:
          type: array
          description: |
            Rozvrh měsíčních záloh paušální daně. Segment platí od `from` (vždy 1. den
            měsíce daného roku, první musí být 1. 1.), dokud ho nevystřídá další —
            sazba se může změnit uprostřed roku (2026: 1. pásmo 9 984 → 9 162 Kč
            od 1. 7.). Roční částka `pausal_annual` je z rozvrhu odvozená a neukládá se.
            Starší klienti smí místo rozvrhu poslat `pausal_annual` (bere se doslova).
          items:
            type: object
            required: [from, band1, band2, band3]
            properties:
              from: { type: string, format: date, example: '2026-07-01' }
              band1: { type: number, description: Měsíční záloha 1. pásma (Kč) }
              band2: { type: number }
              band3: { type: number }
        pausal_annual:
          type: object
          description: |
            Roční částky paušální daně per pásmo. Ve výstupu ODVOZENÉ ze součtu
            dvanácti měsíčních záloh roku; na vstupu se přijímají jen tehdy, není-li
            poslaný `pausal_monthly` (zpětná kompatibilita).
        band_ceilings:
          type: object
          description: Stropy obratu pro pásma paušální daně
        expense_caps:
          type: object
          description: Stropy výdajových paušálů per míra (40/60/80 %)
        child_credits:
          type: array
          minItems: 1
          description: Slevy na dítě (1./2./3.+ dítě)
          items: { type: number }
        minimum_wage: { type: number, description: Minimální měsíční mzda }
        child_bonus_min_income: { type: number, description: Minimální roční příjem pro daňový bonus }
        spouse_income_limit: { type: number }
        tax_loss_carry_years: { type: integer }
        m1_depreciation_limit: { type: number }
        extraordinary_depreciation:
          type: object
          description: Rozhodné období, délka a rozdělení mimořádných odpisů
          additionalProperties: true
        depreciation_straight_rates:
          type: object
          description: Sazby rovnoměrných odpisů podle varianty a odpisové skupiny
          additionalProperties: true
        depreciation_accelerated_coefficients:
          type: object
          description: Koeficienty zrychlených odpisů podle odpisové skupiny
          additionalProperties: true
        entity_category_thresholds:
          type: object
          description: Prahy aktiv, obratu a zaměstnanců pro kategorie účetních jednotek
          additionalProperties: true
        filing_deadlines:
          type: object
          description: Roční termíny ve formátu MM-DD a dny splatnosti záloh
          additionalProperties: true
    TaxProfileInput:
      type: object
      properties:
        year: { type: integer, description: 'Default = aktuální rok' }
        activity_rate: { type: integer, description: 'Výdajový paušál v % (40/60/80)', default: 60 }
        use_actual_expenses: { type: boolean }
        actual_expenses: { type: number }
        flat_tax_band: { type: string, description: Pásmo paušální daně }
        is_secondary: { type: boolean, description: Vedlejší činnost }
        spouse_credit: { type: boolean }
        children_count: { type: integer }
        mortgage_interest: { type: number }
        pension_contrib: { type: number }
        life_insurance: { type: number }
        donations: { type: number }
    DocumentBreadcrumb:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
    DocumentFolder:
      type: object
      properties:
        id: { type: integer }
        parent_id: { type: [integer, "null"] }
        name: { type: string }
        created_at: { type: string }
        subfolder_count: { type: integer, description: "Naplněno jen v listingu potomků; jinak 0" }
        file_count: { type: integer, description: "Naplněno jen v listingu potomků; jinak 0" }
        total_bytes: { type: integer, description: "Součet velikostí aktivních souborů ve složce (jen v listingu potomků)" }
    Document:
      type: object
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        folder_id: { type: [integer, "null"] }
        title: { type: string }
        description: { type: [string, "null"] }
        original_name: { type: string }
        filename: { type: string, description: "Název souboru na disku ({sha8}-{safeName})" }
        sha256: { type: string }
        mime_type: { type: string }
        size_bytes: { type: integer }
        doc_type: { type: string, enum: [pdf, docx, xlsx, xml, zfo, p7s, zip, image, other] }
        source: { type: string, enum: [manual, zfo_extract, zip_extract] }
        parent_document_id: { type: [integer, "null"] }
        signature_for_id: { type: [integer, "null"] }
        text_status: { type: string, enum: [none, extracted, unsupported, failed] }
        thumb_status: { type: string, enum: [none, generated, unsupported, failed] }
        has_thumb: { type: boolean }
        uploaded_by: { type: [integer, "null"], description: "User ID nahrávajícího" }
        created_at: { type: string }
        deleted_at: { type: [string, "null"], description: "Soft-delete (koš); NULL = aktivní" }
    DocumentLink:
      type: object
      properties:
        entity_type: { type: string, enum: [client, invoice, purchase_invoice, project] }
        entity_id: { type: integer }
        label: { type: string }
    DocumentLinkCandidate:
      type: object
      properties:
        entity_type: { type: string, enum: [client, invoice, purchase_invoice, project] }
        entity_id: { type: integer }
        label: { type: string }
        sublabel: { type: string }
        meta: { type: string }
    DocumentDmsMessage:
      type: object
      description: Metadata datové zprávy (ZFO) z ISDS
      properties:
        dm_id: { type: [string, "null"] }
        direction: { type: string, enum: [sent, received, unknown] }
        sender_box_id: { type: [string, "null"] }
        sender_name: { type: [string, "null"] }
        recipient_box_id: { type: [string, "null"] }
        recipient_name: { type: [string, "null"] }
        annotation: { type: [string, "null"], description: Předmět zprávy }
        delivery_time: { type: [string, "null"] }
        acceptance_time: { type: [string, "null"] }
    DocumentDetail:
      allOf:
        - { $ref: '#/components/schemas/Document' }
        - type: object
          properties:
            tags: { type: array, items: { type: string } }
            links: { type: array, items: { $ref: '#/components/schemas/DocumentLink' } }
            attachments: { type: array, items: { $ref: '#/components/schemas/Document' } }
            dms_message: { $ref: '#/components/schemas/DocumentDmsMessage' }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, example: validation_failed }
            message: { type: string, example: "Pole 'name' je povinné." }

    ListMeta:
      type: object
      description: "Stránkovací metadata seznamů (obálka `meta`)."
      required: [total]
      properties:
        total: { type: integer, example: 423, description: "Celkový počet záznamů přes všechny stránky" }
        page: { type: integer, example: 1 }
        per_page: { type: integer, example: 50 }
        pages: { type: integer, example: 9, description: "Celkový počet stránek" }

    ApiMe:
      type: object
      properties:
        user:
          type: object
          properties:
            id: { type: integer }
            email: { type: string, format: email }
            name: { type: string }
            role: { type: string, enum: [admin, accountant, readonly] }
        supplier:
          oneOf:
            - $ref: '#/components/schemas/SupplierBrief'
            - { type: "null" }
        auth_method:
          type: string
          enum: [bearer, session]
        token:
          oneOf:
            - type: object
              properties:
                id: { type: integer }
                name: { type: string }
                prefix: { type: string, example: "mi_pat_abcd" }
                scope: { type: string, enum: [read, read_write] }
                expires_at: { type: [string, "null"], format: date-time }
            - { type: "null" }

    SupplierBrief:
      type: object
      properties:
        id: { type: integer }
        company_name: { type: string }
        display_name: { type: [string, "null"] }
        accounting_mode:
          type: string
          enum: [tax_evidence, double_entry]
          description: "Režim účetnictví firmy (Epic F0): daňová evidence vs podvojné účetnictví"

    UserSupplierAssignment:
      type: object
      description: |
        Přiřazení uživatele k firmě (user_suppliers, Epic F0). Uživatel
        s alespoň jedním přiřazením smí jen do přiřazených firem; `role`
        přepisuje globální `users.role` pro danou firmu (null = zdědit).
      properties:
        supplier_id: { type: integer }
        name: { type: string, description: "display_name, fallback company_name firmy" }
        role:
          type: [string, "null"]
          enum: [admin, accountant, readonly, null]
          description: "Per-supplier override role; null = zdědit globální users.role"

    Country:
      type: object
      properties:
        id: { type: integer }
        code: { type: string, example: CZ }
        name_cs: { type: string }
        name_en: { type: string }
        in_eu: { type: boolean }

    Currency:
      type: object
      properties:
        id: { type: integer }
        code: { type: string, example: CZK }
        symbol: { type: string, example: "Kč" }

    Money:
      type: object
      properties:
        amount: { type: number, format: float, example: 12500.50 }
        currency: { type: string, example: CZK }

    BrandingProfile:
      type: object
      description: "Aktivní vizuální identita aktuálního dodavatele. Prázdná pole se při vystavení dědí z dodavatele."
      properties:
        id: { type: integer }
        name: { type: string, description: "Interní název profilu" }
        display_name: { type: [string, "null"] }
        tagline: { type: [string, "null"] }
        email: { type: [string, "null"], format: email }
        reply_to: { type: [string, "null"], format: email }
        phone: { type: [string, "null"] }
        web: { type: [string, "null"] }
        email_footer: { type: [string, "null"] }
        logo_path: { type: [string, "null"], readOnly: true }
        accent_color: { type: string, pattern: '^#[0-9A-Fa-f]{6}$' }
        branding_enabled: { type: boolean, description: "Zapíná vlastní logo a barevný branding v e-mailech a PDF." }
        pdf_logo_show_name: { type: boolean }
        is_default: { type: boolean, readOnly: true, description: "Výchozí profil dodavatele použitý bez explicitní volby." }

    Client:
      type: object
      description: |
        Klient = firma v roli odběratele a/nebo dodavatele (jedna řádka, flagy
        `is_customer` / `is_vendor`). Stav archivace je timestamp `archived_at`
        (NULL = aktivní), ne boolean.
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        company_name: { type: string }
        first_name: { type: [string, "null"] }
        last_name: { type: [string, "null"] }
        ic: { type: [string, "null"], description: IČO (8 číslic) }
        dic: { type: [string, "null"], description: "DIČ / VAT ID s country prefixem (u SK klienta = IČ DPH)" }
        tax_number: { type: [string, "null"], description: "Národní daňové číslo bez prefixu (SK DIČ, DE/AT Steuernummer, PL NIP, HU Adószám) — issue #120" }
        street: { type: string }
        city: { type: string }
        zip: { type: string }
        country_id: { type: integer }
        country_iso2: { type: string, description: "Denormalizovaný ISO2 kód země (alias z countries)" }
        main_email: { type: [string, "null"], format: email, description: "Volitelný hlavní e-mail klienta. Bez účelových kontaktů slouží jako fallback pro komunikaci." }
        phone: { type: [string, "null"] }
        language: { type: string, enum: [cs, en], description: "Jazyk faktur a emailů. Default cs." }
        currency_default_id: { type: [integer, "null"], description: "Výchozí měna pro faktury klienta. Default = supplier.default_currency_id." }
        currency_default: { type: string, description: "Denormalizovaný kód výchozí měny (alias z currencies)" }
        vat_rate_default_id: { type: [integer, "null"], description: "Legacy výchozí sazba DPH" }
        hourly_rate: { type: [number, "null"], format: float, description: "Default hodinová sazba (CZK / hod) pro projekty klienta" }
        reverse_charge: { type: boolean, description: "Přenesená daňová povinnost — propisuje se do nových faktur." }
        is_customer: { type: boolean, description: "Role odběratele (vystavujeme mu faktury)" }
        is_vendor: { type: boolean, description: "Role dodavatele (evidujeme přijaté faktury)" }
        is_vat_payer: { type: boolean, description: "Plátce DPH (ověřitelné přes /clients/{id}/vat-status)" }
        auto_send_reminders: { type: boolean, description: "Automatické odesílání upomínek" }
        payment_due_default: { type: [integer, "null"], description: "Default splatnost pro nové faktury klienta" }
        payment_due_unit: { type: [string, "null"], enum: [days, month, null], description: "Jednotka splatnosti (dny / konec měsíce)" }
        note: { type: [string, "null"] }
        default_expense_category_id: { type: [integer, "null"], description: "Default kategorie nákladu pro přijaté faktury dodavatele" }
        default_revenue_category_id: { type: [integer, "null"], description: "Default kategorie tržby pro vydané faktury odběratele" }
        invoice_number_format: { type: [string, "null"], description: "Per-klient override číselné řady faktur" }
        proforma_number_format: { type: [string, "null"] }
        credit_note_number_format: { type: [string, "null"] }
        invoice_number_period: { type: [string, "null"], enum: [year, month, none, null] }
        default_branding_profile_id: { type: [integer, "null"], description: "Výchozí brandingový profil pro nové vydané faktury klienta." }
        archived_at: { type: [string, "null"], format: date-time, description: "NULL = aktivní; vyplněno = archivováno" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        _warnings:
          type: array
          items: { type: string }
          description: "Jen v odpovědi POST/PUT — non-blocking varování (např. ic_checksum_invalid, dic_format_invalid, dic_ic_mismatch — audit 2026-07), uložení neblokují."

    ClientEmailContact:
      type: object
      description: "E-mailový kontakt odběratele dle účelu (#86). Účely: communication (běžná komunikace), documents (faktury/dobropisy/poděkování za platbu), reminders (upomínky; bez nich fallback na documents), approvals (schvalování výkazů). Jakmile má klient aktivní kontakt s daným účelem, main_email se pro tento typ zprávy přestane automaticky přidávat (zůstává jen fallback)."
      properties:
        id: { type: integer, readOnly: true }
        email: { type: string, format: email }
        label: { type: [string, "null"], maxLength: 80, description: 'Popisek (např. „účetní oddělení")' }
        contact_name: { type: [string, "null"], maxLength: 120, description: "Jméno kontaktní osoby" }
        is_active: { type: boolean, default: true }
        sort_order: { type: integer, readOnly: true, description: "Pořadí — odvozeno z pořadí v zaslaném poli" }
        usages:
          type: array
          description: "Účely použití kontaktu + role příjemce."
          items:
            type: object
            required: [usage]
            properties:
              usage: { type: string, enum: [communication, documents, reminders, approvals] }
              recipient: { type: string, enum: [to, cc, bcc], default: to }

    ClientBankAccount:
      type: object
      description: "Bankovní účet obchodního partnera. Příznaky původu jsou nezávislé, protože tentýž účet může být ruční, zveřejněný v registru DPH i pozorovaný na výpisu."
      required: [id, client_id, account_number, source_manual, source_vat_registry, source_bank_statement, is_active]
      properties:
        id: { type: integer, format: int64, readOnly: true }
        client_id: { type: integer, format: int64, readOnly: true }
        account_number: { type: string }
        bank_code: { type: [string, "null"] }
        iban: { type: [string, "null"] }
        source_manual: { type: boolean }
        source_vat_registry: { type: boolean }
        source_bank_statement: { type: boolean }
        last_bank_transaction_id: { type: [integer, "null"], format: int64, readOnly: true }
        is_active: { type: boolean }
        first_seen_at: { type: string, format: date-time, readOnly: true }
        last_seen_at: { type: string, format: date-time, readOnly: true }

    ClientDetail:
      allOf:
        - $ref: '#/components/schemas/Client'
        - type: object
          properties:
            email_contacts:
              type: array
              items: { $ref: '#/components/schemas/ClientEmailContact' }
            bank_accounts:
              type: array
              items: { $ref: '#/components/schemas/ClientBankAccount' }
            projects:
              type: array
              items: { $ref: '#/components/schemas/Project' }
            invoices_count:
              type: integer
              description: Celkový počet faktur klienta.
            revenue_by_month:
              type: array
              description: "Obrat po měsících za posledních 24 měsíců (VAT-aware)."
              items:
                type: object
                properties:
                  month: { type: string, example: "2026-04" }
                  currency: { type: string }
                  total: { type: number, format: float }
            revenue_by_year:
              type: array
              description: "Obrat po letech (VAT-aware)."
              items:
                type: object
                properties:
                  year: { type: integer }
                  currency: { type: string }
                  total: { type: number, format: float }
                  count: { type: integer }
            revenue_by_project:
              type: array
              description: 'Obrat agregovaný per zakázka — pro graf "Obrat podle zakázek". Faktury bez project_id agregovány pod project_id=null.'
              items:
                type: object
                properties:
                  project_id: { type: [integer, "null"] }
                  project_name: { type: [string, "null"] }
                  currency: { type: string }
                  total: { type: number, format: float }
                  count: { type: integer }
            unpaid_summary:
              type: array
              items:
                type: object
                properties:
                  currency: { type: string }
                  unpaid_total:  { type: number, format: float }
                  unpaid_count:  { type: integer }
                  overdue_total: { type: number, format: float }
                  overdue_count: { type: integer }

    ClientInput:
      type: object
      required: [company_name, street, city, zip]
      properties:
        company_name: { type: string }
        first_name: { type: [string, "null"] }
        last_name: { type: [string, "null"] }
        ic: { type: [string, "null"], description: "IČO — 8 číslic" }
        dic: { type: [string, "null"], description: "DIČ / VAT ID s country prefixem (u SK = IČ DPH)" }
        tax_number: { type: [string, "null"], description: "Národní daňové číslo bez prefixu (SK DIČ, DE/AT Steuernummer, PL NIP, HU Adószám)" }
        street: { type: string }
        city: { type: string }
        zip: { type: string }
        country_iso2: { type: string, default: CZ, description: "ISO2 kód země (resolvuje se na country_id). POZOR: vstup je `country_iso2`, ne `country_id`." }
        main_email: { type: [string, "null"], format: email, description: "Volitelný hlavní e-mail (backend pole `main_email`, ne `email`)." }
        phone: { type: [string, "null"], maxLength: 40 }
        language: { type: string, enum: [cs, en], default: cs }
        currency_default_id: { type: [integer, "null"], description: "Výchozí měna klienta. Pokud chybí, použije se výchozí měna supplier-a." }
        currency_default: { type: string, description: "Legacy — kód měny (CZK…). Použij currency_default_id." }
        hourly_rate: { type: [number, "null"], format: float, description: "Default hodinová sazba pro nové projekty (≥ 0)" }
        reverse_charge: { type: boolean, default: false }
        is_customer: { type: boolean, default: true, description: "Role odběratele. Aspoň jedna z is_customer/is_vendor musí zůstat true." }
        is_vendor: { type: boolean, default: false, description: "Role dodavatele." }
        is_vat_payer: { type: boolean, default: true }
        auto_send_reminders: { type: boolean, default: true }
        payment_due_default: { type: integer, minimum: 1, maximum: 365, description: "Default splatnost" }
        payment_due_unit: { type: [string, "null"], enum: [days, month, null] }
        note: { type: [string, "null"] }
        default_expense_category_id: { type: [integer, "null"] }
        default_revenue_category_id: { type: [integer, "null"] }
        invoice_number_format: { type: [string, "null"], maxLength: 60 }
        proforma_number_format: { type: [string, "null"] }
        credit_note_number_format: { type: [string, "null"] }
        invoice_number_period: { type: [string, "null"], enum: [year, month, none, null] }
        default_branding_profile_id: { type: [integer, "null"], description: "Volitelný výchozí brandingový profil; musí patřit aktuálnímu supplier-ovi." }
        email_contacts:
          type: array
          description: "E-mailové kontakty dle účelu (#86) — replace-all: pošli kompletní pole; klíč vynech, pokud kontakty neměníš. Max 10 kontaktů."
          items: { $ref: '#/components/schemas/ClientEmailContact' }

    Invoice:
      type: object
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        branding_profile_id: { type: [integer, "null"], description: "Vizuální identita dokladu; při vystavení se uloží do supplier_snapshot." }
        client_id: { type: integer }
        client_company_name: { type: string, description: "Denormalizováno z klienta pro pohodlí výpisů" }
        client_main_email: { type: [string, "null"], format: email }
        project_id: { type: [integer, "null"] }
        project_name: { type: [string, "null"] }
        parent_invoice_id: { type: [integer, "null"], description: "Pro credit_note / cancellation / final-z-proformy odkaz na původní fakturu" }
        recurring_template_id: { type: [integer, "null"], description: "Pro faktury vygenerované ze šablony pravidelné fakturace — odkaz na šablonu (NULL pokud byla šablona smazána, viz ON DELETE SET NULL)." }
        varsymbol: { type: [string, "null"], description: "Variabilní symbol = číslo faktury. Přiděluje se při issue (u draft je null nebo manuální override)." }
        invoice_type:
          type: string
          enum: [invoice, proforma, credit_note, cancellation, tax_document]
          description: "tax_document = daňový doklad k přijaté platbě zálohy (§ 28/2 ZDPH); DUZP = datum platby, parent_invoice_id = proforma."
        status:
          type: string
          enum: [draft, issued, sent, reminded, paid, cancelled]
        payment_status:
          type: [string, "null"]
          enum: [unpaid, partially_paid, paid, overpaid, null]
          description: "Odvozený platební stav (#89) z paid_total vs. amount_to_pay (tolerance 0,05). Null pro draft/cancelled."
        payment_method:
          type: string
          enum: [bank_transfer, card, cash, other]
          default: bank_transfer
          description: "Způsob úhrady. U card/cash se v PDF/emailu nezobrazuje QR ani bankovní spojení a místo výzvy k platbě se uvede odpovídající poznámka."
        language: { type: string, enum: [cs, en], description: "Jazyk PDF/emailů" }
        reverse_charge: { type: boolean }
        prices_include_vat: { type: boolean, description: "Ceny položek zadané včetně DPH (DPH se počítá shora koeficientem). Per-doklad, migrace 0083." }
        auto_send_reminders: { type: boolean, description: "Per-faktura přepínač automatických upomínek (migrace 0088). False = cron tuto fakturu přeskočí, i když má dodavatel i klient upomínky zapnuté." }
        client_ic: { type: [string, "null"], description: "Denormalizováno z klienta" }
        client_dic: { type: [string, "null"] }
        revenue_category_id: { type: [integer, "null"], description: "FK do /revenue-categories — kategorie tržby pro CRM/Grafy" }
        revenue_category_label: { type: [string, "null"], description: "Denormalizovaný label kategorie tržby" }
        revenue_category_code: { type: [string, "null"] }
        revenue_category: { type: [string, "null"], description: "Legacy volný textový tag (nahrazeno revenue_category_id)" }
        vat_classification_code: { type: [string, "null"], description: "Hlavičkový default kódu klasifikace plnění (DPHDP3 / KH)" }
        issue_date: { type: string, format: date }
        due_date: { type: string, format: date }
        tax_date: { type: [string, "null"], format: date, description: "DUZP — null u proforem" }
        currency_id: { type: integer, description: "FK do /settings/currencies (per-supplier)" }
        currency: { type: string, example: CZK, description: "Denormalizovaný kód měny (= currencies.code)" }
        currency_symbol: { type: string, example: "Kč" }
        exchange_rate: { type: [number, "null"], format: float, description: "CZK / 1 jednotka cizí měny (ČNB). NULL = neaplikováno (např. faktura v CZK)" }
        exchange_rate_date: { type: [string, "null"], format: date }
        total_without_vat: { type: number, format: float }
        total_vat: { type: number, format: float }
        total_with_vat: { type: number, format: float }
        rounding: { type: number, format: float, description: "Zaokrouhlení (CZK)" }
        advance_paid_amount: { type: number, format: float, description: "Záloha k odečtení (u faktury vystavované po proforme)" }
        amount_to_pay: { type: number, format: float, description: "total_with_vat + rounding − advance_paid_amount" }
        paid_total: { type: number, format: float, description: "Suma evidovaných plateb (invoice_payments, #89); zbývá k úhradě = amount_to_pay − paid_total" }
        discount_percent: { type: number, format: float, description: "Procentuální sleva z celé faktury (0 = žádná). Materializuje se jako záporná položka „Sleva X %\" v `items`." }
        totals:
          type: object
          description: "Agregát pro renderování — duplikuje stejné hodnoty co výše"
          properties:
            without_vat: { type: number, format: float }
            vat: { type: number, format: float }
            with_vat: { type: number, format: float }
            rounding: { type: number, format: float }
            advance_paid_amount: { type: number, format: float }
            amount_to_pay: { type: number, format: float }
            discount_percent: { type: number, format: float, description: "Procentuální sleva z celé faktury (0 = žádná)" }
            discount_amount: { type: number, format: float, description: "Kladná peněžní hodnota slevy (= součet záporných slevových položek)" }
        vat_breakdown:
          type: array
          description: "Per-sazba rozpad DPH (vstup pro QR + PDF)"
          items:
            type: object
            properties:
              vat_rate_id: { type: integer }
              rate: { type: number, format: float, example: 21 }
              base: { type: number, format: float }
              vat:  { type: number, format: float }
              total: { type: number, format: float }
        czk_recap:
          description: "CZK rekapitulace u cizoměnových faktur (null pokud měna == CZK nebo není zafixován kurz)"
          type: [object, "null"]
        note: { type: [string, "null"], description: "Generická poznámka. Pro výstup viz note_above_items / note_below_items." }
        note_above_items: { type: [string, "null"] }
        note_below_items: { type: [string, "null"] }
        bank_account_number: { type: [string, "null"], description: "Z currencies (per-měna bank účet supplier-a)" }
        bank_code: { type: [string, "null"] }
        bank_iban: { type: [string, "null"] }
        bank_bic: { type: [string, "null"] }
        approval_status:
          type: [string, "null"]
          enum: [none, requested, approved, rejected, null]
        public_token: { type: [string, "null"], description: "Token trvalého veřejného odkazu „web faktura\" (/invoice/{token}); null dokud odkaz nevznikl. Viz POST /invoices/{id}/public-link." }
        public_viewed_at: { type: [string, "null"], format: date-time, description: "Poslední zobrazení web faktury klientem (anonymní přístup); null = klient ji zatím neotevřel." }
        items:
          type: array
          items: { $ref: '#/components/schemas/InvoiceItem' }
        project_billing_emails:
          type: array
          items:
            type: object
            properties:
              email: { type: string, format: email }
              label: { type: [string, "null"] }
        final_invoice:
          type: [object, "null"]
          description: "Jen u proformy — navázaný finální daňový doklad ({ id, varsymbol, status })"
        parent_invoice:
          type: [object, "null"]
          description: "Jen pokud je parent_invoice_id ({ id, varsymbol, status, invoice_type })"
        has_advance_candidates: { type: boolean, description: "UI flag — existují nespárované zálohy k propojení" }
        has_final_candidates: { type: boolean }
        sent_at: { type: [string, "null"], format: date-time, description: "Kdy byla naposledy odeslána emailem" }
        last_reminder_at: { type: [string, "null"], format: date-time }
        reminder_count: { type: integer, description: "Počet odeslaných upomínek" }
        paid_at: { type: [string, "null"], format: date }
        cancelled_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    InvoiceMonthGroup:
      type: object
      description: |
        Jedna měsíční skupina výpisu faktur (GET /invoices). Měsíc se určuje
        z `COALESCE(tax_date, issue_date)`.
      required: [month, count, totals_per_currency, invoices]
      properties:
        month: { type: string, example: "2026-05", description: "Měsíc ve formátu YYYY-MM" }
        count: { type: integer, description: "Počet faktur skupiny na aktuální stránce" }
        totals_per_currency:
          type: array
          description: |
            Měsíční obrat po měnách. Do obratu (`without_vat`/`vat`/`with_vat`) se
            počítají jen vystavené doklady (`issued`/`sent`/`reminded`/`paid`) typu
            `invoice`/`credit_note`/`tax_document`; koncepty (`draft`) stejných typů
            jdou zvlášť do `draft_*` (predikce „rozpracováno k vystavení“).
            Proformy, storna a stornované doklady se nezapočítávají vůbec.
          items:
            type: object
            properties:
              currency: { type: string, example: CZK }
              without_vat: { type: number, format: float }
              vat: { type: number, format: float }
              with_vat: { type: number, format: float }
              draft_without_vat: { type: number, format: float }
              draft_vat: { type: number, format: float }
              draft_with_vat: { type: number, format: float }
        invoices:
          type: array
          description: |
            Faktury skupiny — list vrací podmnožinu polí `Invoice` (bez items,
            bankovních polí apod.) plus listová pole `month_bucket` (YYYY-MM),
            `has_work_report` a `project_requires_approval`.
          items: { $ref: '#/components/schemas/Invoice' }

    InvoiceInput:
      type: object
      required: [client_id, items]
      description: |
        Vstup pro POST /invoices (vytvoří draft) i PUT /invoices/{id} (úprava draftu).
        Vystavená/zaplacená/storno faktura nelze upravovat — vrátí 409.
      properties:
        client_id: { type: integer, description: "Musí patřit aktuálnímu supplier-ovi" }
        project_id: { type: [integer, "null"], description: "Pokud uvedeno, musí patřit `client_id`" }
        branding_profile_id: { type: [integer, "null"], description: "Volitelný brandingový profil stejného supplier-a; bez pole se použije výchozí profil klienta." }
        parent_invoice_id: { type: [integer, "null"], description: "Jen při create (credit_note / final-z-proformy). Na UPDATE se ignoruje." }
        invoice_type:
          type: string
          enum: [invoice, proforma, credit_note, cancellation]
          default: invoice
        issue_date: { type: string, format: date, description: "Default = dnes" }
        due_date: { type: string, format: date, description: "Default = issue_date + payment_due_days (z projektu/klienta/supplier)" }
        tax_date: { type: [string, "null"], format: date, description: "DUZP. U proform se ignoruje (uloží se null)." }
        currency_id: { type: integer, description: "FK do /settings/currencies (per-supplier). Default = výchozí měna projektu/klienta/supplier." }
        currency:
          type: string
          example: CZK
          deprecated: true
          description: "Legacy — kód měny (CZK, EUR, …) resolvuje se na `currency_id` v rámci supplier-a. Preferuj `currency_id`."
        language: { type: string, enum: [cs, en], description: "Default = language klienta (cs)" }
        reverse_charge: { type: boolean, description: "Přenesená daňová povinnost. Default = z klienta." }
        prices_include_vat: { type: boolean, default: false, description: "Ceny položek zadané včetně DPH (DPH shora koeficientem)." }
        auto_send_reminders: { type: boolean, default: true, description: "Per-faktura přepínač automatických upomínek (migrace 0088). False = cron tuto fakturu přeskočí." }
        revenue_category_id: { type: [integer, "null"], description: "Kategorie tržby. Když chybí, na create se odvodí z defaultu projektu/klienta." }
        vat_classification_code: { type: [string, "null"], description: "Hlavičkový default klasifikace plnění; auto-default podle sazby/RC/země, pokud prázdné." }
        varsymbol:
          type: [string, "null"]
          maxLength: 20
          description: "Volitelný manuální override automatického číslování. U draftu lze ponechat null — číslo se přidělí při /issue."
        advance_paid_amount: { type: number, format: float, default: 0, description: "Záloha k odečtení od total_with_vat" }
        discount_percent:
          type: number
          format: float
          minimum: 0
          maximum: 100
          default: 0
          description: |
            Procentuální sleva z celé faktury (0–100). Při uložení se materializuje jako
            záporná položka „Sleva X %" rozpadlá na každou sazbu DPH zvlášť (proto se
            správně propíše do DPH rozpisu i daňových výkazů). Slevovou položku
            NEposílej ručně v `items` — generuje se z tohoto pole.
        payment_method:
          type: string
          enum: [bank_transfer, card, cash, other]
          default: bank_transfer
          description: "Způsob úhrady. Pro card/cash se v PDF/emailu nevygeneruje QR ani bankovní spojení."
        exchange_rate: { type: [number, "null"], format: float, description: "Volitelný manuální override ČNB kurzu (CZK / 1 jednotka cizí měny)" }
        note: { type: [string, "null"], description: "Generická poznámka (mapuje se na note_below_items pokud není uvedeno jinak)" }
        note_above_items: { type: [string, "null"] }
        note_below_items: { type: [string, "null"] }
        items:
          type: array
          items: { $ref: '#/components/schemas/InvoiceItemInput' }

    ProjectBilingEmail:
      type: object
      required: [position, email]
      properties:
        position: { type: integer, minimum: 1, maximum: 3 }
        email: { type: string, format: email }
        label: { type: [string, "null"], example: "účetní" }
        usages:
          type: [array, "null"]
          description: "Typy zpráv, pro které se e-mail použije (#86). NULL / prázdné / všechny tři = všechny typy (default, zpětně kompatibilní)."
          items: { type: string, enum: [documents, reminders, approvals] }

    ProjectBrief:
      type: object
      description: Zkrácená reprezentace zakázky (pro výpisy a selecty).
      properties:
        id: { type: integer, format: int64 }
        name: { type: string }
        status: { type: string, enum: [active, paused, closed] }
        currency_id: { type: integer }
        currency: { type: string, example: CZK }
        hourly_rate: { type: number, format: float, example: 1500 }
        payment_due_days: { type: integer, example: 14 }
        payment_due_unit: { type: [string, "null"], enum: [days, month, null], description: "Jednotka splatnosti (NULL = dny)" }
        project_number: { type: [string, "null"] }
        contract_number: { type: [string, "null"] }
        budget_total:   { type: [number, "null"] }
        budget_yearly:  { type: [number, "null"] }
        budget_monthly: { type: [number, "null"] }
        default_revenue_category_id: { type: [integer, "null"], description: "Default kategorie tržby pro faktury zakázky" }
        archived_at: { type: [string, "null"], format: date-time }

    Project:
      allOf:
        - $ref: '#/components/schemas/ProjectBrief'
        - type: object
          properties:
            client_id: { type: integer, format: int64 }
            client_company_name: { type: string }
            client_main_email: { type: [string, "null"], format: email }
            supplier_id: { type: integer }
            note: { type: [string, "null"] }
            requires_work_report_approval: { type: boolean }
            billing_emails:
              type: array
              maxItems: 3
              items: { $ref: '#/components/schemas/ProjectBilingEmail' }
            billing_emails_mode:
              type: string
              enum: [auto, append, replace]
              default: auto
              description: "Jak kombinovat e-maily zakázky s kontakty klienta (#86): auto = dosavadní per-typ chování (doklady/upomínky přidat, schvalování nahradit), append = vždy přidat, replace = vždy nahradit."
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }

    ProjectDetail:
      allOf:
        - $ref: '#/components/schemas/Project'
        - type: object
          properties:
            invoices_count:
              type: integer
              description: Počet faktur navázaných na zakázku.
            revenue_by_month:
              type: array
              items:
                type: object
                properties:
                  month: { type: string, example: "2026-04" }
                  currency: { type: string, example: CZK }
                  total: { type: number, format: float }
            revenue_by_year:
              type: array
              items:
                type: object
                properties:
                  year: { type: integer, example: 2026 }
                  currency: { type: string, example: CZK }
                  total: { type: number, format: float }
                  count: { type: integer }
            unpaid_summary:
              type: array
              items:
                type: object
                properties:
                  currency: { type: string, example: CZK }
                  unpaid_total:  { type: number, format: float }
                  unpaid_count:  { type: integer }
                  overdue_total: { type: number, format: float }
                  overdue_count: { type: integer }

    ProjectInput:
      type: object
      required: [client_id, name]
      properties:
        client_id:
          type: integer
          format: int64
          description: Musí patřit aktuálnímu supplier-ovi.
        name:
          type: string
          maxLength: 190
          example: "Webové stránky 2026"
        status:
          type: string
          enum: [active, paused, closed]
          default: active
        currency_id:
          type: integer
          description: Default = výchozí měna supplier-a.
        currency:
          type: string
          deprecated: true
          description: "Legacy — kód měny (CZK…). Resolvuje se na currency_id. Preferuj currency_id."
        hourly_rate:
          type: number
          format: float
          example: 1500.00
        payment_due_days:
          type: integer
          minimum: 1
          maximum: 365
          example: 14
        payment_due_unit:
          type: [string, "null"]
          enum: [days, month, null]
        default_revenue_category_id:
          type: [integer, "null"]
          description: "Default kategorie tržby (musí patřit tenantovi); zpětně doplní existující faktury zakázky."
        project_number:
          type: [string, "null"]
          maxLength: 50
        contract_number:
          type: [string, "null"]
          maxLength: 50
        budget_total:
          type: [number, "null"]
          format: float
        budget_yearly:
          type: [number, "null"]
          format: float
        budget_monthly:
          type: [number, "null"]
          format: float
        requires_work_report_approval:
          type: boolean
          default: false
          description: |
            Pokud `true`, faktury z této zakázky vyžadují schválení výkazu
            víceprací zákazníkem před vystavením.
        note:
          type: [string, "null"]
        billing_emails:
          type: array
          maxItems: 3
          items: { $ref: '#/components/schemas/ProjectBilingEmail' }
          description: |
            Až 3 fakturační emaily (slot 1/2/3). Kombinace s kontakty/hlavním
            e-mailem klienta se řídí přes `billing_emails_mode` (default `auto`
            = dosavadní chování: u dokladů a upomínek se přidávají, u schvalování
            výkazů nahrazují).
        billing_emails_mode:
          type: string
          enum: [auto, append, replace]
          default: auto
          description: "Jak kombinovat e-maily zakázky s příjemci dle kontaktů klienta (#86)."

    WorkReportItem:
      type: object
      properties:
        id: { type: integer }
        description: { type: string, example: "Analýza požadavků a tech-spec" }
        hours: { type: number, format: float, example: 8.5 }
        rate:  { type: number, format: float, example: 1500 }
        total_amount: { type: number, format: float, example: 12750, description: hours × rate }
        order_index: { type: integer, example: 1 }
        work_date: { type: [string, "null"], format: date, description: "Datum provedení úkonu (volitelné)" }

    WorkReportItemInput:
      type: object
      required: [description, hours, rate]
      properties:
        description: { type: string }
        hours: { type: number, format: float, minimum: 0 }
        rate:  { type: number, format: float, minimum: 0 }
        order_index: { type: integer }
        work_date: { type: string, format: date, description: "Datum provedení úkonu (volitelné, YYYY-MM-DD)" }

    WorkReport:
      type: object
      properties:
        id: { type: integer, format: int64 }
        invoice_id: { type: integer, format: int64 }
        project_id: { type: [integer, "null"], format: int64 }
        title: { type: string, example: "Květen 2026 — vývoj backendu" }
        total_hours: { type: number, format: float }
        total_amount: { type: number, format: float }
        items:
          type: array
          items: { $ref: '#/components/schemas/WorkReportItem' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    WorkReportInput:
      type: object
      required: [title, items]
      properties:
        project_id:
          type: [integer, "null"]
          description: |
            Volitelné — pokud faktura má `project_id`, výkaz se na něj naváže
            automaticky. Pro faktury bez zakázky nechej `null`.
        title:
          type: string
          maxLength: 190
        items:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/WorkReportItemInput' }

    PdfArchiveEntry:
      type: object
      description: |
        Jeden snapshot vystaveného PDF. Soubor se ukládá s timestampem a typem
        akce (issue / send / reissue) v `storage/invoices-pdf/`. Pro stažení
        použij `GET /api/v1/invoices/{id}/pdfs/{archiveId}`.
      properties:
        id: { type: integer, format: int64 }
        invoice_id: { type: integer, format: int64 }
        kind:
          type: string
          enum: [issue, send, reissue, manual]
          description: Co snapshot iniciovalo
        filename: { type: string, example: "F2026-001.pdf" }
        size_bytes: { type: integer }
        sha256: { type: string, example: "9f86d081…" }
        created_at: { type: string, format: date-time }
        created_by_user_id: { type: [integer, "null"] }

    InvoiceAttachment:
      type: object
      properties:
        id: { type: integer, format: int64 }
        invoice_id: { type: integer, format: int64 }
        filename: { type: string, example: "podklad-vykaz.xlsx" }
        mime_type: { type: string, example: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }
        size_bytes: { type: integer, example: 18452 }
        created_at: { type: string, format: date-time }

    AresLookupResult:
      type: object
      description: |
        Odpověď ARES lookup-u. Pokud IČO existuje, `found = true` (implicitní) a
        ostatní pole jsou vyplněná. Pro neexistující IČO vrátíme `{ found: false }`.
      properties:
        found: { type: boolean, example: true }
        source:
          type: string
          enum: [fresh, cache]
          description: Zda data přišla z ARES API nebo z 24h cache
        company_name: { type: string, example: "MyWebdesign.cz s.r.o." }
        ic:  { type: string, example: "27082440" }
        dic: { type: string, example: "CZ27082440" }
        street: { type: string }
        city: { type: string }
        zip: { type: string, example: "301 00" }
        country_iso2: { type: string, example: "CZ" }
        is_vat_payer: { type: boolean }
        date_active: { type: string, format: date, description: "Datum vzniku firmy" }
        legal_form: { type: string, example: "112" }

    ViesLookupResult:
      type: object
      properties:
        valid: { type: boolean }
        vat_id: { type: string, example: "DE123456789" }
        country_code: { type: string, example: "DE" }
        name: { type: [string, "null"] }
        address: { type: [string, "null"] }
        consultation_number:
          type: [string, "null"]
          description: |
            VIES consultation number — důkazní záznam pro daňovou kontrolu, že DIČ
            byl k danému datu platný.
        request_date: { type: string, format: date-time }
        source:
          type: string
          enum: [fresh, cache]

    ActivityLogEntry:
      type: object
      properties:
        id: { type: integer, format: int64 }
        action:
          type: string
          example: "invoice.issued"
          description: "Tečka-separátor: entity.verb (např. invoice.paid, project.archived, auth.login)"
        entity_type: { type: [string, "null"], example: "invoice" }
        entity_id:   { type: [integer, "null"], format: int64 }
        user_id:     { type: [integer, "null"], format: int64, description: "Null = systémová akce (cron)" }
        user_email:  { type: [string, "null"], format: email }
        payload:
          type: [object, "null"]
          additionalProperties: true
          description: Akce-specifické metadata (citlivá pole se redaktují)
        ip:         { type: [string, "null"], example: "203.0.113.42" }
        user_agent: { type: [string, "null"] }
        created_at: { type: string, format: date-time }

    DashboardSummary:
      type: object
      description: |
        Souhrnná data pro Dashboard (homepage) i stránku Grafy (`/stats`).

        **VAT-aware obrat:** Pro plátce DPH (`supplier.is_vat_payer = 1`) se obrat
        ve všech KPI a grafech počítá z `total_without_vat`; pro neplátce z
        `total_with_vat`. Flag `is_vat_payer` v response signalizuje použitou
        variantu.
      properties:
        today: { type: string, format: date }
        year: { type: integer, example: 2026 }
        prev_year: { type: integer, example: 2025 }
        is_vat_payer:
          type: boolean
          description: "Plátce DPH? Určuje použitý sloupec obratu (total_without_vat vs total_with_vat)."
        kpi:
          type: object
          properties:
            per_currency:
              type: array
              description: "Per měna: obrat YTD + loni + YTD loni (do stejného kalendářního dne) + počty faktur/klientů/zakázek pro každý rok."
              items:
                type: object
                properties:
                  currency: { type: string }
                  this_year: { type: number }
                  prev_year: { type: number }
                  prev_year_ytd: { type: number, description: "Obrat loni do stejného dne v roce (pro fair YoY)" }
                  change_pct: { type: [number, "null"], description: "Letošek vs prev_year_ytd v %" }
                  this_year_invoice_count: { type: integer }
                  prev_year_invoice_count: { type: integer }
                  this_year_client_count: { type: integer, description: "DISTINCT klienti v této měně (letos)" }
                  prev_year_client_count: { type: integer }
                  this_year_project_count: { type: integer, description: "DISTINCT zakázky v této měně (letos)" }
                  prev_year_project_count: { type: integer }
            issued_count_ytd: { type: integer }
            overdue_count: { type: integer }
            overdue_per_currency:
              type: array
              items:
                type: object
                properties:
                  currency: { type: string }
                  count: { type: integer }
                  total: { type: number }
            avg_payment_days: { type: [number, "null"] }
            status_counts_ytd:
              type: object
              additionalProperties: { type: integer }
        overdue:
          type: array
          items: { $ref: '#/components/schemas/Invoice' }
        unpaid_upcoming:
          type: array
          items: { $ref: '#/components/schemas/Invoice' }
        draft_invoices:
          type: array
          description: "Rozpracované (status=draft) vystavené faktury pro widget Výkazy práce na Přehledu — firma + zakázka + odkaz na editaci. Max 24, řazeno dle updated_at DESC."
          items:
            type: object
            properties:
              id: { type: integer }
              varsymbol: { type: [string, "null"] }
              invoice_type: { type: string }
              client_id: { type: integer }
              client_company_name: { type: string }
              project_id: { type: [integer, "null"] }
              project_name: { type: [string, "null"], description: "Název zakázky (NULL = bez zakázky)." }
              currency: { type: string }
              issue_date: { type: string, format: date }
              total_with_vat: { type: number }
        top_clients_ytd:
          type: array
          description: "Top 12 klientů YTD per měna."
          items: { $ref: '#/components/schemas/TopClient' }
        top_clients_prev_year:
          type: array
          description: "Top 12 klientů za minulý rok per měna."
          items: { $ref: '#/components/schemas/TopClient' }
        top_clients_12m:
          type: array
          description: "Top 12 klientů za rolling posledních 12 měsíců. Robustní vůči začátku roku (kdy YTD je téměř prázdné)."
          items: { $ref: '#/components/schemas/TopClient' }
        revenue_by_month:
          type: array
          description: "Per měna: rolling 12 měsíců (`months`) + odpovídajících 12 měsíců o rok dříve (`prev_year`)."
          items:
            type: object
            properties:
              currency: { type: string }
              months:
                type: array
                items: { $ref: '#/components/schemas/MonthBucket' }
              prev_year:
                type: array
                items: { $ref: '#/components/schemas/MonthBucket' }
        revenue_by_year:
          type: array
          description: "Obrat po rocích — všechny roky s aktivitou, VAT-aware."
          items:
            type: object
            properties:
              year: { type: integer }
              currency: { type: string }
              total: { type: number }
              invoice_count: { type: integer }
        rolling_12m:
          type: array
          description: "Plovoucí 12měsíční obrat (rolling) per měna + 12 měsíců předtím (pro rolling YoY srovnání). Relevantní pro DPH limit 2 000 000 Kč."
          items:
            type: object
            properties:
              currency: { type: string }
              total: { type: number, description: "Obrat za posledních 12 měsíců (rolling)" }
              prev_period_total: { type: number, description: "Obrat za 12 měsíců předtím" }
        revenue_last_30d:
          type: array
          description: "Live indikátor obratu za posledních 30 dní per měna."
          items:
            type: object
            properties:
              currency: { type: string }
              total: { type: number }
              invoice_count: { type: integer }
        revenue_forecast:
          type: array
          description: |
            Forecast ročního obratu — medián tří nezávislých projekcí (odolný vůči odlehlému signálu).

            **Postup:** medián ze tří signálů —
            (1) run-rate `ytd / uplynulé_dny × dní_v_roce`,
            (2) sezonalita × krátkodobý růst `ytd + prev_year_remainder × growth_short`,
            (3) sezonalita × dlouhodobý trend `ytd + prev_year_remainder × growth_trend`.
            `forecast` je medián, `forecast_low`/`forecast_high` rozpětí (min/max projekcí).
            Bez loňských dat (`prev_year_full = 0`) se použije pouze run-rate.
          items:
            type: object
            properties:
              currency: { type: string }
              ytd: { type: number, description: "Obrat letos Jan 1 – dnes" }
              prev_year_remainder: { type: number, description: "Obrat loni od dneška do konce roku (sezonalita)" }
              prev_year_full: { type: number, description: "Celý loňský rok pro srovnání" }
              growth_short: { type: number, description: "Krátkodobý růst: rolling 12m / předch. 12m, cap [0.5, 2.5]" }
              growth_trend: { type: number, description: "Dlouhodobý trend: CAGR z posledních ≤5 let, cap [0.8, 1.8]" }
              forecast: { type: number, description: "Medián tří projekcí — predikovaný celoroční obrat" }
              forecast_low: { type: number, description: "Nejnižší ze tří projekcí (spodní hranice rozpětí)" }
              forecast_high: { type: number, description: "Nejvyšší ze tří projekcí (horní hranice rozpětí)" }
        cashflow_ytd:
          type: array
          description: "Měsíční cash-flow (skutečné platby `paid_at`) letošek + minulý rok per měna. Doplňuje obrat o reálné inkaso."
          items:
            type: object
            properties:
              currency: { type: string }
              months:
                type: array
                items: { $ref: '#/components/schemas/MonthBucket' }
              prev_year:
                type: array
                items: { $ref: '#/components/schemas/MonthBucket' }
        cashflow_forecast:
          type: array
          description: "Kumulativní cash-flow forecast — co přiteče z neuhrazených faktur v příštích 30/60/90 dnech."
          items:
            type: object
            properties:
              currency: { type: string }
              in_30: { type: number }
              in_60: { type: number }
              in_90: { type: number }
              count_30: { type: integer }
              count_60: { type: integer }
              count_90: { type: integer }
        due_buckets:
          type: array
          description: "Splatnost neuhrazených faktur — kumulativně dnes / tento týden (Po-Ne) / tento měsíc."
          items:
            type: object
            properties:
              currency: { type: string }
              today_count: { type: integer }
              today_total: { type: number }
              week_count: { type: integer }
              week_total: { type: number }
              month_count: { type: integer }
              month_total: { type: number }
        aging_report:
          type: array
          description: "Aging report — stáří pohledávek po splatnosti v bucketech per měna."
          items:
            type: object
            properties:
              currency: { type: string }
              current: { type: number, description: "Ještě před splatností" }
              b1_30: { type: number, description: "1–30 dní po splatnosti" }
              b31_60: { type: number }
              b61_90: { type: number }
              b90_plus: { type: number, description: "Více než 90 dní po splatnosti" }
              current_n: { type: integer }
              b1_30_n: { type: integer }
              b31_60_n: { type: integer }
              b61_90_n: { type: integer }
              b90_plus_n: { type: integer }
        payment_days_histogram:
          type: object
          description: "Distribuce doby úhrady (`paid_at - issue_date`) za posledních 12 měsíců."
          properties:
            buckets:
              type: array
              items:
                type: object
                properties:
                  key: { type: string, enum: ["0-7", "8-14", "15-30", "30+"] }
                  label: { type: string }
                  count: { type: integer }
            total: { type: integer }
            avg_days: { type: [number, "null"] }
        vat_breakdown_12m:
          type: array
          description: |
            Rozpad obratu (base bez DPH) podle sazby DPH — **pouze pro plátce DPH**.
            Pro neplátce vrací prázdné pole. Per měna a `vat_rate_snapshot`.
            Reverse-charge řádky jsou vyznačeny labelem „RC (reverse charge)".
          items:
            type: object
            properties:
              label: { type: string, example: "21 %" }
              base: { type: number, description: "Suma total_without_vat" }
              currency: { type: string }
        invoice_size_histogram:
          type: object
          description: "Histogram velikosti faktur za posledních 12 měsíců. Buckety v CZK ekvivalentu (non-CZK přepočtené přes `exchange_rate`)."
          properties:
            buckets:
              type: array
              items:
                type: object
                properties:
                  key: { type: string, enum: ["0-5", "5-25", "25-100", "100+"] }
                  label: { type: string }
                  count: { type: integer }
                  total_czk: { type: number }
            total: { type: integer }
        active_clients_count:
          type: integer
          description: "Počet nearchivovaných klientů aktuálního dodavatele."
        active_recurring_count:
          type: integer
          description: "Počet aktivních šablon pravidelné fakturace."
        pending_approvals:
          type: object
          properties:
            requested: { type: integer }
            overdue: { type: integer, description: "Bez reakce > 5 dní" }

    TopClient:
      type: object
      properties:
        client_id: { type: integer }
        company_name: { type: string }
        currency: { type: string }
        total: { type: number }
        invoice_count: { type: integer }

    MonthBucket:
      type: object
      properties:
        ym: { type: string, example: "2026-04", description: "Měsíc ve formátu YYYY-MM" }
        total: { type: number }

    ProjectStats:
      type: object
      description: "Statistiky zakázek pro stránku Grafy. VAT-aware (viz `is_vat_payer`)."
      properties:
        this_year: { type: integer, example: 2026 }
        prev_year: { type: integer, example: 2025 }
        primary_currency:
          type: string
          description: "Nejvyužívanější měna ve fakturách — používá se k filtrování per-currency agregací."
        is_vat_payer: { type: boolean }
        top_this_year: { $ref: '#/components/schemas/ProjectStatsBlock' }
        top_prev_year: { $ref: '#/components/schemas/ProjectStatsBlock' }
        top_12m:
          allOf:
            - $ref: '#/components/schemas/ProjectStatsBlock'
            - description: "Top 12 zakázek za rolling posledních 12 měsíců. Robustní vůči začátku roku."
        totals_per_year:
          type: array
          items:
            type: object
            properties:
              year: { type: integer }
              currency: { type: string }
              total: { type: number }
              invoice_count: { type: integer }
        status_breakdown:
          type: array
          items:
            type: object
            properties:
              status: { type: string, enum: [active, paused, closed] }
              count: { type: integer }

    ProjectStatsBlock:
      type: object
      properties:
        top:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              name: { type: string }
              client_company_name: { type: string }
              revenue: { type: number }
              invoice_count: { type: integer }
        others:
          type: object
          description: 'Agregovaná položka "Ostatní" (mimo top 12).'
          properties:
            revenue: { type: number }
            count: { type: integer }

    PurchasePaymentQr:
      type: object
      description: "QR platba + bankovní účet přijaté faktury (GET/POST/PUT na /purchase-invoices/{id}/payment-qr resp. /payment-account)."
      properties:
        ok: { type: boolean }
        qr_data_uri: { type: [string, "null"], description: "QR jako data URI (PNG)." }
        source: { type: [string, "null"], enum: [manual, isdoc, ai_reextract, qr_image, null] }
        amount: { type: number }
        currency: { type: string }
        account:
          type: object
          properties:
            account_number: { type: [string, "null"] }
            bank_code: { type: [string, "null"] }
            iban: { type: [string, "null"] }
            bic: { type: [string, "null"] }
            variable_symbol: { type: [string, "null"] }
        editable: { type: boolean }
        needs_account: { type: boolean }
        can_extract: { type: [boolean, "null"] }

    WorkReportLinkStatus:
      type: object
      description: "Stav veřejného sledovacího odkazu na výkaz (klient/zakázka)."
      properties:
        exists: { type: boolean }
        url: { type: [string, "null"] }
        last_sent_at: { type: [string, "null"], format: date-time }
        last_viewed_at: { type: [string, "null"], format: date-time }

    WorkReportLinkRecipients:
      type: object
      description: "Navržení příjemci pro odeslání odkazu (předvyplnění modalu)."
      properties:
        to: { type: array, items: { type: string } }
        cc: { type: array, items: { type: string } }
        bcc: { type: array, items: { type: string } }
        resolved: { type: object }

    SupplierBankAccount:
      type: object
      required: [id, account_number, kind, is_active, source]
      properties:
        id: { type: integer, format: int64 }
        label: { type: [string, "null"] }
        account_number: { type: [string, "null"] }
        bank_code: { type: [string, "null"], pattern: '^[0-9]{4}$' }
        iban: { type: [string, "null"] }
        currency: { type: [string, "null"], minLength: 3, maxLength: 3 }
        kind: { type: string, enum: [current, savings, term_deposit] }
        analytic_suffix: { type: [string, "null"] }
        is_active: { type: boolean }
        source: { type: string, enum: [currencies, statement, manual] }
        currency_id: { type: [integer, "null"] }

    BankTransferCandidate:
      type: object
      required: [tx_id, statement_id, posted_at, amount, direction]
      properties:
        tx_id: { type: integer, format: int64 }
        statement_id: { type: integer, format: int64 }
        posted_at: { type: string, format: date }
        amount: { type: number }
        direction: { type: string, enum: [out, in] }

    BankTransferCandidatesError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, data]
          properties:
            code: { type: string, enum: [bank_transfer_candidates] }
            message: { type: string }
            data:
              type: object
              required: [candidates]
              properties:
                candidates:
                  type: array
                  items: { $ref: '#/components/schemas/BankTransferCandidate' }

    BankTransferPostingInfo:
      type: object
      required: [direction, own_account_label, pair]
      properties:
        direction: { type: string, enum: [out, in] }
        own_account_label: { type: [string, "null"] }
        pair:
          type: [object, "null"]
          required: [tx_id, statement_id, posted_at, entry_id]
          properties:
            tx_id: { type: integer, format: int64 }
            statement_id: { type: integer, format: int64 }
            posted_at: { type: string, format: date }
            entry_id: { type: [integer, "null"], format: int64 }

    BankMatchSuggestionCandidate:
      type: object
      required: [type, score, signals, flags, display]
      properties:
        type: { type: string, enum: [invoice, purchase_invoice, split] }
        invoice_id: { type: [integer, "null"], format: int64 }
        invoice_ids:
          type: [array, "null"]
          items: { type: integer, format: int64 }
        purchase_invoice_id: { type: [integer, "null"], format: int64 }
        score: { type: number, minimum: 0, maximum: 1 }
        signals:
          type: object
          additionalProperties: { type: number }
        flags:
          type: array
          items: { type: string, enum: [overpayment, fee_gap, vs_typo, currency_mismatch, proforma] }
        fee_amount: { type: [number, "null"] }
        overpayment_amount: { type: [number, "null"] }
        display:
          type: object
          required: [amount, currency, paid]
          properties:
            ref: { type: [string, "null"] }
            party: { type: [string, "null"] }
            amount: { type: number }
            currency: { type: string }
            due_date: { type: [string, "null"], format: date }
            paid: { type: boolean }

    BankMatchSuggestion:
      type: object
      required: [id, bank_transaction_id, kind, reason, top_score, deterministic_core, status, candidates, created_at]
      properties:
        id: { type: integer, format: int64 }
        bank_transaction_id: { type: integer, format: int64 }
        kind: { type: string, enum: [single, split, vs_typo, overpayment, fee_gap] }
        reason: { type: string }
        top_score: { type: number, minimum: 0, maximum: 1 }
        margin: { type: [number, "null"] }
        deterministic_core: { type: boolean }
        status: { type: string, enum: [pending, accepted, rejected, superseded, auto_applied] }
        candidates:
          type: array
          items: { $ref: '#/components/schemas/BankMatchSuggestionCandidate' }
        created_at: { type: string, format: date-time }

    BankStatement:
      type: object
      description: "Položka seznamu výpisů (GET /bank-statements). Bankovní spojení dodavatele je per-měna na /settings/currencies, ne tady."
      properties:
        id: { type: integer, format: int64 }
        source: { type: string, enum: [gpc, pdf, email_notice, idoklad], description: "Původ výpisu: gpc/pdf = oficiální bankovní výpis, email_notice/idoklad = virtuální měsíční výpis." }
        file_name: { type: string, example: "vypis_2026_05.gpc" }
        account_number: { type: string, example: "1234567890/0100" }
        bank_code: { type: [string, "null"], example: "0100", description: "Kód banky účtu — autoritativně z konfigurovaného účtu (currencies.bank_code), fallback z výpisu. GPC výpisy ho samy nenesou." }
        currency: { type: [string, "null"], example: EUR, description: "ISO 4217. Odvozeno z transakcí výpisu nebo z currencies.account_number. NULL u výpisů importovaných před v4.1.0." }
        statement_date: { type: string, format: date }
        statement_number: { type: [string, "null"] }
        prev_balance: { type: number, format: float }
        curr_balance: { type: number, format: float }
        transaction_count: { type: integer }
        matched_count: { type: integer }
        ignored_count: { type: integer, description: "Počet sekundárních pohybů převzatých autoritativním výpisem a označených jako ignorované." }
        unposted_count: { type: integer, description: "Počet skutečných pohybů bez aktivního účetního zápisu; ignorované položky se nepočítají." }
        imported_at: { type: string, format: date-time }
        has_file: { type: boolean, description: "True pokud server má uložený originál výpisu — ke stažení přes /bank-statements/{id}/download." }
        has_pdf: { type: boolean, description: "True pokud je nahrán doprovodný PDF výpis." }
        pdf_name: { type: [string, "null"] }
        account_label: { type: [string, "null"], description: "Odvozeno z currencies.label" }

    BankStatementDetail:
      allOf:
        - $ref: '#/components/schemas/BankStatement'
        - type: object
          properties:
            file_hash: { type: string, description: "SHA-256 originálního souboru" }
            credit_total: { type: [number, "null"], format: float }
            debit_total: { type: [number, "null"], format: float }
            imported_by: { type: [integer, "null"], description: "User ID importujícího" }
            transactions:
              type: array
              items: { $ref: '#/components/schemas/BankTransaction' }

    BankTransaction:
      type: object
      properties:
        id: { type: integer, format: int64 }
        source: { type: string, enum: [statement, email_notice, idoklad] }
        statement_id: { type: integer, format: int64 }
        posted_at: { type: string, format: date }
        amount: { type: number, format: float, example: 12500.00 }
        balance: { type: [number, "null"], format: float, description: "Disponibilní zůstatek účtu z e-mailového avíza (Creditas/Fio/RB); u transakcí z GPC výpisu null." }
        currency: { type: [string, "null"], example: CZK }
        variable_symbol: { type: [string, "null"] }
        constant_symbol: { type: [string, "null"] }
        specific_symbol: { type: [string, "null"] }
        counterparty_account: { type: [string, "null"] }
        counterparty_bank: { type: [string, "null"] }
        counterparty_name: { type: [string, "null"] }
        description: { type: [string, "null"] }
        bank_ref: { type: [string, "null"], description: "Bankovní referenční ID transakce" }
        match_status:
          type: string
          enum: [unmatched, auto_exact, auto_partial, manual, ignored]
          description: "Stav párování. auto_* = automatika, manual = ruční přiřazení."
        matched_invoice_id: { type: [integer, "null"], format: int64, description: "Spárovaná VYDANÁ faktura" }
        matched_purchase_invoice_id: { type: [integer, "null"], format: int64, description: "Spárovaná PŘIJATÁ faktura (z payment_matches)" }
        matched_varsymbol: { type: [string, "null"] }
        matched_invoice_amount: { type: [number, "null"], format: float }
        client_id: { type: [integer, "null"] }
        matched_client_name: { type: [string, "null"] }
        matched_at: { type: [string, "null"], format: date-time }
        matched_by: { type: [integer, "null"], description: "User ID (u ručního párování)" }
        posting:
          type: [object, "null"]
          description: Stav účetního návrhu nebo zápisu bankovní transakce.
          properties:
            suggestion_source: { type: [string, "null"], enum: [rule, learned, payment_match, transfer, null] }
            transfer: { $ref: '#/components/schemas/BankTransferPostingInfo' }

    SupplierUpdate:
      type: object
      description: |
        Částečný update dodavatele — pošli jen pole ke změně. Zrcadlí
        mass-assign whitelist `PUT /api/v1/settings/supplier`; níže veřejný
        subset (fakturační údaje, defaulty, číslování, branding). `logo_path`
        a `signature_path` přes tento endpoint změnit NELZE (bezpečnost).
      properties:
        company_name: { type: string }
        display_name: { type: [string, "null"], description: "Obchodní/značkové jméno v hlavičce PDF a e-mailů (fallback = company_name)" }
        street: { type: string }
        city: { type: string }
        zip: { type: string }
        country_id: { type: integer }
        ic: { type: [string, "null"] }
        dic: { type: [string, "null"] }
        is_vat_payer: { type: boolean }
        is_identified: { type: boolean }
        data_box_id: { type: [string, "null"] }
        data_box_type: { type: [string, "null"], enum: [FO, PFO, PO, OVM, null], description: "Druh datové schránky v ISDS; nesouvisí s typem poplatníka." }
        oss_enabled: { type: boolean }
        oss_valid_from: { type: [string, "null"], format: date }
        oss_valid_to: { type: [string, "null"], format: date }
        oss_identification_country: { type: [string, "null"], minLength: 2, maxLength: 2 }
        oss_return_currency: { type: string, minLength: 3, maxLength: 3, default: EUR }
        email: { type: [string, "null"], format: email }
        phone: { type: [string, "null"] }
        web: { type: [string, "null"] }
        tagline: { type: [string, "null"], description: "Podtitulek v patičce PDF a e-mailů" }
        default_currency_id: { type: integer }
        default_payment_due_days: { type: integer }
        default_payment_due_unit: { type: string, enum: [days, month] }
        default_prices_include_vat: { type: boolean }
        invoice_number_format:
          type: [string, "null"]
          maxLength: 60
          example: "{YY}{MM}{CCC}"
          description: |
            Template čísla faktury: `{YYYY}`/`{YY}` rok, `{MM}` měsíc, `{C..C}`
            counter s paddingem dle počtu C (např. `{CCC}` → 001). Musí
            obsahovat counter placeholder. Prázdný string / null = fallback
            na globální cfg.
        proforma_number_format: { type: [string, "null"], maxLength: 60, example: "9{YY}{MM}{CCC}" }
        credit_note_number_format: { type: [string, "null"], maxLength: 60, example: "7{YY}{MM}{CCC}" }
        purchase_invoice_number_format: { type: [string, "null"], maxLength: 60, description: "Interní číslování přijatých faktur" }
        invoice_number_period:
          type: string
          enum: [year, month, none]
          description: "Reset counteru: ročně / měsíčně / nikdy (jedna průběžná řada). Společné pro všechny typy dokladů."
        email_branding_enabled: { type: boolean, description: "Původní branding dodavatele používaný při vypnutých brandingových profilech" }
        email_accent_color: { type: string, example: "#3B2D83", description: "Hex #RRGGBB; akcentní barva e-mailů a PDF faktur" }
        pdf_logo_show_name: { type: boolean, description: "Zobrazit vedle loga v PDF i název dodavatele" }
        branding_profiles_enabled: { type: boolean, description: "Zapíná volbu brandingových profilů; výchozí hodnota je false." }
        default_branding_profile_id: { type: [integer, "null"], readOnly: true }
        auto_send_reminders: { type: boolean }
        reminder_days_after_due: { type: integer, minimum: 1, maximum: 365 }

    SupplierFull:
      type: object
      description: |
        Kompletní data dodavatele (per-supplier konfigurace). Vrací `SELECT s.*`
        + odvozená pole. Bankovní účty NEJSOU součástí — jsou per-měna
        na `GET /api/settings/currencies`. (Pozn.: response obsahuje i další
        sloupce mimo tento výčet — branding, integrace, payment-thanks ap.)
      properties:
        id: { type: integer }
        company_name: { type: string }
        display_name: { type: [string, "null"] }
        street: { type: string }
        city: { type: string }
        zip: { type: string }
        country_id: { type: integer }
        country_iso: { type: string, description: "Odvozený ISO2 kód země" }
        ic: { type: [string, "null"] }
        dic: { type: [string, "null"] }
        is_vat_payer: { type: boolean }
        is_identified: { type: boolean, description: "Identifikovaná osoba (§ 6g–6l ZDPH, issue #94) — neplátce v tuzemsku s přeshraničními povinnostmi (RC faktury do EU, SHV, samovyměření bez odpočtu). Nelze kombinovat s is_vat_payer=true." }
        data_box_id: { type: [string, "null"] }
        data_box_type: { type: [string, "null"], enum: [FO, PFO, PO, OVM, null], description: "Druh datové schránky v ISDS; nesouvisí s typem poplatníka." }
        oss_enabled: { type: boolean }
        oss_valid_from: { type: [string, "null"], format: date }
        oss_valid_to: { type: [string, "null"], format: date }
        oss_identification_country: { type: [string, "null"], minLength: 2, maxLength: 2 }
        oss_return_currency: { type: string, minLength: 3, maxLength: 3, default: EUR }
        accounting_mode:
          type: string
          enum: [tax_evidence, double_entry]
          default: tax_evidence
          description: "Režim účetnictví firmy (Epic F0, migrace 1001): 'tax_evidence' = daňová evidence (výchozí), 'double_entry' = podvojné účetnictví. Lze měnit přes PUT /settings/supplier (admin)."
        accounting_mode_effective_from:
          type: string
          format: date
          description: "Účinnost změny režimu; musí připadat na 1. ledna. Přechod fyzické osoby z účetnictví na daňovou evidenci je povolen až po 5 po sobě jdoucích účetních obdobích (§ 4 odst. 7 ZoÚ)."
        commercial_register: { type: [string, "null"], description: "Spisová značka u rejstříkového soudu (doplňuje ARES enricher)" }
        email: { type: [string, "null"], format: email }
        phone: { type: [string, "null"] }
        web: { type: [string, "null"] }
        tagline: { type: [string, "null"] }
        default_currency_id: { type: integer }
        default_currency: { type: string, description: "Odvozený kód výchozí měny" }
        default_payment_due_days: { type: integer }
        default_payment_due_unit: { type: string, enum: [days, month] }
        default_hourly_rate: { type: number, format: float }
        default_prices_include_vat: { type: boolean, description: "Default pro nové doklady (migrace 0083)" }
        stock_enabled: { type: boolean, description: "Feature-flag Epic SKLAD — zapíná skladovou evidenci pro tohoto dodavatele." }
        stock_method:
          type: string
          enum: [B, A]
          default: B
          description: "Způsob účtování zásob (ČÚS 015). V1 vynuceně 'B' (service); 'A' schema-ready pro v2."
        stock_auto_issue: { type: boolean, default: true, description: "Automatická výdejka ze skladu při vystavení faktury (item se stock_item_id)." }
        invoice_number_format:
          type: [string, "null"]
          example: "{YY}{MM}{CCC}"
          description: "{YYYY}/{YY}, {MM}, {C..C} placeholders (viz SupplierUpdate). Per-supplier; null = globální cfg."
        proforma_number_format: { type: [string, "null"] }
        credit_note_number_format: { type: [string, "null"] }
        purchase_invoice_number_format: { type: [string, "null"] }
        invoice_number_period: { type: string, enum: [year, month, none], description: "Reset counteru číselných řad" }
        taxpayer_type: { type: [string, "null"], enum: [fo, po, null], description: "Fyzická / právnická osoba (EPO výkazy)" }
        vat_period: { type: [string, "null"], enum: [monthly, quarterly, null] }
        financial_office_code: { type: [string, "null"], description: "Kód FÚ pro EPO (= CRPDPH cisloFu)" }
        flat_tax_band: { type: string, enum: [none, band1, band2, band3], description: "Pásmo paušální daně" }
        email_branding_enabled: { type: boolean }
        email_accent_color: { type: string, example: "#3B2D83" }
        has_email_logo: { type: boolean }
        auto_send_reminders: { type: boolean }
        reminder_days_after_due: { type: integer, minimum: 1, maximum: 365, default: 3, description: "Per-dodavatel práh: po kolika dnech po splatnosti poslat první automatickou upomínku (migrace 0089). CLI --days ho přebije." }
        self_copy:
          type: [object, "null"]
          description: |
            Kopie odchozích e-mailů na e-mail dodavatele (migrace 0102).
            Klíče = typy zpráv, hodnoty off|cc|bcc. Chybějící klíč nebo null
            = fallback na globální cfg flagy (cc_supplier_on_*). Klíč
            `approvals` platí pro žádost o schválení i schvalovací upomínku.
          properties:
            documents: { type: string, enum: ["off", cc, bcc] }
            reminders: { type: string, enum: ["off", cc, bcc] }
            approvals: { type: string, enum: ["off", cc, bcc] }
          example: { documents: cc, approvals: bcc }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ProjectsPagination:
      type: object
      properties:
        total:    { type: integer, example: 423 }
        page:     { type: integer, example: 1 }
        per_page: { type: integer, example: 50 }
        pages:    { type: integer, example: 9 }

    InvoicePayment:
      type: object
      description: "Evidovaná platba faktury / zálohové faktury (#89). N:1 — jedna faktura může mít víc plateb (splátky, více převodů, avíza)."
      properties:
        id: { type: integer, format: int64 }
        invoice_id: { type: integer, format: int64 }
        paid_on: { type: string, format: date }
        amount: { type: number, format: float, description: "Částka v měně faktury (> 0)" }
        currency: { type: string, example: CZK, description: "Denormalizovaný kód měny faktury v okamžiku platby" }
        variable_symbol: { type: [string, "null"] }
        bank_reference: { type: [string, "null"] }
        note: { type: [string, "null"] }
        source:
          type: string
          enum: [manual, mark_paid, bank, legacy]
          description: "manual = ruční záznam, mark_paid = zkratka „označit zaplaceno“, bank = bankovní párování (GPC i e-mailové avízo), legacy = backfill z dob před evidencí plateb"
        bank_transaction_id: { type: [integer, "null"], format: int64, description: "Vazba na bankovní transakci (taková platba se maže přes unmatch ve výpisu)" }
        bank_statement_id: { type: [integer, "null"], format: int64 }
        tax_document_invoice_id: { type: [integer, "null"], format: int64, description: "Daňový doklad k přijaté platbě vystavený k této platbě (jen u záloh)" }
        tax_document_varsymbol: { type: [string, "null"] }
        tax_document_status: { type: [string, "null"], enum: [draft, issued, sent, reminded, paid, cancelled, null] }
        created_by: { type: [integer, "null"] }
        created_at: { type: string, format: date-time }

    InvoiceRelatedBankTransaction:
      type: object
      description: "Bankovní operace přímo spárovaná s fakturou. Bez odpovídajícího InvoicePayment se nezapočítává do paid_total."
      properties:
        id: { type: integer, format: int64 }
        statement_id: { type: integer, format: int64 }
        statement_source: { type: string, enum: [gpc, pdf, email_notice, idoklad] }
        posted_at: { type: string, format: date }
        amount: { type: number, format: float }
        currency: { type: [string, "null"] }
        variable_symbol: { type: [string, "null"] }
        constant_symbol: { type: [string, "null"] }
        specific_symbol: { type: [string, "null"] }
        counterparty_account: { type: [string, "null"] }
        counterparty_bank: { type: [string, "null"] }
        counterparty_name: { type: [string, "null"] }
        description: { type: [string, "null"] }
        bank_ref: { type: [string, "null"] }
        match_status: { type: string, enum: [unmatched, auto_exact, auto_partial, manual, ignored] }

    InvoiceItem:
      type: object
      properties:
        id: { type: integer }
        invoice_id: { type: integer }
        description: { type: string }
        quantity: { type: number, format: float, description: "Může být záporné pro slevový řádek. Nesmí být 0." }
        unit: { type: string, example: ks }
        unit_price_without_vat: { type: number, format: float, description: "Jednotková cena bez DPH. Může být záporná pro slevový řádek." }
        vat_rate_id: { type: integer, description: "FK do /codebooks/vat-rates (kontextově k supplierovi)" }
        vat_rate_snapshot:
          type: number
          format: float
          example: 21
          description: "Snapshot procentní hodnoty DPH v okamžiku přidání položky (drží se i po případné změně sazby v číselníku)"
        vat_code: { type: [string, "null"], example: "STD", description: "Kód sazby ze snapshot-u (STD / RED1 / RED2 / ZERO / FREE)" }
        vat_label_cs: { type: [string, "null"], description: "Lokalizovaný název sazby (CS)" }
        vat_label_en: { type: [string, "null"], description: "Lokalizovaný název sazby (EN)" }
        vat_classification_code: { type: [string, "null"], example: "3", description: "Kód klasifikace plnění pro DPH přiznání / Kontrolní hlášení (default podle sazby + reverse charge + země klienta). U neplátce DPH bez významu." }
        total_without_vat: { type: number, format: float }
        total_vat: { type: number, format: float }
        total_with_vat: { type: number, format: float }
        item_kind:
          type: string
          enum: [standard, discount]
          default: standard
          description: "`discount` = systémově vygenerovaný záporný řádek „Sleva X %\" z `discount_percent` faktury (po sazbách DPH). Needituj ho ručně."
        order_index: { type: integer }
        linked_work_report_id: { type: [integer, "null"] }
        stock_item_id:
          type: [integer, "null"]
          description: "FK na skladovou kartu (Epic SKLAD) — řídí automatickou výdejku při vystavení faktury (supplier.stock_auto_issue)."
        warehouse_id:
          type: [integer, "null"]
          description: "FK na sklad (/api/stock/warehouses), ze kterého se položka vydává. Relevantní jen když je vyplněn stock_item_id."
        oss_applicable: { type: boolean }
        oss_consumer_country: { type: [string, "null"], minLength: 2, maxLength: 2 }
        oss_rate_type: { type: [string, "null"], enum: [standard, reduced, second_reduced, parking, null] }
        oss_supply_type: { type: [string, "null"], enum: [goods, services, null] }
        oss_exchange_rate: { type: [number, "null"] }
        oss_exchange_rate_date: { type: [string, "null"], format: date }
        oss_taxable_amount_return: { type: [number, "null"] }
        oss_vat_amount_return: { type: [number, "null"] }
        oss_original_period: { type: [string, "null"], pattern: '^[0-9]{4}Q[1-4]$' }

    InvoiceItemInput:
      type: object
      required: [description, quantity, unit_price_without_vat, vat_rate_id]
      properties:
        description: { type: string }
        quantity: { type: number, format: float, description: "Nesmí být 0. Může být záporné pro slevový řádek, ale ne současně se zápornou cenou." }
        unit: { type: string, default: "ks" }
        unit_price_without_vat: { type: number, format: float, description: "Jednotková cena BEZ DPH. Může být záporná pro slevový řádek, ale ne současně se záporným množstvím." }
        vat_rate_id: { type: integer, description: "FK do /codebooks/vat-rates (kontextově k supplierovi)" }
        order_index: { type: integer, description: "Pořadí v rámci faktury. Default = pozice v poli." }
        vat_classification_code: { type: [string, "null"], description: "Volitelný kód klasifikace plnění (DPH přiznání / KH). Když nevyplněno, server dopočítá default podle sazby + reverse charge + země klienta." }
        stock_item_id:
          type: [integer, "null"]
          description: "Vazba na skladovou kartu (Epic SKLAD). Když vyplněno, doporučeno vyplnit i warehouse_id."
        warehouse_id:
          type: [integer, "null"]
          description: "Sklad, ze kterého se má vydat (jen ve spojení se stock_item_id)."
        oss_applicable: { type: boolean, default: false }
        oss_consumer_country: { type: [string, "null"], minLength: 2, maxLength: 2 }
        oss_rate_type: { type: [string, "null"], enum: [standard, reduced, second_reduced, parking, null] }
        oss_supply_type: { type: [string, "null"], enum: [goods, services, null] }
        oss_exchange_rate: { type: [number, "null"] }
        oss_exchange_rate_date: { type: [string, "null"], format: date }
        oss_taxable_amount_return: { type: [number, "null"] }
        oss_vat_amount_return: { type: [number, "null"] }
        oss_original_period: { type: [string, "null"], pattern: '^[0-9]{4}Q[1-4]$' }

    RecurringTemplate:
      type: object
      description: "Šablona pravidelné fakturace. Cron `bin/cron-generate-recurring-invoices.php` generuje faktury dle periodicity."
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        client_id: { type: integer }
        client_company_name: { type: string }
        project_id: { type: [integer, "null"] }
        project_name: { type: [string, "null"] }
        name: { type: string, description: "Display name pro admin (např. \"Hosting Acme s.r.o.\")" }
        frequency:
          type: string
          enum: [monthly, quarterly, semi_annually, annually]
        day_of_month:
          type: [integer, "null"]
          description: "1–28; pokud null a end_of_month=false, odvodí se z anchor_date"
        end_of_month:
          type: boolean
          description: "true = poslední den měsíce (28/29/30/31 dynamicky); přebíjí day_of_month"
        anchor_date: { type: string, format: date }
        end_date:    { type: [string, "null"], format: date }
        next_run_date: { type: string, format: date }
        last_run_date: { type: [string, "null"], format: date }
        invoice_type:
          type: string
          enum: [invoice, proforma]
        currency_id: { type: integer }
        currency: { type: string, example: CZK }
        language: { type: string, enum: [cs, en] }
        payment_method:
          type: string
          enum: [bank_transfer, card, cash, other]
        reverse_charge: { type: boolean }
        payment_due_days: { type: integer }
        discount_percent: { type: number, format: float, description: "Procentuální sleva (0–100) zděděná na každou vygenerovanou fakturu." }
        revenue_category_id:
          type: [integer, "null"]
          description: "Pevná kategorie tržby pro generované faktury (#119) — přebíjí fallback výchozí kategorie zakázky > zákazníka. NULL = fallback. Na fakturu se ukládá jako snapshot při generování."
        revenue_category_label: { type: [string, "null"], description: "Label kategorie (jen detail šablony)." }
        revenue_category_code: { type: [string, "null"], description: "Kód kategorie (jen detail šablony)." }
        draft_open_mode:
          type: string
          enum: [at_issue, period_start]
          description: "at_issue = koncept vznikne až v den vystavení; period_start = koncept se otevře na začátku období (průběžný výkaz víceprací) a vystaví se na konci."
        reminder_days_before: { type: integer, description: "0–14; připomenutí konceptu N dní před vystavením (0 = vypnuto; jen pro period_start)." }
        note_above_items: { type: [string, "null"] }
        note_below_items: { type: [string, "null"] }
        increment_month_in_descriptions:
          type: boolean
          description: "Posun M/YYYY v popiscích položek (např. „Hosting 3/2026\" → „Hosting 4/2026\"). Pro frequency != monthly se posouvá o odpovídající počet měsíců."
        auto_issue:
          type: boolean
          description: "true = cron rovnou vystaví (varsymbol + snapshot, status=issued); false = nechá draft"
        auto_send_email:
          type: boolean
          description: "true = po vystavení rovnou odešle klientovi; vyžaduje auto_issue=true"
        status:
          type: string
          enum: [active, paused, expired]
        invoices_generated_count: { type: integer }
        last_error: { type: [string, "null"], description: "Poslední chyba generování (cron) — UI ji ukáže jako banner u šablony; vyčistí se po úspěšném ručním běhu." }
        last_error_at: { type: [string, "null"], format: date-time }
        created_at: { type: string }
        updated_at: { type: string }
        items:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              description: { type: string }
              quantity: { type: number, format: float, description: "Může být záporné pro slevový řádek. Nesmí být 0." }
              unit: { type: string }
              unit_price_without_vat: { type: number, format: float, description: "Může být záporná pro slevový řádek." }
              vat_rate_id: { type: integer }
              vat_rate_percent: { type: number, format: float }
              order_index: { type: integer }

    RecurringTemplateInput:
      type: object
      required: [client_id, name, frequency, anchor_date, currency_id, items]
      properties:
        client_id: { type: integer }
        project_id: { type: [integer, "null"] }
        name: { type: string, maxLength: 200 }
        frequency:
          type: string
          enum: [monthly, quarterly, semi_annually, annually]
        day_of_month:
          type: [integer, "null"]
          minimum: 1
          maximum: 28
        end_of_month: { type: boolean, default: false }
        anchor_date: { type: string, format: date }
        end_date:    { type: [string, "null"], format: date }
        invoice_type:
          type: string
          enum: [invoice, proforma]
          default: invoice
        currency_id: { type: integer }
        language: { type: string, enum: [cs, en], default: cs }
        payment_method:
          type: string
          enum: [bank_transfer, card, cash, other]
          default: bank_transfer
        reverse_charge: { type: boolean, default: false }
        payment_due_days: { type: integer, default: 14 }
        discount_percent:
          type: number
          format: float
          minimum: 0
          maximum: 100
          default: 0
          description: "Procentuální sleva z celé faktury (0–100); zdědí ji každá vygenerovaná faktura (materializuje se jako záporná položka „Sleva X %\")."
        revenue_category_id:
          type: [integer, "null"]
          description: "Pevná kategorie tržby pro generované faktury (#119). Musí existovat a patřit dodavateli šablony. NULL/vynecháno = fallback výchozí kategorie zakázky > zákazníka při generování."
        draft_open_mode:
          type: string
          enum: [at_issue, period_start]
          default: at_issue
          description: "period_start (koncept na začátku období → vystavení na konci) vyžaduje auto_issue=true."
        tax_date_mode:
          type: string
          enum: [same_as_issue, previous_month_last_day]
          default: same_as_issue
          description: "DUZP vygenerované faktury: stejné jako datum vystavení, nebo poslední den předchozího měsíce."
        reminder_days_before:
          type: integer
          minimum: 0
          maximum: 14
          default: 0
          description: "Připomenutí konceptu N dní před vystavením (jen draft_open_mode=period_start; 0 = vypnuto)."
        note_above_items: { type: [string, "null"] }
        note_below_items: { type: [string, "null"] }
        increment_month_in_descriptions: { type: boolean, default: true }
        auto_issue: { type: boolean, default: true }
        auto_send_email: { type: boolean, default: true }
        items:
          type: array
          items:
            type: object
            required: [description, quantity, unit_price_without_vat, vat_rate_id]
            properties:
              description: { type: string }
              quantity: { type: number, format: float, description: "Nesmí být 0. Může být záporné pro slevový řádek, ale ne současně se zápornou cenou." }
              unit: { type: string, default: ks }
              unit_price_without_vat: { type: number, format: float, description: "Může být záporná pro slevový řádek, ale ne současně se záporným množstvím." }
              vat_rate_id: { type: integer }
              order_index: { type: integer }

    # ========================================================================
    # Purchase invoices (přijaté faktury) — fáze 1 integrace forku
    # ========================================================================
    PurchaseInvoiceListItem:
      type: object
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        vendor_id: { type: integer }
        varsymbol: { type: [string, "null"], description: "Naše interní číslo PF-YYYYMM-NNNN" }
        vendor_invoice_number: { type: string }
        document_kind: { type: string, enum: [invoice, receipt, credit_note, advance] }
        issue_date: { type: string, format: date }
        tax_date: { type: [string, "null"], format: date, description: "DUZP" }
        due_date: { type: string, format: date }
        received_at: { type: string, format: date }
        currency_id: { type: integer }
        currency: { type: string }
        total_without_vat: { type: number }
        total_vat: { type: number }
        total_with_vat: { type: number }
        advance_paid_amount: { type: number }
        amount_to_pay: { type: number, description: "Generated column = total_with_vat - advance_paid_amount" }
        exchange_rate: { type: [number, "null"] }
        exchange_rate_date: { type: [string, "null"], format: date }
        status: { type: string, enum: [draft, received, booked, paid, cancelled] }
        booked_at: { type: [string, "null"], format: date-time }
        paid_at: { type: [string, "null"], format: date }
        cancelled_at: { type: [string, "null"], format: date-time }
        vat_deduction: { type: string, enum: [full, none, proportional] }
        vat_deduction_percent: { type: number, format: float }
        tax_deductible: { type: boolean }
        extraction_warning: { type: [string, "null"] }
        currency_symbol: { type: string }
        vendor_company_name: { type: string }
        vendor_ic: { type: [string, "null"] }
        month_bucket: { type: string, example: "2026-05" }
        has_small_asset:
          type: boolean
          description: "§DM — aspoň jedna položka dokladu má expense_kind='small_asset' (drobný majetek). Jen v seznamu, v detailu se odvozuje z items[]."

    PurchaseInvoiceItem:
      type: object
      properties:
        id: { type: integer }
        purchase_invoice_id: { type: integer }
        description: { type: string }
        quantity: { type: number }
        unit: { type: string }
        unit_price_without_vat: { type: number }
        vat_rate_id: { type: integer }
        vat_rate_snapshot: { type: number }
        total_without_vat: { type: number }
        total_vat: { type: number }
        total_with_vat: { type: number }
        order_index: { type: integer }
        vat_code: { type: [string, "null"], description: "Kód sazby ze snapshot-u (STD / RED1 / …)" }
        vat_label_cs: { type: [string, "null"] }
        vat_label_en: { type: [string, "null"] }
        vat_classification_code:
          type: [string, "null"]
          description: "MFČR kód pro DPH/KH (připraveno pro fázi 6)"
        expense_kind:
          type: [string, "null"]
          enum: [service, material, small_asset, fixed_asset, null]
          description: |
            §DM — druh nákladu na řádku. Řídí nákladový účet i evidenci majetku:
            service → 518, material (vč. PHM) → 501, small_asset → 501 + karta drobného
            majetku, fixed_asset → 042 + odpisy. NULL = neurčeno (chová se jako dosud → 518).
            Autoritativní vůči `is_fixed_asset`, který se z něj odvozuje.
            Návrhy vrací `GET /accounting/purchase-invoices/{id}/expense-suggestions`;
            zapisuje je vždy až uživatel, nikdy klasifikátor ani AI import.
        is_fixed_asset:
          type: boolean
          default: false
          description: |
            Řádek je za dlouhodobý majetek (§ 4 odst. 4 písm. c — vozidlo, stroj, ...).
            Per-řádek override umožňuje mixed doklady (pár položek majetek, zbytek běžné).
            Pokud je nastaveno na řádku, vyhrává nad `PurchaseInvoice.is_fixed_asset` (header).
            Hodnota se na DPHDP3 navíc uvede na ř. 47 (`nar_maj`).
        stock_item_id:
          type: [integer, "null"]
          description: "FK na skladovou kartu (Epic SKLAD). NULL = volný text/služba, bez vazby na sklad."
        stock_sku: { type: [string, "null"], description: "SKU navázané skladové karty (jen pro zobrazení, denormalizováno)" }
        stock_name: { type: [string, "null"], description: "Název navázané skladové karty (jen pro zobrazení, denormalizováno)" }

    PurchaseInvoiceItemPayload:
      type: object
      required: [description, quantity, unit_price_without_vat, vat_rate_id]
      properties:
        description: { type: string }
        quantity: { type: number }
        unit: { type: string, default: ks }
        unit_price_without_vat: { type: number }
        vat_rate_id: { type: integer }
        order_index: { type: integer }
        vat_classification_code: { type: [string, "null"] }
        is_fixed_asset: { type: boolean, default: false }
        stock_item_id:
          type: [integer, "null"]
          description: |
            Volitelná vazba na skladovou kartu (Epic SKLAD) — jen naznačuje kartu pro
            předvyplnění příjemkového wizardu; skutečný pohyb dělá až příjemka
            (POST /api/purchase-invoices/{id}/stock-receipt). Musí patřit stejnému
            dodavateli, jinak se tiše ignoruje (uloží se NULL).

    AiAssistSettings:
      type: object
      required: [enabled, scope, provider, provider_label, data_region, dpa_confirmed, embedding_available, knn_warm, muted_sources, daily_limit, today_used]
      properties:
        enabled: { type: boolean }
        scope:
          type: array
          items: { type: string, enum: [bank_tx, purchase_invoices] }
        provider: { type: string, enum: [anthropic, azure_openai, openai, gemini] }
        provider_label: { type: string }
        data_region: { type: string, enum: [us, eu] }
        dpa_confirmed:
          type: object
          additionalProperties:
            type: [string, "null"]
            format: date-time
        embedding_available: { type: boolean }
        knn_warm:
          type: object
          properties:
            bank_transaction: { type: boolean }
            purchase_invoice: { type: boolean }
            labels:
              type: object
              properties:
                bank_transaction: { type: integer, minimum: 0 }
                purchase_invoice: { type: integer, minimum: 0 }
        muted_sources:
          type: array
          items:
            type: object
            properties:
              source: { type: string, enum: [knn, llm] }
              muted_at: { type: string, format: date-time }
              reason: { type: object, additionalProperties: true }
        daily_limit: { type: integer, minimum: 0 }
        today_used: { type: integer, minimum: 0 }

    AiPostingSuggestion:
      type: object
      required: [id, entity_type, entity_id, source, payload, confidence, status]
      properties:
        id: { type: integer, format: int64 }
        entity_type: { type: string, enum: [purchase_invoice, invoice, cash_transaction] }
        entity_id: { type: integer, format: int64 }
        source: { type: string, enum: [knn, llm] }
        payload:
          type: object
          properties:
            debit_account_code: { type: string }
            credit_account_code: { type: [string, "null"] }
            expense_category_id: { type: [integer, "null"] }
        confidence: { type: number, minimum: 0, maximum: 0.4 }
        reasoning: { type: [string, "null"] }
        status: { type: string, enum: [pending, accepted, rejected, superseded, expired] }
        created_at: { type: string, format: date-time }

    PurchaseInvoice:
      type: object
      properties:
        id: { type: integer }
        supplier_id: { type: integer }
        vendor_id: { type: integer }
        varsymbol: { type: [string, "null"] }
        vendor_invoice_number: { type: string }
        document_kind: { type: string, enum: [invoice, receipt, credit_note, advance] }
        advance_purchase_invoice_id: { type: [integer, "null"], description: "FK na zálohovou fakturu, kterou tento doklad vyúčtovává (párování zálohy)" }
        advance_link_suggested_id: { type: [integer, "null"], description: "AI/auto návrh na propojení zálohy (čeká na potvrzení)" }
        issue_date: { type: string, format: date }
        tax_date: { type: [string, "null"], format: date }
        due_date: { type: string, format: date }
        received_at: { type: string, format: date }
        prices_include_vat: { type: boolean, description: "Ceny položek zadané včetně DPH (migrace 0083)" }
        expense_category_id: { type: [integer, "null"], description: "FK do /expense-categories — kategorie nákladu pro CRM/dashboard" }
        expense_category_label: { type: [string, "null"] }
        expense_category_code: { type: [string, "null"] }
        ai_posting_suggestion:
          allOf:
            - $ref: '#/components/schemas/AiPostingSuggestion'
          type: [object, "null"]
        currency_id: { type: integer }
        currency: { type: string }
        currency_symbol: { type: string }
        exchange_rate: { type: [number, "null"] }
        exchange_rate_date: { type: [string, "null"], format: date }
        exchange_rate_source: { type: string, enum: [cnb, manual, idoklad, fakturoid] }
        reverse_charge: { type: boolean }
        vat_deduction:
          type: string
          enum: [full, none, proportional]
          default: full
          description: |
            Nárok na odpočet DPH. `none` = faktura NEvstupuje do Knihy DPH ani do
            DPHDP3 / KH (jen účetní náklad). `proportional` = krácený / poměrný odpočet
            (§75) — odpočet (základ i daň) se zkrátí o `vat_deduction_percent`.
        vat_deduction_percent:
          type: number
          format: float
          minimum: 0
          maximum: 100
          default: 100
          description: "Procento odpočtu při `vat_deduction='proportional'` (§75). Pro full/none se ignoruje (drží 100)."
        tax_deductible:
          type: boolean
          default: true
          description: "Daňová uznatelnost nákladu pro daň z příjmů (DPFO/DPPO); false = nezahrne se do nákladů. Nezávislé na DPH."
        is_fixed_asset:
          type: boolean
          default: false
          description: |
            Doklad je za dlouhodobý majetek (§ 4 odst. 4 písm. c) — vozidlo, stroj.
            Header flag pro celý doklad. Pro mixed doklady (část majetek, část běžné
            náklady) použij per-řádek `PurchaseInvoiceItem.is_fixed_asset`.

            Když je nastaveno a doklad jde do odpočtu na ř. 40-45 DPHDP3 (přímo
            nebo přes ř. 43 RC mirror), hodnota se navíc uvede na ř. 47 jako
            doplňující údaj (`nar_maj`).
        language: { type: string, enum: [cs, en] }
        note_above_items: { type: [string, "null"] }
        note_below_items: { type: [string, "null"] }
        vendor_snapshot:
          type: object
          description: Immutable JSON snapshot vendor row at time of receipt
        own_snapshot: { type: [object, "null"] }
        total_without_vat: { type: number }
        total_vat: { type: number }
        total_with_vat: { type: number }
        rounding: { type: number }
        advance_paid_amount: { type: number }
        amount_to_pay: { type: number }
        payment_currency_id: { type: [integer, "null"] }
        payment_exchange_rate: { type: [number, "null"] }
        paid_amount_payment_ccy: { type: [number, "null"] }
        paid_amount_invoice_ccy: { type: [number, "null"] }
        exchange_diff_base:
          type: [number, "null"]
          description: "FX gain/loss v base ccy (CZK)"
        status: { type: string, enum: [draft, received, booked, paid, cancelled] }
        booked_at: { type: [string, "null"], format: date-time }
        paid_at: { type: [string, "null"], format: date }
        cancelled_at: { type: [string, "null"], format: date-time }
        pdf_path:
          type: [string, "null"]
          description: "Relative path within archive_storage"
        pdf_hash: { type: [string, "null"], description: "SHA-256 hex" }
        pdf_size_bytes: { type: [integer, "null"] }
        pdf_original_name: { type: [string, "null"] }
        pdf_uploaded_at: { type: [string, "null"], format: date-time }
        source_path: { type: [string, "null"], description: "Relative path to machine-readable source artifact within archive_storage (sources/ subtree)" }
        source_hash: { type: [string, "null"], description: "SHA-256 hex of source artifact" }
        source_size_bytes: { type: [integer, "null"] }
        source_original_name: { type: [string, "null"] }
        source_format: { type: [string, "null"], enum: [isdoc, isdocx, pdf, pohoda_xml, idoklad_json, fakturoid_json, null], description: "Format of the archived authoritative source document" }
        source_uploaded_at: { type: [string, "null"], format: date-time }
        vat_classification_code: { type: [string, "null"] }
        vat_overrides:
          type: [array, "null"]
          description: "Per-sazba override rekapitulace DPH (§73; migrace 0086). Zapéká se do řádkových totálů."
          items:
            type: object
            properties:
              rate: { type: number }
              base: { type: number }
              vat:  { type: number }
        vat_allocations:
          type: array
          description: "Volitelné rozdělení rekapitulace DPH na odpočtové a účetní režimy; prázdné pole zachovává hlavičkové nastavení."
          items: { $ref: '#/components/schemas/PurchaseVatAllocation' }
        extraction_warning: { type: [string, "null"], description: "Varování z AI/automatické extrakce dokladu (rozdíl součtů apod.)" }
        idoklad_id: { type: [integer, "null"], description: "ID v iDoklad (jen u importovaných)" }
        fakturoid_id: { type: [integer, "null"], description: "ID ve Fakturoid (jen u importovaných)" }
        created_by: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        vendor_company_name: { type: string }
        vendor_ic: { type: [string, "null"] }
        vendor_dic: { type: [string, "null"] }
        vendor_main_email: { type: [string, "null"] }
        linked_advance:
          type: [object, "null"]
          description: "Brief zálohy, kterou tento doklad vyúčtovává ({ id, varsymbol, document_kind, status, total_with_vat, currency })"
        advance_link_suggestion: { type: [object, "null"], description: "Brief AI-navržené zálohy k propojení" }
        settled_by: { type: [object, "null"], description: "Jen u zálohy — brief finální faktury, která ji vyúčtovává" }
        has_advance_candidates: { type: boolean }
        has_settlement_candidates: { type: boolean }
        items:
          type: array
          items: { $ref: '#/components/schemas/PurchaseInvoiceItem' }
        vat_breakdown:
          type: array
          items:
            type: object
            properties:
              vat_rate: { type: number }
              without_vat: { type: number }
              vat: { type: number }
              with_vat: { type: number }
        totals:
          type: object
          properties:
            without_vat: { type: number }
            vat: { type: number }
            with_vat: { type: number }
            rounding: { type: number }
            advance_paid_amount: { type: number }
            amount_to_pay: { type: number }
        _warnings:
          type: array
          items: { type: string }
          description: "Jen v odpovědi POST/PUT — non-blocking varování (credit_note_positive_total, vendor_non_payer_deduction, vendor_unreliable_payer — CRPDPH nespolehlivý plátce, audit 2026-07), uložení neblokují."

    PurchaseVatAllocation:
      type: object
      required: [description, usage_type, vat_rate, base_amount, vat_amount, total_amount, vat_deduction, vat_deduction_percent, tax_treatment, account_code, order_index]
      properties:
        id: { type: integer, readOnly: true }
        description: { type: string, maxLength: 190 }
        usage_type: { type: string, enum: [business, personal, mixed, non_deductible] }
        vat_rate: { type: number, format: float }
        base_amount: { type: number }
        vat_amount: { type: number }
        total_amount: { type: number }
        vat_deduction: { type: string, enum: [full, none, proportional, reduced] }
        vat_deduction_percent: { type: number, minimum: 0, maximum: 100 }
        tax_treatment: { type: string, enum: [deductible, non_deductible, not_expense] }
        account_code: { type: string, maxLength: 10 }
        vat_classification_code: { type: [string, "null"] }
        order_index: { type: integer }

    PurchaseInvoicePayload:
      type: object
      required: [vendor_id, vendor_invoice_number, issue_date, due_date, currency_id, items]
      properties:
        vendor_id: { type: integer }
        vendor_invoice_number: { type: string, maxLength: 50 }
        varsymbol: { type: [string, "null"], maxLength: 20 }
        document_kind: { type: string, enum: [invoice, receipt, credit_note, advance], default: invoice }
        issue_date: { type: string, format: date }
        tax_date: { type: [string, "null"], format: date }
        due_date: { type: string, format: date }
        received_at: { type: string, format: date }
        currency_id: { type: integer }
        exchange_rate: { type: [number, "null"] }
        exchange_rate_date: { type: [string, "null"], format: date }
        exchange_rate_source: { type: string, enum: [cnb, manual, idoklad, fakturoid], default: cnb }
        reverse_charge: { type: boolean, default: false }
        prices_include_vat: { type: boolean, default: false, description: "Ceny položek zadané včetně DPH (DPH shora koeficientem)." }
        expense_category_id: { type: [integer, "null"], description: "Kategorie nákladu. Když chybí, na create se odvodí z default kategorie dodavatele." }
        vat_overrides:
          type: [array, "null"]
          description: "Per-sazba override rekapitulace DPH (§73). Zapisuje se jen když je klíč přítomen; zapéká se do řádkových totálů."
          items:
            type: object
            properties:
              rate: { type: number }
              base: { type: number }
              vat:  { type: number }
        vat_allocations:
          type: array
          description: "Účetní alokace celé rekapitulace; součty musí po sazbách přesně odpovídat dokladu. [] alokace zruší."
          items: { $ref: '#/components/schemas/PurchaseVatAllocation' }
        rounding: { type: number, description: "Jen PUT — zaokrouhlení dokladu; zapisuje se jen když je klíč přítomen." }
        vat_deduction: { type: string, enum: [full, none, proportional, reduced], default: full, description: "Nárok na odpočet DPH (none → mimo Knihu DPH/DPHDP3/KH; proportional → poměrný §75; reduced → krácený §76)." }
        vat_deduction_percent: { type: number, format: float, minimum: 0, maximum: 100, default: 100, description: "Procento odpočtu pro proportional (§75)." }
        tax_deductible: { type: boolean, default: true, description: "Daňová uznatelnost pro daň z příjmů (nezávislé na DPH)." }
        is_fixed_asset:
          type: boolean
          default: false
          description: "Doklad za dlouhodobý majetek — propíše se do ř. 47 DPHDP3 jako doplňující údaj."
        language: { type: string, enum: [cs, en], default: cs }
        note_above_items: { type: [string, "null"] }
        note_below_items: { type: [string, "null"] }
        advance_paid_amount: { type: number, default: 0 }
        payment_currency_id: { type: [integer, "null"] }
        payment_exchange_rate: { type: [number, "null"] }
        paid_amount_payment_ccy: { type: [number, "null"] }
        paid_amount_invoice_ccy: { type: [number, "null"] }
        exchange_diff_base: { type: [number, "null"] }
        vat_classification_code: { type: [string, "null"] }
        items:
          type: array
          items: { $ref: '#/components/schemas/PurchaseInvoiceItemPayload' }

    InboxScanResult:
      type: object
      properties:
        created: { type: integer }
        skipped: { type: integer }
        failed: { type: integer }
        dry_run: { type: boolean }
        inbox_dir: { type: string }
        details:
          type: array
          items:
            type: object
            properties:
              file: { type: string }
              status:
                type: string
                enum: [created, skipped, failed, rejected, mapper_pending, config_missing, inbox_missing, limit_reached]
              reason: { type: [string, "null"] }
              purchase_invoice_id: { type: [integer, "null"] }
              isdoc_invoice_count: { type: [integer, "null"] }
              supplier_ic: { type: [string, "null"] }

    Car:
      type: object
      properties:
        id: { type: integer, format: int64 }
        registration: { type: string }
        name: { type: [string, "null"] }
        brand: { type: [string, "null"] }
        model: { type: [string, "null"] }
        vin: { type: [string, "null"] }
        fuel_type: { type: [string, "null"], enum: [diesel, petrol, lpg, cng, electric, hybrid, other, null] }
        odometer_start: { type: [integer, "null"] }
        odometer_start_date: { type: [string, "null"], format: date }
        is_default: { type: boolean }
        is_archived: { type: boolean }
        note: { type: [string, "null"] }
        last_odometer: { type: [integer, "null"], description: Poslední známý stav tachometru (z jízd) }
        trips_count: { type: [integer, "null"] }
        fuelings_count: { type: [integer, "null"] }
        created_at: { type: string, format: date-time }
    CarInput:
      type: object
      required: [registration]
      properties:
        registration: { type: string, maxLength: 20 }
        name: { type: [string, "null"] }
        brand: { type: [string, "null"] }
        model: { type: [string, "null"] }
        vin: { type: [string, "null"] }
        fuel_type: { type: [string, "null"], enum: [diesel, petrol, lpg, cng, electric, hybrid, other, null] }
        odometer_start: { type: [integer, "null"] }
        odometer_start_date: { type: [string, "null"], format: date }
        is_default: { type: boolean }
        is_archived: { type: boolean }
        note: { type: [string, "null"] }
    Trip:
      type: object
      properties:
        id: { type: integer, format: int64 }
        car_id: { type: integer }
        car_registration: { type: [string, "null"] }
        trip_date: { type: string, format: date }
        time_start: { type: [string, "null"] }
        time_end: { type: [string, "null"] }
        odometer_start: { type: [integer, "null"] }
        odometer_end: { type: [integer, "null"] }
        distance_km: { type: number }
        category_id: { type: [integer, "null"] }
        category_label: { type: [string, "null"] }
        category_is_private: { type: [boolean, "null"] }
        purpose: { type: [string, "null"] }
        origin: { type: [string, "null"] }
        destination: { type: [string, "null"] }
        note: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
    TripInput:
      type: object
      required: [car_id, trip_date]
      properties:
        car_id: { type: integer }
        trip_date: { type: string, format: date }
        time_start: { type: [string, "null"] }
        time_end: { type: [string, "null"] }
        odometer_start: { type: [integer, "null"] }
        odometer_end: { type: [integer, "null"] }
        distance_km: { type: [number, "null"], description: "Prázdné = dopočítá se z tachometru (konec − začátek)" }
        category_id: { type: [integer, "null"] }
        purpose: { type: [string, "null"] }
        origin: { type: [string, "null"] }
        destination: { type: [string, "null"] }
        note: { type: [string, "null"] }
    Fueling:
      type: object
      properties:
        id: { type: integer, format: int64 }
        car_id: { type: [integer, "null"] }
        car_registration: { type: [string, "null"] }
        fueled_date: { type: string, format: date }
        fueled_time: { type: [string, "null"] }
        fuel_type: { type: [string, "null"] }
        quantity: { type: [number, "null"] }
        unit: { type: string }
        unit_price: { type: [number, "null"] }
        amount_without_vat: { type: [number, "null"] }
        amount_vat: { type: [number, "null"] }
        amount_with_vat: { type: number }
        currency: { type: string }
        odometer: { type: [integer, "null"] }
        station: { type: [string, "null"] }
        vendor_id: { type: [integer, "null"] }
        vendor_name: { type: [string, "null"] }
        source: { type: string, enum: [manual, invoice, axigon, axigon_ai, import] }
        source_purchase_invoice_id: { type: [integer, "null"] }
        source_invoice_number: { type: [string, "null"] }
        receipt_number: { type: [string, "null"] }
        note: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
    FuelingInput:
      type: object
      required: [fueled_date, amount_with_vat]
      properties:
        car_id: { type: [integer, "null"] }
        fueled_date: { type: string, format: date }
        fueled_time: { type: [string, "null"] }
        fuel_type: { type: [string, "null"] }
        quantity: { type: [number, "null"] }
        unit: { type: string }
        unit_price: { type: [number, "null"] }
        amount_with_vat: { type: number }
        currency: { type: string }
        odometer: { type: [integer, "null"] }
        station: { type: [string, "null"] }
        vendor_id: { type: [integer, "null"] }
        note: { type: [string, "null"] }
    TripCategory:
      type: object
      properties:
        id: { type: integer }
        code: { type: string }
        label: { type: string }
        is_private: { type: boolean }
        display_order: { type: integer }
        is_archived: { type: boolean }
        trips_count: { type: [integer, "null"] }
    LogbookSummary:
      type: object
      properties:
        year: { type: integer }
        available_years: { type: array, items: { type: integer } }
        vehicles: { type: array, items: { $ref: '#/components/schemas/SummaryVehicle' } }
        totals: { $ref: '#/components/schemas/SummaryTotals' }
        monthly: { $ref: '#/components/schemas/MonthlyKm' }
    MonthlyKm:
      type: object
      description: Najeté km po měsících (12 hodnot, leden..prosinec) — vybraný rok vs předchozí.
      properties:
        year: { type: integer }
        prev_year: { type: integer }
        current: { type: array, items: { type: number } }
        previous: { type: array, items: { type: number } }
    SummaryVehicle:
      type: object
      properties:
        car_id: { type: integer }
        registration: { type: string }
        name: { type: [string, "null"] }
        trips_count: { type: integer }
        km: { type: number }
        business_km: { type: number }
        private_km: { type: number }
        uncategorized_km: { type: number }
        private_ratio: { type: number }
        business_ratio: { type: number }
        odometer_start: { type: [integer, "null"] }
        odometer_end: { type: [integer, "null"] }
        fuel_count: { type: integer }
        liters: { type: number }
        liters_count: { type: integer }
        liters_incomplete: { type: boolean }
        kwh: { type: number, description: Charged energy (kWh) for electric/PHEV vehicles. }
        kwh_count: { type: integer }
        kwh_incomplete: { type: boolean }
        fuel_cost: { type: number }
        avg_consumption: { type: [number, "null"], description: Fuel consumption l/100 km. }
        avg_consumption_kwh: { type: [number, "null"], description: Electric consumption kWh/100 km. }
        continuity_issues: { type: integer }
        continuity_detail:
          type: array
          items:
            type: object
            properties:
              prev_date: { type: string, format: date }
              prev_end: { type: integer }
              date: { type: string, format: date }
              start: { type: integer }
              gap: { type: integer }
        pausal_months: { type: integer }
        pausal_rate: { type: integer }
        pausal_year: { type: integer }
    SummaryTotals:
      type: object
      properties:
        vehicles_count: { type: integer }
        trips_count: { type: integer }
        km: { type: number }
        business_km: { type: number }
        private_km: { type: number }
        uncategorized_km: { type: number }
        private_ratio: { type: number }
        fuel_count: { type: integer }
        liters: { type: number }
        kwh: { type: number, description: Total charged energy (kWh). }
        fuel_cost: { type: number }
        avg_consumption: { type: [number, "null"] }
        avg_consumption_kwh: { type: [number, "null"] }
        liters_incomplete: { type: boolean }
        kwh_incomplete: { type: boolean }
        continuity_issues: { type: integer }
    EpoSubmissionSettings:
      type: object
      required: [vat_root_folder_id, income_tax_root_folder_id, epo_environment, folders]
      properties:
        vat_root_folder_id:
          type: [integer, "null"]
          format: int64
          description: Kořenová složka pro DPH, KH, SH a OSS; null vytvoří výchozí kořen.
        income_tax_root_folder_id:
          type: [integer, "null"]
          format: int64
          description: Kořenová složka pro DPFO a DPPO; null vytvoří výchozí kořen.
        epo_environment:
          type: string
          enum: [production, test]
          readOnly: true
          description: Aktivní serverové prostředí přímých podepsaných EPO operací; asistovaný formulář vždy používá produkční interaktivní endpoint.
        folders:
          type: array
          readOnly: true
          description: Aktivní složky firmy dostupné pro konfiguraci bez oprávnění k celému modulu Dokumenty.
          items: { $ref: '#/components/schemas/DocumentFolder' }
    EpoSubmissionAttempt:
      type: object
      required: [id, channel, epo_environment, status, request_sha256, requested_at]
      properties:
        id: { type: integer, format: int64 }
        channel: { type: string, enum: [epo_assisted, epo_direct] }
        epo_environment: { type: string, enum: [production, test] }
        status:
          type: string
          enum:
            [prepared, handoff_created, awaiting_confirmation, testing, test_passed,
             test_failed, submitting, processing, submitted, confirmed, rejected,
             uncertain, failed, expired, cancelled]
        request_sha256: { type: string, pattern: '^[a-f0-9]{64}$' }
        signing_credential_id: { type: [integer, "null"], format: int64 }
        signing_fingerprint: { type: [string, "null"], pattern: '^[a-f0-9]{64}$' }
        response_http_status: { type: [integer, "null"] }
        test_passed: { type: [boolean, "null"] }
        test_messages:
          type: array
          items: { $ref: '#/components/schemas/EpoMessage' }
        tested_at: { type: [string, "null"], format: date-time }
        error_code: { type: [string, "null"] }
        error_message: { type: [string, "null"] }
        requested_by: { type: [integer, "null"], format: int64 }
        requested_at: { type: string, format: date-time }
        handoff_expires_at: { type: [string, "null"], format: date-time }
        submitted_at: { type: [string, "null"], format: date-time }
        remote_submission_ref: { type: [string, "null"] }
        remote_status: { type: [object, "null"], additionalProperties: { type: string } }
        last_status_at: { type: [string, "null"], format: date-time }
        confirmed_at: { type: [string, "null"], format: date-time }
        poll_count: { type: integer, minimum: 0 }
        next_poll_at: { type: [string, "null"], format: date-time }
        status_query_available: { type: boolean }
        refresh_available: { type: boolean }
        confirmation_recovery_available: { type: boolean }
        resolution_code: { type: [string, "null"] }
        resolution_note: { type: [string, "null"] }
        resolved_by: { type: [integer, "null"], format: int64 }
        resolved_at: { type: [string, "null"], format: date-time }
        updated_at: { type: string, format: date-time }
    EpoSubmissionArtifact:
      type: object
      required:
        [id, tax_submission_id, document_id, artifact_kind, sha256, verification_status, created_at]
      properties:
        id: { type: integer, format: int64 }
        tax_submission_id: { type: integer }
        attempt_id: { type: [integer, "null"], format: int64 }
        document_id: { type: integer, format: int64 }
        artifact_kind:
          type: string
          enum:
            [source_xml, epo_xml, signed_submission_p7s, confirmation_p7s,
             epo_error_xml, epo_status_xml, receipt_pdf, other]
        sha256: { type: string, pattern: '^[a-f0-9]{64}$' }
        verification_status:
          type: string
          enum: [not_applicable, pending, valid, warning, invalid]
        verification: { type: [object, "null"], additionalProperties: true }
        title: { type: string }
        original_name: { type: string }
        size_bytes: { type: integer, format: int64 }
        doc_type: { type: string }
        folder_id: { type: [integer, "null"], format: int64 }
        created_at: { type: string, format: date-time }
    TaxSubmission:
      type: object
      required:
        [id, form_code, period_year, xml_size_bytes, xml_sha256, validation_status, status,
         generated_at, attempts, artifacts]
      properties:
        id: { type: integer }
        form_code: { type: string, enum: [dphdp3, dphkh1, dphshv, dpfdp5, dpfdp7, dppdp9, ossei1] }
        form_variant: { type: string }
        period_year: { type: integer }
        period_month: { type: [integer, "null"], minimum: 1, maximum: 12 }
        period_quarter: { type: [integer, "null"], minimum: 1, maximum: 4 }
        xml_size_bytes: { type: integer, format: int64 }
        xml_sha256: { type: string, pattern: '^[a-f0-9]{64}$' }
        validation_status: { type: string, enum: [passed, failed, skipped] }
        validation_errors: { type: array, items: { type: string } }
        status: { type: string, enum: [draft, generated, downloaded, submitted, accepted, rejected] }
        submitted_at: { type: [string, "null"], format: date-time }
        submission_ref: { type: [string, "null"] }
        summary: { type: [object, "null"], additionalProperties: true }
        generated_at: { type: string, format: date-time }
        notes: { type: [string, "null"] }
        attempts:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionAttempt' }
        artifacts:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionArtifact' }
    EpoHandoffResult:
      type: object
      required: [attempt_id, url, expires_at, archive_folder_id, source_document_id, environment]
      properties:
        attempt_id: { type: integer, format: int64 }
        url: { type: string, format: uri }
        expires_at: { type: string, format: date-time }
        archive_folder_id: { type: [integer, "null"], format: int64 }
        source_document_id: { type: [integer, "null"], format: int64 }
        environment: { type: string, enum: [production, test] }
    EpoArtifactUploadResult:
      type: object
      required: [created, errors, artifacts, attempts]
      properties:
        created:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionArtifact' }
        errors:
          type: array
          items:
            type: object
            required: [name, code]
            properties:
              name: { type: string }
              code: { type: string }
              message: { type: string }
        source_artifact: { $ref: '#/components/schemas/EpoSubmissionArtifact' }
        artifacts:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionArtifact' }
        attempts:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionAttempt' }
    EpoMessage:
      type: object
      required: [type, code, text, field, section, line]
      properties:
        type:
          type: [string, "null"]
          description: I informativní; S struktura; K kritická; N závažná propustná; P propustná; E výjimka.
        code: { type: [string, "null"] }
        text: { type: [string, "null"] }
        field: { type: [string, "null"] }
        section: { type: [string, "null"] }
        line: { type: [string, "null"] }
    EpoSigningCredential:
      type: object
      required:
        [id, label, fingerprint_sha256, subject_dn, issuer_dn, valid_from,
         valid_to, ik_mpsv_present, epo_verified, valid_now, enabled_for_supplier,
         linked_profiles_count, linked_supplier_profiles_count, created_at]
      properties:
        id: { type: integer, format: int64 }
        label: { type: string }
        fingerprint_sha256: { type: string, pattern: '^[a-f0-9]{64}$' }
        subject_dn: { type: string }
        issuer_dn: { type: string }
        serial_hex: { type: [string, "null"] }
        valid_from: { type: string, format: date-time }
        valid_to: { type: string, format: date-time }
        ik_mpsv_present: { type: boolean }
        epo_verified: { type: boolean, description: Alespoň jeden test=1 s tímto přesným otiskem úspěšně prošel EPO. }
        epo_verified_at: { type: [string, "null"], format: date-time }
        valid_now: { type: boolean }
        enabled_for_supplier: { type: boolean }
        linked_profiles_count: { type: integer, minimum: 0, description: Počet aktivních podpisových profilů používajících certifikát napříč firmami vlastníka. }
        linked_supplier_profiles_count: { type: integer, minimum: 0, description: Počet aktivních podpisových profilů používajících certifikát v aktuální firmě. }
        created_at: { type: string, format: date-time }
    EpoDirectTestResult:
      type: object
      required: [attempt_id, passed, messages, large_submission, environment, artifacts, attempts]
      properties:
        attempt_id: { type: integer, format: int64 }
        passed: { type: boolean }
        messages:
          type: array
          items: { $ref: '#/components/schemas/EpoMessage' }
        large_submission: { type: boolean }
        environment: { type: string, enum: [production, test] }
        artifacts:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionArtifact' }
        attempts:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionAttempt' }
    EpoDirectSubmissionResult:
      type: object
      required: [attempt_id, status, environment]
      properties:
        attempt_id: { type: integer, format: int64 }
        status: { type: string, enum: [processing, confirmed, rejected, uncertain, cancelled] }
        environment: { type: string, enum: [production, test] }
        reference: { type: string }
        submitted_at: { type: string, format: date-time }
        chain_valid: { type: boolean }
        message: { type: string }
        messages:
          type: array
          items: { $ref: '#/components/schemas/EpoMessage' }
        remote_status: { type: object, additionalProperties: { type: string } }
        artifacts:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionArtifact' }
        attempts:
          type: array
          items: { $ref: '#/components/schemas/EpoSubmissionAttempt' }
