Internal Documentation internal
TalkIDE internal documentation

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:

DokumentCoStav
UC-10008 — Marže reálně do účtováníPhase-2a, model-agnosticKOMPLETNÍ, implementovatelná
UC-10009 — Postpaid hosting model (DESIGN)Phase-2b, přeodvozené Open Decisions pod postpaidDESIGN / č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 → tabulka hosting_cost_events (HostingCostEventEntity): raw hosting cost per tenant namespace / time window. Money je NUMERIC(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 (changeset 0031) — single-row (deterministický singleton id=1, vzor platform_kill_switch), sloupce ai_markup_percent, hosting_markup_percent, updated_by_user_id, updated_at. Seed default ai=30, hosting=50 z pricing-markup.yaml (env-overridable PRICING_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 v usage_events / hosting_cost_events se 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á RAW costUsd (z UsagePricingCalculator.calculate), PricingService se zde vůbec nevolá.
  • grep přes celé src/main: calculateChargedCostjediného volajícího — sám PricingService. PricingService je referencován pouze z usage/GetMyUsageUseCase, usage/GetUsageBreakdownUseCase, pricing/Admin*/Get*/Update* — tj. výhradně read/zobrazovací cesty. Žádný reconciliation / quota / budget-debit kód PricingService nevolá.
  • DecrementBudgetUseCase odečítá z user_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 v PublishService: když hosting_credit_usd < estimateCostHostingCreditExhaustedException → HTTP 402 HOSTING_BUDGET_EXCEEDED.
  • ALE: estimate je fixní placeholder $0.50 per Publish (PUBLISH_ESTIMATE_USD), do hosting_credit_usd se nikde nedobíjí (žádný topup), nikde neodečítá podle reálné spotřeby z hosting_cost_events, a hostingCreditUsd je 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:

FondCo 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 appsBlokuje/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)

  1. C.2 poller zapíše raw řádek do hosting_cost_events (beze změny).
  2. NOVÝ periodický reconciler vezme nezúčtované hosting_cost_events, pro každý spočte charged = PricingService.applyMarkup(raw, hostingMarkupPercent), zapíše DEBIT řádek do hosting_credit_ledger (s raw + charged + markup snapshot + FK na cost event) a atomicky odečte charged z user_budget.hosting_credit_usd.
  3. Idempotence: hosting_cost_event_id v ledgeru UNIQUE pro source=HOSTING_USAGE ⇒ stejný cost event se nezúčtuje dvakrát.
  4. 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 v UpdatePricingMarkupUseCase musí povolit 0 (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ísto decrementBudgetUseCase(userId, costUsd) odečíst pricingService.applyMarkup(costUsd, aiMarkup). Do usage_events se RAW cost_usd/cost_cents nemění (immutable); charged se buď ukládá do nového sloupce/ledgeru, nebo se odvozuje.
    • Reconciliation/quota: usage_events zů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?).
  • 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).
  • Scheduler/po-debit hook: když hosting_credit_usd < threshold_usd a autopay enabled a < max_per_period_usd v daném období → vytvoř PaymentIntent (off-session, uloženou kartou z UC-10001), trigger=AUTOPAY. Webhook payment_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.succeeded handler o větvení podle metadata: metadata.fund = "AI" | "HOSTING" (+ topupId/ hostingTopupId). Idempotence přes sdílenou stripe_webhook_events (UC-10006) + per-topup status (UC-10007 vzor).
  • hosting_credit_topup je strukturní klon credit_topup (UC-10007) — stejný PENDING→SUCCEEDED/FAILED lifecycle, stejný confirmCardPayment FE 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}/status
    • GET/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:

  1. Ledger jako účetní pravda, ne balance. hosting_credit_ledger je 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”.
  2. source na topupu/ledgeru je enum, ne bool. Snadno přibude SUBSCRIPTION_INVOICE / SUBSCRIPTION_INCLUDED bez schema breaku.
  3. Marže je multiplikativní a snapshotovaná → funguje stejně pro prepaid odečet i pro postpaid fakturu (charged = co se fakturuje).
  4. 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ů).
  5. 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:

  • USAGE mockup → 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 s EnforceHostingBudgetUseCase na 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).

Was this page helpful?

Thanks for the feedback.