Skip to content

Commit 4d61c1a

Browse files
Merge pull request #14 from piotrlangowski/feat/pool-per-customer-fee-and-size-basis
feat: pool per-customer fee basis + per-member pool sizing (ADR 0012 amendment)
2 parents b60f9bb + fea9afc commit 4d61c1a

25 files changed

Lines changed: 988 additions & 154 deletions

Architecture/0012-abonament-z-limitem-w-puli.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,48 @@ Parametry (ustalone 2026-07-02):
4444
- **Q2 — Decyzja 2 wdrażana teraz.** Usługi spoza tabeli burn-rate w scenariuszu `pool` księgują przychód przez `calculateMonetizationRevenue`, tak jak dziś `plan`/`feature`/`pack`. To odblokowuje wzorzec "tylko Fable w puli, Sonnet obok" bez czekania na sublimity (Q4).
4545
- **Q3 — jawna `overage_price_per_credit` odłożona.** Floor kosztowy + EVC z ADR 0010 Decyzja 1 zostaje jedynym mechanizmem cenowym nadwyżki; z pustym EVC już dziś poprawnie daje pass-through bez marży.
4646
- **Q4 — sublimity per usługa w puli odłożone (YAGNI).** Do czasu realnego przypadku z >1 usługą w jednej puli i nierównym podziałem limitu.
47+
48+
## Amendment (2026-07) — `fee_basis: 'per_customer'` + `PoolTier.pool_size_basis`
49+
50+
Po wdrożeniu Decyzji 1 (`fee_basis: 'per_member'`) drugi przebieg tego samego żywego scenariusza (Claude Pro + pula Fable, `c6d297d3-…`) ujawnił dwie kolejne luki, obie w tym samym miejscu — założeniu, że "opłata subskrypcyjna" i "wliczony limit" mają tę samą bazę co `aiUsers`:
51+
52+
1. **`per_member` liczy fee po adopterach, nie po całej bazie płacącej.** `aiUsers` (`ai_adoption_rate × current_users`) to populacja **korzystająca z AI**, nie populacja **płacąca abonament**. Dla Claude Pro każdy z 1M subskrybentów płaci $20, niezależnie od tego, czy w danym miesiącu w ogóle użył Fable — `fee_basis: 'per_member'` na scenariuszu z `ai_adoption_rate: 0.3` liczyłby fee od 300k, zaniżając przychód ~3,3×.
53+
2. **`credit_pool_size` jest absolutne — nie ma sposobu wyrazić "2,5 kredytu wliczone NA UŻYTKOWNIKA".** Realna polityka Anthropic to allowance *per subskrybent*, nie jeden wspólny worek kredytów na całą kohortę. Przy stałym `credit_pool_size` allowance efektywnie znika przy milionowej bazie (limit wyczerpuje się natychmiast, cała konsumpcja Fable księguje się jako nadwyżka pass-through) — więc nie da się odtworzyć podstawowej mechaniki "pierwsze 2,5 kredytu na koszt firmy, reszta pass-through per user".
54+
55+
Root cause dodatkowo pogłębiony przez bug implementacyjny: loader scenariusza w MCP (`mcp-server/src/index.ts`, `getFullScenario`) nie pobierał kolumny `fee_basis` z `pool_tiers` w ogóle — więc nawet ustawiony poprawnie `per_member` był po stronie silnika `undefined` → płaski fee. Naprawiony w tym samym przebiegu (SELECT rozszerzony o `fee_basis, pool_size_basis`).
56+
57+
### Decyzja (amendment)
58+
59+
1. **`PoolTier.fee_basis` zyskuje trzecią wartość `'per_customer'`.** Opłata tieru = `monthly_fee × activeCustomers` tego miesiąca — ta sama zmienna co baza kohorty w projekcji (nie `aiUsers`). Modeluje subskrypcję płaconą przez każdego aktywnego klienta niezależnie od realnego użycia AI. `'per_member'` zostaje bez zmian semantyki (× `aiUsers`) — użyteczne tam, gdzie opłata faktycznie skaluje się z adopcją (np. dodatek per-seat aktywowany na żądanie).
60+
2. **Nowe pole `PoolTier.pool_size_basis: 'absolute' | 'per_member'`** (domyślnie `'absolute'`, zachowuje dzisiejsze zachowanie). Przy `'per_member'` efektywna pula tego miesiąca = `credit_pool_size × aiUsers` — allowance skalowany per adopter, a nie stały worek. Konsumpcja/nadwyżka/breakage liczą się względem tej efektywnej puli (upper-band, zgodnie z istniejącą konwencją poola).
61+
3. **Rozdzielenie „kto płaci" (fee_basis) od „ile jest wliczone" (pool_size_basis) jest celowe.** To dwa niezależne wymiary tej samej struktury cenowej — realny przypadek referencyjny łączy `fee_basis: 'per_customer'` (płaci każdy subskrybent) z `pool_size_basis: 'per_member'` (wliczone tylko dla tych, którzy faktycznie korzystają), ale kombinacja jest dowolna (np. B2B org-wide fee + per-seat allowance).
62+
63+
### Ekonomia referencyjna (Claude Pro + pula Fable, zweryfikowana silnikiem)
64+
65+
Dla kohorty 1 000 000 subskrybentów, `ai_adoption_rate: 0.3` (300k adopterów), fee $20/`per_customer`, `credit_pool_size: 2.5`/`per_member`, Fable 9,2 kredytu/user (burn rate w puli), Sonnet 3,68 kredytu/user (poza pulą, PAYG), EVC puste (floor kosztowy $1/kredyt):
66+
67+
| Pozycja | Wzór | Wynik / mies. |
68+
|---|---|---|
69+
| Opłata tieru | `$20 × 1 000 000` | `$20 000 000` |
70+
| Nadwyżka Fable | `(9.2 − 2.5) × 300 000 × $1` | `$2 010 000` |
71+
| Przychód Sonnet (poza pulą) | `3.68 × 300 000 × $1` | `$1 104 000` (wash, = koszt) |
72+
| Allowance (COGS bez przychodu) | `2.5 × 300 000 × $1` | `$750 000` |
73+
| **Kontrybucja** | fee − allowance | **≈ $19 250 000** |
74+
75+
Test regresyjny: `src/lib/shared/financial-math.test.ts`, describe `'ADR 0012 amendment — two knobs'`, `'reproduces the Fable 5 reference economics …'`.
76+
77+
### Konsekwencje (amendment)
78+
79+
Pozytywne:
80+
- Realny scenariusz Claude Pro + pula Fable koduje się dziś dokładnie (bez obejść, bez zniekształceń rzędu wielkości) — jedyne pozostałe uproszczenie to statyczne `seats` planu (nieużywane w tej ścieżce — `pool` nie potrzebuje planu do przychodu) i miesięczna granulacja silnika (tydzień → miesiąc, drobne).
81+
- `fee_basis`/`pool_size_basis` to ortogonalne przełączniki — istniejące tiery (`flat`/`absolute`) i tiery z pierwszej Decyzji (`per_member`/`absolute`) działają bez zmian.
82+
83+
Negatywne / koszty:
84+
- `pool_size_basis` to kolejna kolumna + migracja (Migracja 23), plus UI/MCP/eksport do rozszerzenia — czwarty punkt dotyku po `fee_basis` z Decyzji 1.
85+
- Cztery kombinacje `fee_basis × pool_size_basis` (rosnące do sześciu przy przyszłych wartościach) zaczynają przypominać osobną strukturę cenową bardziej niż "pulę z dwoma trybami" — jeśli pojawi się piąty wymiar (np. sublimity per usługa, Q4 z sekcji głównej), warto rozważyć, czy `pool` nie powinien zostać rozbity na jawny obiekt konfiguracji zamiast rosnącej listy pól na `PoolTier`.
86+
87+
### Otwarte pytania (nie rozstrzygnięte tym amendmentem)
88+
89+
- Czy `pool_size_basis` powinien też przyjmować `'per_customer'` (allowance dla każdego subskrybenta, nie tylko adoptera)? Dziś niepotrzebne dla żadnego znanego przypadku — realna polityka Anthropic wlicza limit tylko aktywnym userom.
90+
- Statyczne `seats` planu (luka #5 z analizy pierwotnej) — wciąż nierozwiązane, wciąż poza zakresem `pool`.
91+
- Jawna cena nadwyżki (Q3 z sekcji głównej) — wciąż odłożona.

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ They share code through a **symlink**: `mcp-server/src/shared → ../../src/lib/
8282
`calculateScenario` does **not** credit top-line revenue. The model (all in the shared module):
8383

8484
- **One revenue carrier (ADR 0001–0004).** Each scenario sets a `modeling_type` (`incremental` | `gtm` | `appraisal`) and a `revenue_carrier` (`cohort` | `plan` | `pack` | `feature`); `resolveCarrier` maps type → carrier (`incremental`→`cohort`, `gtm`→`plan`, `appraisal`→ the explicit carrier). **Exactly one carrier books revenue**, so cohort ARPU uplift, plan `seats × base_price`, and monetization overrides are never summed (the double-counting the old `revenue_source` `'both'` model caused). `calculateScenario` switches on the carrier — `cohort` = the incremental contribution-margin band; `plan` = plan subscription + monetization; `pack`/`feature` = monetization only. A `revenue_bridge` (`upsell_on_cohort` | `separate_market`) reconciles plan seats that coexist with a `cohort` carrier (`separate_market` = additive subscription; `upsell_on_cohort` = informational only, revenue still from cohort uplift). `validateRevenueIntegrity` returns `ok`/`warn`/`block` to stop double-counting at save time, surfaced via `scenario_results.revenue_integrity_status`/`revenue_integrity_message`. This replaces the deprecated `revenue_source` (`cohort` | `monetization` | `both`) summing model.
85-
- **Per-archetype streams + credit pool (ADR 0009–0010).** `revenue_carrier` also accepts `'pool'`. Copilot (seat) and agent (interaction) are disjoint, summable streams: a monetized agent service (any `monetization_type !== 'none'`) books its price as revenue and excludes labor savings from cash (memo only, via `monthLaborSavingsCapacity`), avoiding price-vs-labor-savings double counting; an unmonetized agent still books labor savings as cash. `detectDriverProfile` classifies a scenario as `seat_only` | `interaction_only` | `mixed` (a revenue-booking seat-plan counts as the copilot signal even with no literal copilot service); `streamMargins` reports blended + per-stream gross margin against soft, scenario-overridable thresholds (`copilot_margin_threshold`/`agent_margin_threshold`, cascaded from `client_base` like `gross_margin`). `streamsDisjoint` is the test ADR 0001's incremental block defers to before hard-blocking monetization. The `'pool'` carrier (ADR 0010) bills one shared tier fee covering both streams via a `pool_tiers`/`pool_burn_rates` table; `validateRevenueIntegrity` hard-blocks mixing `monetization_type`s (or any `'outcome'` type) within one pool scenario. Credit value is `max(token-cost floor, capture × value_per_outcome)`; tier-fee + overage is attributed to copilot/agent proportional to lifetime EVC weight (`value_per_outcome × activity`), falling back to an even split when unset. Results surface as `CalculationResult.driverProfile`/`streamMargins`/`poolEconomics`, persisted in `scenario_results.driver_profile`/`stream_margins`/`pool_economics`.
85+
- **Per-archetype streams + credit pool (ADR 0009–0010, amended by ADR 0012).** `revenue_carrier` also accepts `'pool'`. Copilot (seat) and agent (interaction) are disjoint, summable streams: a monetized agent service (any `monetization_type !== 'none'`) books its price as revenue and excludes labor savings from cash (memo only, via `monthLaborSavingsCapacity`), avoiding price-vs-labor-savings double counting; an unmonetized agent still books labor savings as cash. `detectDriverProfile` classifies a scenario as `seat_only` | `interaction_only` | `mixed` (a revenue-booking seat-plan counts as the copilot signal even with no literal copilot service); `streamMargins` reports blended + per-stream gross margin against soft, scenario-overridable thresholds (`copilot_margin_threshold`/`agent_margin_threshold`, cascaded from `client_base` like `gross_margin`). `streamsDisjoint` is the test ADR 0001's incremental block defers to before hard-blocking monetization. The `'pool'` carrier bills one shared tier fee covering both streams via a `pool_tiers`/`pool_burn_rates` table. Under ADR 0012, this is governed by two knobs: `fee_basis` (`flat` for flat monthly fee, `per_customer` for tier fee × active customers, `per_member` for tier fee × active AI users) and `pool_size_basis` (`absolute` for a static included-credits pool, `per_member` for included credits × active AI users). Credit value is `max(token-cost floor, capture × value_per_outcome)`; tier-fee + overage is attributed to copilot/agent proportional to lifetime EVC weight (`value_per_outcome × activity`), falling back to a profile-informed allocation (`profile_fallback`) or even split when EVC is 0. Results surface as `CalculationResult.driverProfile`/`streamMargins`/`poolEconomics`, persisted in `scenario_results.driver_profile`/`stream_margins`/`pool_economics`.
8686
- **Contribution margin.** Each cohort carries a `gross_margin` (cascaded global → vertical → cohort, default `1.0`); incremental revenue is booked at margin, so cashflows reflect contribution, not gross revenue.
8787
- **Upper/lower attribution band.** Three projections are built per cohort — baseline, full-adoption, and uplift-only (ARPU effect on a baseline customer-count path). The engine returns a band: **upper** = full retention + acquisition + price effect; **lower** = ARPU-uplift only. NPV, payback and the PI are each returned as `…Upper`/`…Lower`.
8888
- **Adoption ramp.** `adoption_ramp_months` ramps effective adoption linearly from 0 to the target; implemented as a per-month blend `a(t)·fullAdopt + (1−a(t))·baseline` (the cohort model is linear in size, so `ramp=0` reproduces the old split). Late adopters inherit retention as if from month 0 — a documented approximation.

0 commit comments

Comments
 (0)