STATUS: HISTORICKÁ FÁZE-1 ANALÝZA. PREPAID NÁVRH PŘEKONÁN USER PIVOTEM NA POSTPAID. Tento dokument zůstává jako platný záznam stavu-as-is (co C.1–C.3 reálně postavily — sekce a) a jako historie Open Decisions. Aktivní deliverable je jinde:
Dokument Co Stav UC-10008 — Marže reálně do účtování Phase-2a, model-agnostic KOMPLETNÍ, implementovatelná UC-10009 — Postpaid hosting model (DESIGN) Phase-2b, přeodvozené Open Decisions pod postpaid DESIGN / čeká na user potvrzení DP-0..DP-6 PIVOT (závazné): hosting jde POSTPAID (metered akruál → měsíční Stripe charge, dle ADR-103
- design handoff). ADR-103 zůstává platné a NEdotčené — postpaid je s ním v souladu, rozpor z FEEDBACK sekce d) tímto zaniká. Prepaid fond + topup + autopay (sekce b) níže) = degradováno na future „doors-open” (UC-10009 sekce 6), nestaví se teď. Open Decisions D1–D8 níže jsou přeodvozeny pod postpaid v UC-10009 sekci 4 (D6=A a D7=A už rozhodnuto userem).
Sekce a) (stav-as-is) je stále přesná a závazná. Sekce b)–d) čti jako prepaid historii — aktivní rozhodnutí jsou v UC-10009.
a) Stav-as-is — co C.1–C.3 reálně postavily a kde je díra vůči vizi
Co reálně existuje (ověřeno čtením kódu talkide-be main, ne předpoklad)
C.2 — OpenCost hosting cost poller (be#25, commit b07d9e6)
HostingCostPoller(scheduler) →RecordHostingCostBatchProcessor→ tabulkahosting_cost_events(HostingCostEventEntity): raw hosting cost per tenant namespace / time window. Money jeNUMERIC(20,6)BigDecimal.- Je to čistě záznam raw nákladu. Nikde se z něj nic neodečítá z kreditu,
není napojen na
user_budget, nemá enforcement.
C.3 — runtime marže config + charged cost (be#26, commit 76e64a7, 543cd73)
- Tabulka
pricing_markup_config(changeset0031) — single-row (deterministický singleton id=1, vzorplatform_kill_switch), sloupceai_markup_percent,hosting_markup_percent,updated_by_user_id,updated_at. Seed default ai=30, hosting=50 zpricing-markup.yaml(env-overridablePRICING_MARKUP_AI_PERCENT/PRICING_MARKUP_HOSTING_PERCENT). - Admin endpoint
GET/PUT /api/v1/admin/pricing/markup(AdminPricingController,GetPricingMarkupUseCase,UpdatePricingMarkupUseCase). PricingService.calculateChargedCost(rawCost, kind)/applyMarkup(rawCost, percent)—charged = raw * (1 + percent/100), scale 6 HALF_UP. Komentář v kódu explicitně říká: “RAW costs vusage_events/hosting_cost_eventsse NIKDY nemutují; markup je čistě read-side.”- Endpoint
GET /api/v1/users/me/usage/breakdown(GetUsageBreakdownUseCase) — ai/hosting × raw+charged+markup + totals. Pouze čte a zobrazuje.
Díra vůči vizi — POTVRZENO
POTVRZUJI tvrzení ze zadání: AI marže se uživateli reálně NEúčtuje, jen ukazuje.
Důkaz z kódu (ne odhad):
RecordUsageEventUseCase(řádek ~96) po zápisu usage eventu voládecrementBudgetUseCase(userId, costUsd)— odečítá RAWcostUsd(zUsagePricingCalculator.calculate),PricingServicese zde vůbec nevolá.greppřes celésrc/main:calculateChargedCostmá jediného volajícího — sám PricingService.PricingServiceje referencován pouze zusage/GetMyUsageUseCase,usage/GetUsageBreakdownUseCase,pricing/Admin*/Get*/Update*— tj. výhradně read/zobrazovací cesty. Žádný reconciliation / quota / budget-debit kód PricingService nevolá.DecrementBudgetUseCaseodečítá zuser_budget.ai_credit_usdčistou raw částku.
⇒ Marže (AI i hosting) je dnes kosmetika v breakdownu. Reálné peníze (odečet z kreditu, quota/reconciliation) jedou na RAW.
Co z hosting fondu už NÁHODOU existuje (důležité — nestavět od nuly)
UserBudgetEntity (tabulka user_budget) už má sloupce:
hosting_credit_usd, hosting_credit_initial_usd (NUMERIC(10,2), default 0) —
přidané ve Stopa B.7 jako placeholder.
EnforceHostingBudgetUseCase(Stopa B.7, ADR-022 §7) existuje a je volán před PROD buildem vPublishService: kdyžhosting_credit_usd < estimateCost→HostingCreditExhaustedException→ HTTP 402HOSTING_BUDGET_EXCEEDED.- ALE: estimate je fixní placeholder
$0.50per Publish (PUBLISH_ESTIMATE_USD), dohosting_credit_usdse nikde nedobíjí (žádný topup), nikde neodečítá podle reálné spotřeby zhosting_cost_events, ahostingCreditUsdje v MVP fakticky vždy 0 ⇒ enforcement dnes blokuje Publish komukoliv bez ručně nasypaného kreditu (resp. je celá větev v praxi mrtvá/obejitá seedem).
Závěr stav-as-is: základní stavební kameny stojí (raw hosting cost
záznamy, marže config + math, hosting credit sloupce, enforcement hook na
Publish), ale chybí celý “tok peněz”: žádný hosting topup, žádný autopay,
žádné napojení hosting_cost_events → odečet z hosting_credit_usd, žádný
oddělený ledger, a marže se reálně neúčtuje ani u AI.
b) Návrh cílového modelu (FÁZE 1 — koncept, ne finální spec)
B.1 Princip: dva oddělené fondy, decoupled enforcement
Rationale (závazný, zapsat do finální UC): AI vývoj nesmí vyčerpat hosting budget a tím shodit běžící produkční aplikace. Vyčerpání AI kreditu ≠ shození hostingu. Proto dva fyzicky oddělené fondy a oddělené enforcement cesty:
| Fond | Co platí | Enforcement při vyčerpání |
|---|---|---|
AI credit (ai_credit_usd) | Mara/Anthropic tokeny (vývoj v workspace, preview generování) | Blokuje další AI inference (gateway/quota). Neshazuje běžící apps. |
Hosting credit (hosting_credit_usd) | OpenCost infra: pod runtime, storage, bandwidth published + preview apps | Blokuje/pozastaví hosting (rozsah = Open Decision D4). Nezávislé na AI. |
user_budget sloupce hosting_credit_usd / hosting_credit_initial_usd se
recyklují (už existují) — žádná nová tabulka pro balance není nutná.
B.2 Datový model (náčrt)
user_budget (EXISTUJE, recyklujeme)
├─ ai_credit_usd / ai_credit_initial_usd (beze změny)
├─ hosting_credit_usd / hosting_credit_initial_usd (aktivovat — dnes mrtvé)
└─ spending_limit_usd (UC-10005, beze změny)
hosting_credit_topup (NOVÁ — analog credit_topup z UC-10007)
id, user_id, amount_usd, stripe_payment_intent_id,
status PENDING|SUCCEEDED|FAILED, trigger MANUAL|AUTOPAY,
created_at, completed_at
hosting_autopay_config (NOVÁ — opt-in autopay)
user_id (PK, 1:1 user), enabled bool,
threshold_usd (když hosting_credit_usd klesne pod → nabít),
topup_amount_usd (o kolik nabít / na jaký strop — viz D-rozsah),
max_per_period_usd (uživatelský bezpečnostní strop — povinné),
updated_at
hosting_credit_ledger (NOVÁ — append-only pohyby fondu)
id, user_id, type CREDIT|DEBIT,
source TOPUP|AUTOPAY|HOSTING_USAGE|GRANT|ADJUSTMENT,
raw_amount_usd, charged_amount_usd, markup_percent_snapshot,
hosting_cost_event_id (nullable FK), ref_id, created_at
hosting_cost_events (C.2, EXISTUJE) — raw, IMMUTABLE, beze změny
pricing_markup_config (C.3, EXISTUJE) — beze změny (viz B.4 ohledně 0)
Klíčový invariant (raw zůstává raw): hosting_cost_events a
usage_events se NIKDY nemutují. Marže se aplikuje při přechodu raw →
charged v ledgeru: do fondu se odečte charged_amount_usd
(= raw * (1 + markup/100)), ale raw_amount_usd i
markup_percent_snapshot se uloží zvlášť pro auditovatelnost a re-derivaci.
Ledger je append-only — žádná destrukce historie.
B.3 Tok raw → charged → debit (hosting)
- C.2 poller zapíše raw řádek do
hosting_cost_events(beze změny). - NOVÝ periodický reconciler vezme nezúčtované
hosting_cost_events, pro každý spočtecharged = PricingService.applyMarkup(raw, hostingMarkupPercent), zapíše DEBIT řádek dohosting_credit_ledger(s raw + charged + markup snapshot + FK na cost event) a atomicky odečtechargedzuser_budget.hosting_credit_usd. - Idempotence:
hosting_cost_event_idv ledgeru UNIQUE prosource=HOSTING_USAGE⇒ stejný cost event se nezúčtuje dvakrát. - Při poklesu pod práh → varování (D2) a/nebo autopay (B.5). Při vyčerpání → enforcement (D4).
Analogicky AI (samostatná, menší změna — viz B.6): na debit-side použít
PricingService místo raw costUsd.
B.4 Marže reálně do účtování (samostatná, oddělitelná změna)
- Obě marže (ai, hosting) zůstávají v
pricing_markup_config, konfigurovatelné adminem (už hotovo C.3). Požadavek: obě musí snést hodnotu 0 (marži lze vypnout). Ověřit a doplnit: validace vUpdatePricingMarkupUseCasemusí povolit0(a zakázat záporné);applyMarkup(raw, 0)→raw * 1 = raw(math už 0 unese, ověřeno — potřeba jen povolit na vstupu, pokud dnes vyžaduje > 0). - Změna účtovací cesty:
- AI:
RecordUsageEventUseCase→ místodecrementBudgetUseCase(userId, costUsd)odečístpricingService.applyMarkup(costUsd, aiMarkup). Dousage_eventsse RAWcost_usd/cost_centsnemění (immutable); charged se buď ukládá do nového sloupce/ledgeru, nebo se odvozuje. - Reconciliation/quota:
usage_eventszůstává raw SSoT. Reconciliation report (UC-08005) musí nově rozlišovat raw vs. charged (co se reálně odečetlo z kreditu = charged). Quota okna (Redis, UC-08002/08006) — Open Decision D7 (počítají se z raw nebo charged?).
- AI:
- Tato část je menší a nezávislá na velkém hosting fondu — dá se dodat první (viz Open Decision D6).
B.5 Autopay (opt-in, se stropem)
Analog UC-10007 PaymentIntent flow, ale do hosting fondu a triggered automaticky:
hosting_autopay_config.enabled = true+threshold_usd+topup_amount_usd- povinný
max_per_period_usd(bezpečnostní strop, user-set).
- povinný
- Scheduler/po-debit hook: když
hosting_credit_usd < threshold_usda autopay enabled a< max_per_period_usdv daném období → vytvoř PaymentIntent (off-session, uloženou kartou z UC-10001),trigger=AUTOPAY. Webhookpayment_intent.succeeded→ připíše hosting credit (sdílená webhook infra UC-10006). - Off-session platba může selhat (SCA/karta) — chování = Open Decision D5.
B.6 Napojení na existující Stripe (UC-10006 / UC-10007)
- Žádná nová webhook infra. Rozšířit existující
StripeWebhookController/payment_intent.succeededhandler o větvení podlemetadata:metadata.fund = "AI" | "HOSTING"(+topupId/hostingTopupId). Idempotence přes sdílenoustripe_webhook_events(UC-10006) + per-topup status (UC-10007 vzor). hosting_credit_topupje strukturní kloncredit_topup(UC-10007) — stejný PENDING→SUCCEEDED/FAILED lifecycle, stejnýconfirmCardPaymentFE flow, jen jiný fond na webhook připsání.- API náčrt (finalizovat po Open Decisions):
POST /api/v1/users/me/billing/hosting/topup(analog UC-10007)GET /api/v1/users/me/billing/hosting/topup/{id}/statusGET/PUT /api/v1/users/me/billing/hosting/autopay(autopay config)- hosting breakdown už pokryje rozšíření existujícího
GET /api/v1/users/me/usage/breakdown(C.3) — viz D8 ohledně estimate.
B.7 Future-proof: model NESMÍ zavřít dveře měsíčním platbám (subscription)
Toto je architektonicky závazné (požadavek 5). Důvody, proč model subscription neuzavírá + co konkrétně to zajišťuje:
- Ledger jako účetní pravda, ne balance.
hosting_credit_ledgerje append-only zdroj pohybů. Prepaid model = balance jeΣ CREDIT − Σ DEBIT. Subscription model = stejné DEBIT řádky (HOSTING_USAGE) se na konci období sečtou do faktury místo aby snižovaly prepaid balance. Účtovací (debit) cesta je identická; mění se jen co se děje s běžícím součtem (snižuje prepaid vs. akumuluje do měsíční faktury). Žádná migrace dat, jen nový “settlement mode”. sourcena topupu/ledgeru je enum, ne bool. Snadno přibudeSUBSCRIPTION_INVOICE/SUBSCRIPTION_INCLUDEDbez schema breaku.- Marže je multiplikativní a snapshotovaná → funguje stejně pro prepaid odečet i pro postpaid fakturu (charged = co se fakturuje).
- Stripe vrstva už je “PaymentIntent + uložená karta”, ne Checkout. Stripe Subscriptions/Invoicing se přidá vedle, ne místo (UC-10006 webhook už umí přijmout víc event typů).
- Pozn. k design handoffu:
design_handoff_talkide/profile.jsxříká “Pay-as-you-go. You’re charged on the 1st of each month for what you used.” — to je postpaid/měsíční framing, NE prepaid. To podtrhuje, proč future-proof není teoretický: produktová vize už dnes mluví o měsíčním účtování. Rozhodnutí prepaid-teď vs. kolik subscription podpory hned = Open Decision D6.
Co se teď NEimplementuje: skutečné Stripe Subscriptions, generování měsíční faktury, proration. Pouze se drží datový model + Stripe vrstva tak, aby to později šlo přidat bez destruktivní migrace.
c) Revize framingu fe#10 (C.4)
V kódu je C.4 reprezentováno TODO markery:
BillingSection.vue:49 a :136 — “TODO Stopa C.4: replace with real hosting
usage aggregation endpoint”. Konstanta USAGE je plně hardcoded mockup:
projectHours: 142 / cap 200, storageGb 12 / 50, bandwidthGb 8 / 30,
estimated: 24.20, nextInvoice = 1. den příštího měsíce.
Co z fe#10/C.4 platí:
- Vizuální struktura “Hosting & infrastructure” boxu (HostingCreditPanel + estimated number + 3 usage bary + “next invoice on” + “View breakdown”) zůstává — design je dobrý, naváže se na něj, nestaví se paralelní UI.
- “View breakdown” tlačítko → napojit na existující C.3
GET /api/v1/users/me/usage/breakdown(hosting část).
Co se mění / co fe#10 musí dořešit:
USAGEmockup → reálná data. ALE: caps (200 hrs / 50 GB / 30 GB) a per-unit ceny ($0.18/hr, $0.10/GB, $0.05/GB) implikují subscription/tier model s bundlem, zatímco hosting fond je prepaid $ balance. Tyto dva pohledy je nutné sladit (Open Decision D8 — je estimate závazný kontrakt nebo placeholder?).- “Estimated this month” + “next invoice on 1st” je postpaid framing, ale zbytek návrhu (fond + topup + autopay) je prepaid. Tento rozpor MUSÍ vyřešit user v Open Decisions (D6/D8), ne já.
- Přidat do FE: hosting topup tlačítko (analog “Add credit” z UC-10007) + autopay nastavení (toggle + threshold + max strop).
d) OPEN DECISIONS — konsolidovaný seznam pro usera
Nerozhoduji sám. U každého varianty + mé doporučení. Relayuj userovi.
D1 — Počáteční hosting grant / trial?
Dostane nový user nějaký hosting kredit zdarma (aby si mohl Publish vyzkoušet bez platby)?
- A) Žádný grant — Publish vyžaduje hosting topup od první minuty.
- B) Malý fixní welcome grant (analog AI $10 z signup), např. $3–5.
- C) Časově/objemově omezený trial (X dní hostingu zdarma).
- Doporučení: B, malý grant ($3–5). Konzistentní s existujícím AI welcome grantem, odstraní friction pro první Publish (alpha milník), strop nákladu je malý. C je over-engineering pro alfu.
D2 — Prahy varování (% vs. absolutní)
Kdy varovat usera, že hosting kredit dochází?
- A) Procentuální z
hosting_credit_initial_usd(analog UC-08006 50/85/95). - B) Absolutní $ práh (např. < $5 a < $1).
- C) Kombinace + autopay threshold jako implicitní práh.
- Doporučení: B (absolutní). Prepaid fond nemá smysluplné “initial” pro % (dobíjí se nepravidelně, na rozdíl od AI initial). Absolutní práh ($5 amber, $1 rose) je pro usera srozumitelnější u hostingu. Autopay threshold (pokud zapnut) potlačí varování.
D3 — Grace period při vyčerpání hosting kreditu
Když hosting_credit_usd dosáhne 0, co hned?
- A) Žádná grace — okamžitě enforcement (D4).
- B) Grace period X (24–72 h): apps běží dál, user dostane urgentní notifikaci, pak teprve enforcement.
- C) Grace jen pro published prod apps, preview hned.
- Doporučení: B, 48 h grace. Shození běžící produkční appky bez varování je nejhorší UX/reputační riziko (přesně to, čemu má decoupling zabránit). 48 h dá userovi reálný čas zaplatit. Hodnota grace = konfigurovatelná.
D4 — Co přesně se zastaví (published vs. preview/dev) a vztah preview→AI vs hosting
- A) Vyčerpání hosting kreditu zastaví VŠECHNO (published prod + preview/dev apps).
- B) Zastaví jen published prod apps; preview/dev apps jedou dál (preview compute se počítá jinam).
- C) Zastaví published prod; preview/dev se účtuje proti AI fondu (preview = součást vývoje), takže hosting vyčerpání preview neovlivní.
- Otevřená pod-otázka: patří preview/dev pod runtime cost vůbec do hosting fondu, nebo do AI (jako součást “vývoje”)? C.2 poller dnes loguje per tenant ns — preview i prod jsou samostatné ns.
- Doporučení: C. Nejlépe naplňuje rationale “AI vyčerpání ≠ shození hostingu” a symetricky “hosting vyčerpání ≠ zablokování vývoje”. Preview = součást iterace → logicky AI/vývojový fond; published prod = hosting fond. Vyžaduje, aby C.2 raw cost klasifikoval ns na PREVIEW vs PROD (ověřit, že ns naming to umožňuje — pravděpodobně ano dle CLAUDE.md tenant-env modelu). Je to nejvíc práce, ale architektonicky nejčistší a v souladu s vizí. Pokud user chce minimální rozsah pro alfu → fallback B.
D5 — Chování při selhání autopay platby
Off-session autopay PaymentIntent selže (karta declined / SCA required)?
- A) Apps padají hned (žádná ochrana).
- B) Spadne do D3 grace period (jako manuální vyčerpání) + notifikace “autopay selhal, zaplať ručně”.
- C) Retry autopay X× s backoffem, pak teprve grace.
- Doporučení: B (+ lehký C: 1 retry za 6 h). Selhání autopay je ekvivalent vyčerpání → stejná grace ochrana jako D3. Jeden retry pokryje tranzientní Stripe/SCA glitch. Agresivní retry = riziko opakovaných failnutých plateb / Stripe penalizace.
D6 — Pořadí dodání: “AI marže do účtování” (malé) vs. velký hosting fond
- A) Nejdřív malá nezávislá změna “marže (AI+hosting) reálně do účtování + obě snesou 0”, pak velký hosting fond.
- B) Všechno najednou jako jeden velký balík.
- C) Nejdřív hosting fond, marže do účtování až potom.
- Doporučení: A. “Marže do účtování” je malá, dobře testovatelná, nezávislá změna s okamžitou business hodnotou (začne se reálně účtovat marže, kterou už dnes jen ukazujeme — přímý dopad na revenue). Velký hosting fond je víc UC (topup, autopay, ledger, reconciler, enforcement) a potřebuje rozhodnuté D1–D5/D8. Doručit A první, izolovaně ověřit dopad na reconciliation/quota, pak stavět fond.
D7 — Rozsah future-proof subscription
Jak daleko jít teď kvůli budoucímu subscription modelu?
- A) Jen datový model neuzavřít (ledger append-only, source enum, charged snapshot) — žádný subscription kód.
- B) A + abstraktní “settlement mode” seam (prepaid vs. postpaid) připravený v doméně, ale jen prepaid implementace.
- C) Rovnou implementovat i postpaid/měsíční fakturaci.
- Doporučení: A. Požadavek 5 explicitně říká “neimplementovat teď, ale návrh to musí unést”. A přesně to splňuje s minimem rizika a práce. B je spekulativní abstrakce dokud nevíme, jak subscription reálně bude vypadat (YAGNI). C je mimo rozsah Stopy C.
D8 — Je hosting estimate na FE výpočetně závazný kontrakt, nebo placeholder?
Mockup USAGE má caps (200 hrs / 50 / 30 GB), per-unit ceny a “estimated
$24.20 / next invoice 1st”. To je tier/subscription jazyk uvnitř prepaid
fondu.
- A) Placeholder — FE jen zobrazí reálný součet charged hosting nákladů z C.3 breakdownu za aktuální období; žádné caps, žádné “next invoice”, jazyk se změní na “Hosting credit balance + spotřeba tento měsíc”.
- B) Závazný kontrakt — estimate je odhad měsíčního nákladu (lineární extrapolace dosavadní spotřeby), caps jsou informativní limity tieru.
- C) Hybrid — zobrazit reálnou spotřebu (A) + nezávazný “projected end-of-month” odhad jako pomůcku, bez caps a bez “invoice” jazyka.
- Doporučení: C. Reálná data jsou must (A). Lehký projected odhad je užitečný UX pro prepaid (kdy dojde kredit) bez toho, aby předstíral tier/subscription, který neexistuje. Caps a “next invoice” jazyk z mockupu odstranit, dokud subscription model reálně nepřijde (D7=A). Estimate není API kontrakt — je to FE-side projekce z breakdown dat.
FEEDBACK
- Hosting credit sloupce už existují v
user_budget(Stopa B.7) i sEnforceHostingBudgetUseCasena Publish — ale s mrtvým fixním estimate$0.50. Doporučuji to v navazující práci buď aktivovat (napojit na fond + reálný estimate) nebo explicitně deprecovat; nenechávat třetí polo-mrtvou cestu vedle nové. - Rozpor produktové vize: design handoff (
profile.jsx) i hosting-architecture spec (ADR-103: “Tiered subscription flat $/měs + metered overage”) mluví subscription/postpaid, zatímco zadání Stopy C žádá prepaid fond + topup + autopay. To není detail — je to fundamentální produktové rozhodnutí (D6/D7/D8). Doporučuji userovi explicitně potvrdit: “prepaid teď, subscription jako budoucí evoluce” — návrh to unese, ale chci to slyšet potvrzené, ať nestavíme proti vlastní spec/ADR-103 (možná chce ADR-103 revidovat/superseednout). - Doporučuji pořadí D6=A: “marže reálně do účtování” doručit jako první samostatnou malou UC — nejvyšší hodnota / nejmenší riziko, a odblokuje to reálné účtování marže, kterou už dnes jen ukazujeme (přímý revenue impact).
- Záměrně jsem nepsal sekvenční diagramy/API kontrakty/test-case tabulky — to je FÁZE 2 po rozhodnutí Open Decisions (zadání to explicitně zakazuje dělat dřív).
Thanks for the feedback.