Internal Documentation internal
TalkIDE internal documentation

Marže (ai_markup_percent, hosting_markup_percent z pricing_markup_config, C.3) se přestane jen zobrazovat a začne se reálně promítat do odečtu z kreditu a do reconciliation/quota. RAW data zůstávají immutable; charged je odvozená částka aplikovaná v okamžiku debitu/akruálu. Obě marže musí snést hodnotu 0 (vypnutá marže = chování jako dnes = žádná regrese). Model-agnostic změna — nezávislá na pre/postpaid hosting modelu (Phase-2b).

  • Kontext (potvrzeno čtením kódu, viz UC-10008-PHASE1-ANALYSIS): dnes RecordUsageEventUseCase po zápisu usage eventu volá decrementBudgetUseCase(userId, costUsd) s RAW costUsd. PricingService.calculateChargedCost / applyMarkup má jediného volajícího — sám sebe — a je referencován výhradně z read/display cest (GetMyUsageUseCase, GetUsageBreakdownUseCase, Admin*Pricing*). ⇒ marže je dnes kosmetika v breakdownu, reálné peníze jedou na RAW.
  • Cíl této UC: charged_amount = applyMarkup(raw, markup_percent) se reálně odečte z kreditu a stane se hodnotou, kterou reconciliation/quota porovnává jako „co user reálně utratil”. Týká se AI debit cesty teď; hosting debit/akruál cesta dostává stejný markup-aware kontrakt, ale její reálné napojení na fond/akruál řeší Phase-2b (UC-10009).
  • Invariant — raw zůstává raw: usage_events (cost_usd/cost_cents) a hosting_cost_events se NIKDY nemutují. Markup se aplikuje až při přechodu raw → charged v okamžiku debitu. Charged + použitý markup_percent_snapshot se ukládá zvlášť pro auditovatelnost a re-derivaci.
  • Marže = 0 ⇒ žádná regrese: applyMarkup(raw, 0) = raw * (1 + 0/100) = raw, scale 6 HALF_UP. Pokud admin nastaví obě marže na 0, odečet z kreditu je bit-identický s dnešním RAW chováním. Toto je vynucený akceptační požadavek (test case níže).
  • Marže ≥ 0, ne > 0: UpdatePricingMarkupUseCase validace musí povolit 0 a zakázat záporné hodnoty. Pokud dnešní validace vyžaduje > 0, mění se na >= 0. Horní mez ponechána dle stávající config (žádná nová horní mez touto UC).
  • Marže změněná adminem za běhu: markup se aplikuje v okamžiku debitu a do charged řádku se uloží markup_percent_snapshot použité hodnoty. Již zaúčtované debety se NEPŘEPOČÍTÁVAJÍ (immutable historie). Nová admin hodnota platí pro všechny budoucí debety od okamžiku PUT.
  • Reconciliation/quota: usage_events zůstává raw single-source-of-truth pro spotřebu. Reconciliation report (UC-08005) a billing/quota pohledy (UC-08002 / UC-08006) nově rozlišují raw vs. charged: „kolik se reálně odečetlo z kreditu” = charged. Rolling Redis kvótová okna (UC-08002) se počítají z charged (= co user reálně utrácí) — viz Open Decision rozhodnuté níže (Q7).
  • DB migration: nový changeset 0030-add-charged-cost-to-usage-events.xml — přidává odvozené, ne-mutující sloupce na usage_events (raw zůstává nedotčen). Liquibase immutable (production phase) — nový soubor, nikdy edit existujícího.
  • Žádná nová Stripe webhook infra, žádný nový HTTP endpoint pro user/admin (markup config endpoint už existuje z C.3 — jen validace >= 0).
  • Related: UC-08001 (gateway emituje usage events), UC-08005 (raw vs charged v reconciliation), UC-08006 (charged v billing view), UC-10009 (hosting postpaid — používá stejný markup kontrakt).

Sekvence — debit cesty s reálně aplikovanou marží

sequenceDiagram
    actor User
    participant FE
    participant GW as AnthropicGatewayService
    participant RUE as RecordUsageEventUseCase
    participant PS as PricingService
    participant DEC as DecrementBudgetUseCase
    participant DB

    User->>+FE: odešle zprávu (Mara turn)
    FE->>+GW: turn (přes UC-08001)
    GW->>+RUE: recordUsageEvent(userId, rawCostUsd, tokens, ...)

    RUE->>DB: INSERT usage_events (cost_usd = RAW, immutable)

    RUE->>+PS: applyMarkup(rawCostUsd, aiMarkupPercent)
    Note over PS: charged = raw * (1 + percent/100)<br/>scale 6 HALF_UP<br/>percent = 0 ⇒ charged == raw
    PS-->>-RUE: chargedCostUsd + markupPercentSnapshot

    RUE->>DB: UPDATE usage_events SET charged_cost_usd, markup_percent_snapshot<br/>(odvozené sloupce, RAW cost_usd nedotčen)

    RUE->>+DEC: decrementBudget(userId, chargedCostUsd)
    Note over DEC: dříve: decrementBudget(userId, rawCostUsd)
    DEC->>DB: UPDATE user_budget SET ai_credit_usd = ai_credit_usd - chargedCostUsd
    DEC-->>-RUE: ok

    RUE-->>-GW: ok
    GW-->>-FE: PM odpověď
    FE-->>-User: zobraz odpověď

    Note over RUE,DB: Reconciliation (UC-08005) a quota okna (UC-08002)<br/>nově čtou charged_cost_usd jako "reálně odečteno".<br/>RAW cost_usd zůstává SSoT pro spotřebu.

Interní kontrakt (žádný nový HTTP endpoint)

Tato UC nepřidává nový user/admin endpoint. Mění se interní debit cesta a rozšiřuje datový model.

PricingService (existuje z C.3 — beze změny chování, ověřit 0)

// charged = raw * (1 + percent / 100), scale 6 HALF_UP
fun applyMarkup(rawCost: BigDecimal, percent: BigDecimal): BigDecimal
fun calculateChargedCost(rawCost: BigDecimal, kind: MarkupKind): ChargedResult

ChargedResult (nově vystavený do debit cesty, ne jen read):

data class ChargedResult(
    val rawAmountUsd: BigDecimal,        // = vstupní raw, beze změny
    val chargedAmountUsd: BigDecimal,    // raw * (1 + percent/100)
    val markupPercentSnapshot: BigDecimal // hodnota percent v okamžiku výpočtu
)

RecordUsageEventUseCase — změna debitu

PŘED:  decrementBudgetUseCase(userId, costUsd)                       // RAW
PO:    val charged = pricingService.calculateChargedCost(costUsd, AI)
       // INSERT usage_events ... cost_usd = costUsd (RAW, immutable)
       // UPDATE usage_events ... charged_cost_usd = charged.chargedAmountUsd,
       //                          markup_percent_snapshot = charged.markupPercentSnapshot
       decrementBudgetUseCase(userId, charged.chargedAmountUsd)      // CHARGED

UpdatePricingMarkupUseCase — validace

PŘED (předpoklad k ověření v kódu): percent musí být > 0  (nebo už >= 0)
PO:   percent >= 0  (0 = marže vypnutá, dovoleno);  percent < 0 → VALIDATION

DB Migration — 0030-add-charged-cost-to-usage-events.xml

ALTER TABLE usage_events
    ADD COLUMN charged_cost_usd        NUMERIC(20,6) NULL,
    ADD COLUMN markup_percent_snapshot NUMERIC(6,3)  NULL;

-- Backfill historických řádků: charged = raw (marže se zpětně NEaplikuje;
-- už zaúčtované debety se nepřepočítávají — viz invariant).
UPDATE usage_events
    SET charged_cost_usd = cost_usd, markup_percent_snapshot = 0
    WHERE charged_cost_usd IS NULL;
  • RAW cost_usd / cost_cents zůstávají nedotčené (immutable SSoT spotřeby).
  • charged_cost_usd nullable jen kvůli ALTER; po backfillu vždy vyplněno (nové řádky vyplňuje RecordUsageEventUseCase).
  • Historický backfill charged = raw, markup = 0 zajišťuje, že reconciliation nad starými řádky nehlásí drift (žádná retroaktivní marže).
  • Liquibase immutable (production phase): nový soubor 0030-..., nikdy edit existujícího changesetu.

POST ani jiný HTTP request se touto UC nezavádí.

200 OK na existujícím GET /api/v1/users/me/usage/breakdown (C.3) — beze změny tvaru, hodnoty charged nyní odpovídají reálně odečtené částce (dříve čistě teoretické).

400 Bad Request (validation, existující endpoint PUT /api/v1/admin/pricing/markup) ErrorResponse:

{
  "code": "VALIDATION",
  "message": "Markup percent must be zero or positive"
}

Frontend

Tato UC nemá vlastní FE obrazovku. Admin markup config UI (C.3) musí jen povolit zadání 0 (dnes možná blokuje > 0).

Validations

FieldConstraintsSizePatternNote
aiMarkupPercentnot_null, min 0≥ 00 = marže vypnutá (dovoleno)
hostingMarkupPercentnot_null, min 0≥ 00 = marže vypnutá (dovoleno)

Backend

Validations

FieldConstraintsSizePatternNote
aiMarkupPercentnot_null, min 0≥ 0>= 0 (změna z případného > 0); záporné → VALIDATION
hostingMarkupPercentnot_null, min 0≥ 0>= 0; záporné → VALIDATION

Test Cases

GIVENWHENTHEN
ai_markup_percent = 30, user má ai_credit_usd = 100recordUsageEvent s raw cost $1.00usage_events.cost_usd = 1.00 (raw), charged_cost_usd = 1.30, ai_credit_usd = 98.70
ai_markup_percent = 0, user má ai_credit_usd = 100recordUsageEvent s raw cost $1.00charged_cost_usd = 1.00 = raw, ai_credit_usd = 99.00 — bit-identické s pre-change RAW chováním (žádná regrese)
ai_markup_percent = 30recordUsageEventusage_events.cost_usd (raw) se NEMĚNÍ; charged je v samostatném sloupci (immutable raw)
existují zaúčtované řádky s markup 30, admin změní markup na 50recordUsageEvent (nový event)nový řádek markup_percent_snapshot = 50; staré řádky NEPŘEPOČÍTÁNY (markup_percent_snapshot = 30)
historický usage_events řádek z doby před touto UCreconciliation report (UC-08005)charged_cost_usd = cost_usd (backfill), žádný drift nehlášen
ai_markup_percent = 30reconciliation/quota čte „reálně odečteno”čte charged_cost_usd; raw cost_usd zůstává SSoT spotřeby
admin nastaví markup = 0updatePricingMarkupuloženo, žádná chyba (0 dovoleno)
admin nastaví markup = -5updatePricingMarkup400 VALIDATION (záporné zakázáno)
PUT s vynechaným/null polem (např. {"aiMarkupPercent":25}, hosting vynechán)updatePricingMarkup200 — partial update: ai=25, hosting beze změny (no-op pro null pole)

Q (rozhodnuto — bylo Open Decision, nyní fixní pro implementaci)

  • Q7 (bylo D7 ANALYSIS): quota okna z raw nebo charged?charged. Rolling Redis kvótová okna (UC-08002) reprezentují „kolik user reálně utrácí” → musí počítat charged. Raw zůstává odděleně pro analytics/cost-side. Rationale: limit utrácení je o penězích uživatele = charged; kdyby quota běžela na raw, user s nenulovou marží by narazil na limit jinde než ukazuje billing.
  • Marže 0 = no-op je akceptační invariant, ne volitelné chování — garantuje bezpečné nasazení (deploy s marží 0 = žádná změna chování; marže se „zapne” samostatným admin PUT, oddělený od deploye).

Was this page helpful?

Thanks for the feedback.