Skip to content

Ceníky

Ceny v systému Dveřník se konfigurují pomocí ceníkových pravidel na úrovni zóny nebo konkrétního zařízení.

Typy pravidel

Zónová pravidla (zone_pricing_rules)

Platba za celou zónu. Používají se u entry-based i time-based zón.

Typ pravidla Použití Popis
flat_fee Entry-based zóny Fixní vstupné
per_minute Time-based zóny Sazba za minutu
free_minutes Time-based zóny Počet volných minut (sleva)

Pravidla zařízení (device_pricing_rules)

Platba za použití konkrétního zařízení (světla, zásuvky). Používají se u time-based zón.

Cenový model Popis
per_minute Sazba za minutu používání
per_use Fixní cena za každé zapnutí
free Zdarma

Pole pravidel

Obě kolekce mají stejná pole; liší se jen vazba (zone × device) a název sloupce s typem pravidla (rule_type u zón × pricing_model u zařízení).

Pole Typ Povinné Popis
zone / device relace Ano Ke které zóně nebo zařízení pravidlo patří
name string Ano Název pravidla (např. "Standardní", "Víkend")
rule_type (zóna) / pricing_model (zařízení) select Ano Typ pravidla, resp. cenový model (viz tabulky výše)
amount number ≥ 0 Ano Částka v Kč (u free_minutes počet minut). 0 je platné (zdarma)
time_from string Ne Začátek okna HH:MM (prázdné = bez dolní hranice)
time_to string Ne Konec okna HH:MM, vyjma (prázdné = bez horní hranice)
days_of_week JSON pole Ne Dny v týdnu (prázdné / null = každý den)
priority celé číslo ≥ 0 Ano Vyšší číslo = vyšší priorita
is_active bool Ano Musí být true aby se pravidlo uplatnilo

Časové okno

  • Okno je polouzavřené: [time_from, time_to) - 06:00–18:00 platí od 06:00:00 do 17:59:59.
  • Když je time_from větší než time_to, okno přechází přes půlnoc: 22:00–06:00 = od 22:00 do půlnoci a od půlnoci do 06:00.
  • Prázdná hranice znamená "bez omezení": jen time_from = od tohoto času do půlnoci, jen time_to = od půlnoci do tohoto času.
  • time_from = time_to je prázdné okno, které nikdy nenastane (uložení projde, ale dostanete varování).
  • Dny v týdnu se vždy posuzují podle dne okamžiku, který se cení (i u oken přes půlnoc).

Dny v týdnu (days_of_week)

Ve formuláři pravidla se dny vybírají zaškrtávacími políčky Po–Ne (dříve se psalo JSON pole ručně). Nic nezaškrtnuto = každý den. Do databáze se ukládá stále stejný formát: JSON pole čísel podle ISO normy.

Hodnota Den
1 Pondělí
2 Úterý
3 Středa
4 Čtvrtek
5 Pátek
6 Sobota
7 Neděle

Příklady:

  • Každý den: null, [] nebo [1,2,3,4,5,6,7]
  • Pouze pracovní dny: [1,2,3,4,5]
  • Pouze víkend: [6,7]

Kontrola při ukládání

Při vytvoření pravidla a při změně kontrolovaného pole (typ, částka, časy, dny, priorita) backend ověří:

  • typ pravidla / cenový model je jedna z povolených hodnot,
  • amount je číslo ≥ 0, priority je celé číslo ≥ 0,
  • time_from / time_to jsou prázdné nebo HH:MM (stejná kontrola jako dříve, přijímá se i např. 7:5),
  • days_of_week je null, prázdné pole, nebo pole unikátních celých čísel 1-7 ([0], [8], [1,1], ["1"] nebo text "[1,2]" se odmítne).

Chyba se vrací jako HTTP 400 s polem, kterého se týká (např. days_of_week). Starší pravidla, která dnešním pravidlům nevyhovují, zůstávají upravitelná (přejmenování, deaktivace) - kontrola se spustí jen když změníte některé z kontrolovaných polí. Seznam takových pravidel vypíše kontrola stavu konfigurace (GET /api/v1/admin/config/health, položky zone_pricing_rule / device_pricing_rule).

Rozlišení priorit

Pokud se více pravidel překrývá (stejný čas, stejný den), systém vybere pravidlo s nejvyšší prioritou (u zóny se vybírá zvlášť pro každý typ pravidla, u zařízení napříč modely). Dvě pravidla se stejnou prioritou, která se mohou překrývat, jsou nejednoznačná - vybere se libovolné z nich. Tarifní šablony se tomu vyhýbají a formulář při použití šablony na takové překrytí upozorní.

  1. Filtruje aktivní pravidla (is_active = true)
  2. Filtruje podle času (time_from / time_to)
  3. Filtruje podle dne v týdnu (days_of_week)
  4. Seřadí podle priority sestupně
  5. Použije první (nejprioritnější) pravidlo

Příklady konfigurace

Základní: Denní sazba 8:00–21:00

Pole Hodnota
name Standardní
rule_type per_minute
amount 1.668
time_from 08:00
time_to 21:00
days_of_week [1,2,3,4,5,6,7]
priority 1
is_active true

Víkendový příplatek (vyšší priorita)

Pole Hodnota
name Víkend
rule_type per_minute
amount 2.5
time_from 08:00
time_to 21:00
days_of_week [6,7]
priority 10
is_active true

Pravidlo "Víkend" (priorita 10) přepíše "Standardní" (priorita 1) o víkendech.

Večerní sazba

Pole Hodnota
name Večerní
rule_type per_minute
amount 2.5
time_from 18:00
time_to 21:00
days_of_week [1,2,3,4,5,6,7]
priority 10
is_active true

Tarifní šablony

Namísto ručního zadávání pravidel jedno po druhém lze použít tarifní šablonu - hotovou sadu pravidel. V Administraci → Zóny a zařízení (sekce "Zone Pricing Rules" a "Device Pricing Rules") tlačítko Použít tarifní šablonu:

  1. vyberte zónu (nebo zařízení),
  2. vyberte šablonu - u zón se nabízejí jen šablony odpovídající režimu zóny (time_based / entry_based),
  3. sada pravidel se zobrazí jako upravitelná tabulka (název, typ, částka, od, do, dny zaškrtávátky, priorita); částky jsou jen vzorové (Kč), upravte je, případně pravidlo odeberte. Každý řádek se po úpravě (po krátké pauze) průběžně kontroluje na backendu a problémy se zobrazí přímo pod řádkem; dokud některý řádek obsahuje chybu, tlačítko pro potvrzení je neaktivní (upozornění ho neblokují),
  4. zvolte, co se stávajícími pravidly: přidat (výchozí), nebo nahradit - dosud aktivní pravidla zóny/zařízení se pouze deaktivují (is_active = false, nemažou se, historie zůstane),
  5. potvrďte. Všechna pravidla se zvalidují a vytvoří v jedné transakci - když je jediné neplatné, nevznikne nic (a nic se nedeaktivuje). Chyby se zobrazí i s cestou (např. rules[1].days_of_week[0]).

Zónové šablony

Šablona Režim zóny Obsah
Nonstop, sazba za minutu time_based per_minute 2 Kč/min, bez omezení
Denní a večerní sazba time_based 06:00-18:00 za 2 Kč/min, 18:00-06:00 za 3 Kč/min
Všední dny a víkend time_based Po-Pá 2 Kč/min, So-Ne 3 Kč/min
Prvních 10 minut zdarma + sazba time_based per_minute 2 Kč/min + free_minutes 10
Otevírací doba Po-Pá 6-22, So-Ne 8-20 time_based dvě per_minute pravidla 2 Kč/min s okny a dny
Vstupné (jednotná cena) entry_based flat_fee 80 Kč

Šablony zařízení

Šablona Obsah
Cena za použití per_use 10 Kč
Cena za minutu provozu per_minute 1 Kč
Zdarma free 0 Kč

Otevírací doba se odvozuje z pravidel zóny

Otevírací doba zóny není samostatné nastavení - plyne z časových oken pravidel zóny (kromě free_minutes, které je jen sleva a otevírací dobu nikdy neomezuje):

  • zóna bez pravidel, nebo jen s pravidly bez time_from/time_to, je otevřená nonstop,
  • pokud mají pravidla časová okna, session lze zahájit jen v některém z oken platném pro dnešní den (pravidla omezená na jiné dny se pro dnešek nepočítají),
  • mimo okna se zahájení odmítne (outside_operating_hours); výjimkou je produkt maintenance_access.

Šablona "Otevírací doba Po-Pá 6-22, So-Ne 8-20" proto zároveň nastaví otevírací dobu (v pátek 21:59 otevřeno, v sobotu 07:59 zavřeno, v neděli v 21:00 zavřeno). Šablony "Nonstop", "Denní a večerní" (okna se dotýkají a pokrývají celých 24 hodin), "Všední dny a víkend" a "Prvních 10 minut zdarma" otevírací dobu neomezují.

Rozhraní API

POST /api/v1/admin/zones/{id}/pricing-presets/apply a POST /api/v1/admin/devices/{id}/pricing-presets/apply (pouze administrátor, 401/403 jinak):

{
  "rules": [
    {"name": "Denní sazba", "rule_type": "per_minute", "amount": 2,
     "time_from": "06:00", "time_to": "18:00", "days_of_week": null, "priority": 1}
  ],
  "replace_existing": false
}

U zařízení se místo rule_type použije pricing_model. Odpověď 200: {"created": 1, "ids": ["..."], "deactivated": 0, "warnings": []}; warnings jsou nezávazná upozornění (překrytí pravidel se stejnou prioritou, typ pravidla bez účinku v režimu zóny). Neplatná sada: 400 s issues[] (path, message), nic se nevytvoří. Zóna/zařízení neexistuje: 404. Maximálně 50 pravidel na volání. Nová pravidla jsou vždy aktivní.

Jak se počítá cena relace

Entry-based zóna

  1. Najdi zónové pravidlo flat_fee
  2. Strhni částku z kreditu při vstupu
  3. Hotovo

Time-based zóna

  1. Spočítej dobu používání každého zařízení (z action_logs)
  2. Pro každé zařízení najdi odpovídající cenové pravidlo
  3. Spočítej cenu za zařízení:
    • per_minute: minuty × sazba
    • per_use: počet_zapnutí × cena
    • free: 0
  4. Sečti ceny všech zařízení
  5. Odečti zónové volné minuty (free_minutes)
  6. Aplikuj slevy z produktů
  7. Zaokrouhli výslednou cenu na PRICE_DECIMAL_PLACES desetinných míst (výchozí 0 = celé koruny, půlka se zaokrouhlí směrem od nuly). Rozdíl mezi přesnou a zaokrouhlenou cenou se uloží zvlášť jako zaokrouhlení (price_rounding) — nikdy se nezapočítává do slevy

Rozpad ceny

Po ukončení relace systém vygeneruje cenový rozpad:

  • Celková cena — součet všech poplatků za zařízení + zónové poplatky
  • Poplatky za zařízení — detailní rozpad pro každé zařízení (typ, doba, počet použití, částka)
  • Zónové volné minuty — sleva za volné minuty
  • Sleva — celková sleva (z produktů), jen skutečná snížení ceny
  • Zaokrouhlení — ± rozdíl způsobený zaokrouhlením na PRICE_DECIMAL_PLACES
  • Výsledná cena — po odečtení slev a zaokrouhlení

Cenová účetní kniha (session_price_items)

Pole total_price/discount/price_rounding/final_price/amount_due/paid na relaci jsou pouze odvozené, dopočítané hodnoty — skutečným zdrojem pravdy je kolekce session_price_items: řádkový, needitovatelný (append-only) seznam všech položek, ze kterých se cena relace skládá. Každá položka má:

Pole Popis
kind charge (poplatek, kladná částka), discount (sleva) nebo payment (úhrada — kredit, bankovní převod, ruční úhrada)
label Anglický záložní popis (např. „Rate 20:00-24:00 (60 min)“, „Light (2x)“, „Credit“) — backend nikdy nevrací popis natvrdo v jednom jazyce, viz níže
label_key Strojově čitelný, jazykově neutrální identifikátor šablony popisu (např. zone_rate_range, credit_payment)
label_params Dynamické hodnoty do šablony (časy, počty, název zařízení/produktu, poznámka admina)
amount Částka — poplatky kladné, slevy a platby záporné
source Strojově čitelný původ (zone_rule:<název>, device_rule:<id>, product, credit, bank_tx:<id>, manual, migration)

Frontend zobrazuje popis vždy přeložený podle jazyka přihlášeného uživatele — překládá label_key/label_params přes i18n namespace ledger (frontend/src/i18n/{cs,en,pl,ua}.json), nikdy nezobrazuje anglický label přímo, pokud label_key existuje. Sloupec label slouží jen jako záložní hodnota pro položky zapsané před zavedením label_key (needitovatelné, historické řádky) a pro necitlivé kontexty mimo UI (logy). Název produktu, název zařízení nebo admin poznámka jsou samy o sobě nepřeložitelné — cestují jako hodnota v label_params a frontend je do přeloženého textu jen dosadí.

Odvozená pole se z položek počítají takto:

Pole Význam
total_price Součet poplatků (charge)
discount Skutečná snížení ceny: slevy, produkty a permanentky, ruční slevy a historické úpravy z migrace (nikdy záporné). Nezahrnuje zaokrouhlení
price_rounding Zaokrouhlení — ± rozdíl mezi přesnou cenou (na 2 desetinná místa) a final_price zaokrouhlenou na PRICE_DECIMAL_PLACES. Může být záporné (820,30 → 820 = −0,30) i kladné (818,55 → 819 = +0,45). Při PRICE_DECIMAL_PLACES = 2 je vždy 0
final_price Cena po slevách a zaokrouhlení = součet všech položek kromě úhrad zaokrouhlený na PRICE_DECIMAL_PLACES (nikdy záporná). Úhrady ji nesnižují — po zaplacení zůstává např. 818 Kč
amount_due Kolik zbývá doplatit = součet všech položek včetně úhrad (nikdy záporný). U zaplacené relace 0
paid Relace má poplatek a amount_due ≤ 0

Úhrada (settlement) je položka payment, jejíž source není migration (kredit, bankovní převod bank_tx:*, ruční úhrada manual, kredit při vstupu). Položka payment se source = "migration" je naopak historická úprava ceny a počítá se do discount. Produkty a permanentky jsou slevy, ne úhrady.

amount_due se používá pro QR kód / SPD částku, párování bankovních převodů (VS/SS + částka se musí rovnat amount_due), dluh, upomínky pozdní platby, platební e-maily a součty nezaplaceného. Tržby ve statistikách a „Celkem zaplaceno“ sčítají final_price zaplacených relací.

Platí tyto identity (rozpis platby tedy vždy „sedí“):

  • total_price − discount + price_rounding = final_price
  • součet položek charge a discount + price_rounding = final_price
  • final_price − součet úhrad = amount_due

Úhrada kreditem se zaokrouhluje dolů

Kredit se strhává jen v přesnosti PRICE_DECIMAL_PLACES (zaokrouhleno dolů). Při kreditu 300,50 Kč a ceně 820 Kč se zaplatí 300 Kč, zbylých 0,50 Kč zůstane na účtu a k doplacení je 520 Kč. Vstupné z kreditu se strhává ve stejné zaokrouhlené výši (např. 50 Kč − 15 % = 42,50 → 43 Kč) a kontrola dostatku kreditu používá právě tuto zaokrouhlenou částku (42,80 kreditu na vstupné 43 Kč nestačí; 10,20 kreditu na vstupné 10,40 stačí, účtuje se 10).

Příklad: cena 818 Kč, částečně zaplacená kreditem 300 Kč → final_price 818, discount 0, amount_due 518. Bankovní převod 518 Kč relaci doplatí (do položek se zapíše úhrada −518) a amount_due klesne na 0, final_price zůstane 818.

Například relace přes půlnoc se dvěma různými sazbami (20:00-24:00 a 00:00-06:00) se v session_price_items zobrazí jako dvě samostatné charge položky místo jedné neprůhledné celkové částky — umožňuje to zpětně zjistit přesně, kdy a proč byla jaká částka naúčtována.

Kolekce je čitelná přes REST API (session.user = @request.auth.id || role = 'admin'), zápis je pouze backendový.

Manuální zásah admina

total_price/discount/price_rounding/final_price/amount_due/paid nejde v administraci upravit přímo — jsou to needitovatelná, jen zobrazená pole (stejný důvod jako u zrušeného pole debt, viz task 30: přímá úprava by se rozešla s tím, co říká session_price_items, a při dalším přepočtu by tiše zmizela). Místo toho slouží v editaci relace (Administrace → Sessions) dvě akce, obě zapisují novou položku do session_price_items a výsledná cena/paid se dopočítá sama:

  • Manuální úprava ceny — kladná částka + poznámka přidá charge položku (např. doúčtování za poškozené vybavení), záporná přidá discount položku (např. sleva mimo běžný ceník). Poznámka se stane součástí label_params.note, takže důvod úpravy je vidět přímo v přeloženém rozpisu platby (šablona manual_adjustment/manual_payment v i18n namespace ledger).
  • Označit jako zaplaceno — zapíše payment položku na přesně tolik, kolik relace aktuálně dluží (nikdy méně ani více), s poznámkou jako důvodem (např. „Sponzorováno obcí" pro turnaj, který nemusí platit za hřiště). Pokud je relace už zaplacená, jde o no-op. Cena relace (final_price) se nemění, jen amount_due klesne na 0.

Obě akce mají source = "manual" a jsou dostupné jen adminovi (POST /api/v1/admin/sessions/{id}/manual-adjustment, POST /api/v1/admin/sessions/{id}/manual-payment).

Zobrazení v UI

Rozpis platby se zobrazuje po rozkliknutí tlačítka „Detail" u konkrétní relace — jak v uživatelské historii (Historie), tak v administraci (Administrace → Sessions). Frontend načítá session_price_items přímo přes PocketBase JS SDK (bez zvláštního REST endpointu) a zobrazí:

  • Rozpis ceny — řádky Poplatek / Sleva / Úprava (s haléři), řádek Zaokrouhlení (jen pokud je nenulové) a řádek Celkem (final_price, cena po slevách a zaokrouhlení),
  • samostatnou sekci Úhrady — Kredit, Bankovní převod, Ruční úhrada,
  • stav: Zaplaceno, nebo Zbývá doplatit X (amount_due).

Toto zobrazení je oddělené od e-mailu s platební výzvou — ten obsahuje pouze částku k úhradě (amount_due).

V tabulkách historie a administrace relací je navíc sloupec Zaokr. (price_rounding).