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:00platí od 06:00:00 do 17:59:59. - Když je
time_fromvě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, jentime_to= od půlnoci do tohoto času. time_from=time_toje 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,
amountje číslo ≥ 0,priorityje celé číslo ≥ 0,time_from/time_tojsou prázdné neboHH:MM(stejná kontrola jako dříve, přijímá se i např.7:5),days_of_weekjenull, 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í.
- Filtruje aktivní pravidla (
is_active = true) - Filtruje podle času (
time_from/time_to) - Filtruje podle dne v týdnu (
days_of_week) - Seřadí podle priority sestupně
- 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:
- vyberte zónu (nebo zařízení),
- vyberte šablonu - u zón se nabízejí jen šablony odpovídající režimu zóny (
time_based/entry_based), - 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í),
- 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), - 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 produktmaintenance_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¶
- Najdi zónové pravidlo
flat_fee - Strhni částku z kreditu při vstupu
- Hotovo
Time-based zóna¶
- Spočítej dobu používání každého zařízení (z action_logs)
- Pro každé zařízení najdi odpovídající cenové pravidlo
- Spočítej cenu za zařízení:
per_minute:minuty × sazbaper_use:počet_zapnutí × cenafree:0
- Sečti ceny všech zařízení
- Odečti zónové volné minuty (
free_minutes) - Aplikuj slevy z produktů
- Zaokrouhli výslednou cenu na
PRICE_DECIMAL_PLACESdesetinný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
chargeadiscount+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á
chargepoložku (např. doúčtování za poškozené vybavení), záporná přidádiscountpolož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 (šablonamanual_adjustment/manual_paymentv i18n namespaceledger). - Označit jako zaplaceno — zapíše
paymentpolož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í, jenamount_dueklesne 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).