Skip to content

Relace a vyúčtování (REST API)

Tato stránka popisuje části REST API, ze kterých se skládá panel aktivní relace: náhled vyúčtování, pole active_session na dashboardu a stav zařízení. Všechny endpointy vyžadují přihlášeného uživatele (PocketBase auth token).

Jedno vyúčtování pro náhled i skutečný konec

Cena, kterou vidíte během relace, a částka, kterou systém při ukončení skutečně účtuje, jsou počítány jednou a toutéž funkcí (session.ComputeSettlement). Náhled ji spustí nad ještě běžící relací „jako by skončila právě teď“ a výsledek zahodí; EndSession ji spustí a výsledek zapíše. Proto náhled zahrnuje všechno, co skutečné ukončení:

  • ceník zóny (sazby, přechody mezi sazbami, volné minuty) a ceny zařízení — i zařízení, které je v tu chvíli ještě zapnuté (účtuje se do okamžiku ukončení),
  • slevy a pokrytí produkty (procentní sleva, permanentka, minutová kvóta, věrnostní program, balíček vstupů, sleva na zařízení),
  • administrátora (vše zdarma),
  • vstupní zóny: vstupné zaplacené při zahájení (prepaid) a případně doplatek za zařízení, allow_debt,
  • rozdělení na úhradu z kreditu a dluh.

Náhled je čistě čtecí: nic nezapisuje (relace, položky vyúčtování, stav produktů, kredit, action logy zůstanou beze změny), neposílá příkazy zařízením a nespouští vedlejší efekty produktů (např. načítání odehraných minut věrnostního programu — ty proběhnou právě jednou, až při skutečném ukončení). Lze ho volat opakovaně.

Částka se počítá k okamžiku volání; mezi náhledem a potvrzením ukončení uplyne několik sekund, o které se výsledek může lišit.

GET /api/v1/sessions/{id}/settlement-preview

Vrátí vyúčtování, které by teď nastalo při ukončení relace. Pouze vlastník relace a pouze aktivní relace.

Stav Kód chyby Význam
401 — Nepřihlášen
403 forbidden Relace patří jinému uživateli
404 session_not_found Relace neexistuje
400 session_not_active Relace už skončila

Odpověď (200):

{
  "items": [
    { "kind": "charge", "label": "Rate 10:00-10:30 (30 min)", "label_key": "zone_rate_range",
      "label_params": { "from": "10:00", "to": "10:30", "minutes": 30 }, "amount": 60 },
    { "kind": "discount", "label": "Discount (20% Sleva)", "label_key": "discount_product_named",
      "label_params": { "product": "20% Sleva" }, "amount": 12 },
    { "kind": "payment", "label": "Credit", "label_key": "credit_payment", "label_params": {}, "amount": 48 }
  ],
  "total_price": 60,
  "discount": 12,
  "rounding": 0,
  "final_price": 48,
  "prepaid": 0,
  "prepaid_price": 0,
  "amount_due": 48,
  "from_credit": 48,
  "to_debt": 0,
  "coverage": "none",
  "coverage_name": null
}
Pole Význam
items Řádky vyúčtování, které by EndSession přidal, v pořadí zápisu. amount je vždy kladné, směr určuje kind (charge zvyšuje dlužnou částku, discount a payment ji snižují). label je anglický záložní text; UI překládá label_key + label_params (jména produktů a zařízení v label_params se nepřekládají). U vstupní zóny, jejíž vstupné bylo zaplaceno při zahájení, se jeho řádky znovu neuvádějí — viz prepaid.
total_price Hrubé poplatky celkem.
discount Jen skutečné snížení ceny (volné minuty, produkty, administrátor) před použitím kreditu — nezahrnuje zaokrouhlení.
rounding ± rozdíl mezi přesnou cenou a final_price zaokrouhlenou na PRICE_DECIMAL_PLACES (např. -0.3 pro 820,30 → 820). Při 2 desetinných místech vždy 0.
final_price Cena relace po slevách a zaokrouhlení, před kreditem (total_price − discount + rounding). U vstupní zóny včetně již zaplaceného vstupného.
prepaid Co už bylo zaplaceno při zahájení (vstupné z kreditu). Jinak 0.
prepaid_price Přesná cena (na 2 desetinná místa) již zaplaceného vstupného — první řádek účtenky ve vyúčtování. Platí prepaid_price + Σ items (charge − discount) + rounding = final_price. prepaid je naproti tomu skutečně zaplacená (zaokrouhlená) částka. Jinak 0.
amount_due Co ukončení právě teď ještě vypořádá (≥ 0). U zaplaceného vstupu jen doplatek za zařízení. amount_due = from_credit + to_debt.
from_credit Část amount_due pokrytá aktuálním kreditem uživatele.
to_debt Zbytek, který se stane dluhem.
coverage Proč relace stojí, kolik stojí: none (platí se běžně), admin, product, prepaid (vstup zaplacen při zahájení), free (nic k placení).
coverage_name Název produktu při coverage: "product" (pokud je znám), jinak null.

Cena a dlužná částka u uložené relace

U uložených (ukončených) relací mají peněžní pole tento význam:

Pole Význam
final_price Cena po slevách a zaokrouhlení. Úhrady (kredit, bankovní převod, ruční úhrada) ji nesnižují — zaplacená relace má dál např. 818.
discount Jen skutečná snížení (slevy, produkty, permanentky, ruční slevy, historické úpravy z migrace). Zaokrouhlení v ní už není.
price_rounding ± zaokrouhlení na PRICE_DECIMAL_PLACES (výchozí 0 = celé koruny, půlka od nuly). Při PRICE_DECIMAL_PLACES = 2 vždy 0.
amount_due Kolik zbývá doplatit (≥ 0, u zaplacené relace 0).
paid Relace má poplatek a amount_due ≤ 0.

Platí identity: total_price − discount + price_rounding = final_price, součet řádků session_price_items (bez úhrad) + price_rounding = final_price a final_price − součet úhrad = amount_due.

Klíč rounding obsahují odpovědi vyúčtování (settlement-preview, end-session, terminálový odchod); relace vrácené jako záznamy (statistiky, recent_sessions na dashboardu, admin endpointy relací, PocketBase kolekce sessions) používají název pole price_rounding.

amount_due vrací odpovědi end-session (POST /api/v1/sessions/{id}/end), GET /api/v1/sessions/{id}/qr (částka QR/SPD), dashboard (recent_sessions), statistiky, terminálový odchod a admin endpointy relací vedle final_price. Tento amount_due je stav po ukončení; amount_due v settlement-preview výše je naproti tomu částka, kterou by ukončení teprve vypořádalo.

Souhrny

total_unpaid (session-summary, statistiky) sčítá amount_due; total_paid a tržby sčítají final_price zaplacených relací.

GET /api/v1/me/dashboard — active_session

Při aktivní relaci obsahuje active_session (jinak je active_session null):

Pole Popis
settlement Stejný objekt jako výše (z téže funkce); null, pokud se vyúčtování nepodařilo spočítat (chyba se zaloguje a do errors[] dashboardu se přidá failed to compute session settlement; dashboard neselže). Frontend v tom případě nezobrazí žádnou cenu (ani „Zdarma“), jen hlášku, že cenu nelze načíst.
estimated_price settlement.final_price (zpětná kompatibilita).
estimated_discount settlement.discount.
discount_items Slevové položky z settlement.items (kind: "discount").
zone_discount_percent Pevné procento slevy na čas v zóně (procentní sleva, permanentka), jinak null.
zone_discount_stepped true, pokud sleva nemá pevné procento (minutová kvóta, věrnostní program) — UI ji nesmí extrapolovat.
zone_fully_covered true, pokud stupňovitá sleva právě teď pokrývá celý poplatek za čas v zóně (sazba je skutečně 0).
device_discount_percents Sleva na zařízení v % podle ID zařízení (nezávisle na použití).
device_charges Poplatky za zařízení: device_id, device_name, amount, discount_amount, net_amount, use_count, duration_sec.
device_states Skutečný stav zařízení zóny (níže).

device_states

"device_states": {
  "<device_id>": {
    "state": "on",
    "confirmed": true,
    "last_command": "on",
    "last_command_at": "2026-10-07 18:02:11.000Z"
  }
}
Pole Popis
state on | off | open | close | unknown
confirmed true právě tehdy, když adaptér vrátil jiný stav než unknown bez chyby. Jen potvrzený stav smí UI zvýraznit.
last_command Poslední příkaz měnící stav (on, off, open, close, short_open) napříč všemi relacemi — blink se ignoruje; null, pokud žádný není.
last_command_at Čas toho příkazu v UTC, formát PocketBase (2026-10-07 18:02:11.000Z), nebo null.

Klíče jsou všechna zařízení zóny aktivní relace. Dotazy na stav běží souběžně, každý s limitem 2 s; výsledek se na 3 s cachuje po zařízení a po úspěšném příkazu (POST /api/v1/sessions/{id}/devices/{deviceId}/control) se cache daného zařízení zahodí, takže následné obnovení dashboardu vrací čerstvý stav. Chyba nebo timeout zařízení nikdy neshodí dashboard — zařízení se pouze ohlásí jako unknown / confirmed: false.

GET /api/v1/sessions/{id}/timeline

Události zařízení relace seřazené podle času (vlastník relace nebo admin):

{
  "events": [
    {
      "timestamp": "2026-10-08T08:34:00Z",
      "device_name": "Světla",
      "device_type": "light",
      "action": "off",
      "triggered_by": "user",
      "status": "failed",
      "failure_reason": "timeout",
      "error_detail": "http request failed: context deadline exceeded"
    }
  ]
}
Pole Popis
status success | failed | not_sent. Prázdná hodnota v databázi (starší záznam) se vrací jako success.
failure_reason U failed: timeout | unreachable | device_rejected | config_error | unknown; jinak prázdný řetězec.
error_detail Technický detail (oříznutý na 1024 znaků, bez přihlašovacích údajů). Jen pro admina — pro ostatní klíč v odpovědi vůbec není.

failed záznamy se do ceny nepočítají (jediný filtr: actionlog.Billable v pricing enginu); not_sent ano. GET /api/v1/me/dashboard vrací v active_session.action_logs[] navíc pole status (stejná normalizace).