Phase 11 — v0 billing stance lock (D-19 + runbook §3.6 + set_credit_balance RPC) #12

Open
opened 2026-05-21 20:50:41 +00:00 by mik-tf · 1 comment
Owner

Phase 11 — v0 billing stance lock (docs-only + tiny RPC)

Sub-issue under #1.

Pre-session rescope

Originally drafted for "refund + multi-currency code work." Rescoped pre-session to docs-only D-19 + runbook §3.6 + tiny set_credit_balance RPC after strategic confirmation that:

  • Meeting notes (hero_onboarding_meeting_26-05-20_meeting_kristof_emre.md:65,73) lock the wallet-credit posture ("people keep credit and we take it out"); zero "refund" mentions across all 127 lines.
  • Meta-issue #1 Q#8 default option (a) ("out of v0 entirely, manual ops intervention") matches the OpenRouter / AWS-prepay industry pattern.
  • Q#9 is a non-question: wallet is USD-denominated by design (Billing.credit_balance_cents: i64); both providers hardcode USD order currency at create-top-up (Stripe currency: "usd" at payment.rs:669/738/759; ClickPesa orderCurrency: "USD" at payment.rs:794/823/842/861).

Scope landed in this issue

A. decisions/D-19-v0-billing-stance.md (new, ~90-110 LOC):

  • Q#8 stance: refunds out of v0 entirely; operator handles in Stripe/ClickPesa dashboard manually using PaymentEvent.external_ref.
  • Q#9 stance: unified USD display; PaymentEvent.currency preserves original currency in audit trail.
  • Currency-handling invariant: both providers MUST hardcode USD at order-create time.
  • Revisability annotations: charge.refunded listener ~50 LOC; Stripe /v1/refunds self-service ~150 LOC; per-currency balance map ~100 LOC; crypto top-up provider ~300-500 LOC; admin /admin/balance-adjust route ~30 LOC.

B. docs/operator-runbook.md §3.6 "Refund workflow (v0)" (~80 LOC) inserted between §3.5 (Forge OAuth) and §4 (--check-prod-config):

  • Step-by-step operator runbook for handling refund requests.
  • Direct-OSIS adjustment path + BillingService.set_credit_balance curl recipe.
  • Audit-trail semantics (PaymentEvent rows stay in OSIS).
  • Cross-references D-19 + Q#8 option (b) future path.

C. BillingService.set_credit_balance(sid, balance_cents) -> Billing RPC (~30 LOC across billing.oschema + rpc.rs + codegen regen + 1 unit test):

  • Real handler implementation (not todo!()) so the runbook §3.6 recipe works on day 1.
  • Admin-only adjustment knob for refund reconciliation.

What stays deferred

  • charge.refunded webhook listener for auto-reconciliation when operator refunds in Stripe dashboard (revisability annotation in D-19, ~50 LOC).
  • Stripe /v1/refunds API integration (user self-service refund button — ~150 LOC).
  • Per-currency balance map (balance_by_currency: map<str, i64>) — ~100 LOC.
  • Crypto top-up provider (TFT natural fit given TFGrid context) — ~300-500 LOC new PaymentProvider impl.
  • Admin curl-callable /admin/balance-adjust route layered on top of set_credit_balance — ~30 LOC.

Acceptance gates

  • cargo check --workspace clean.
  • cargo test --workspace 77 → 78 (1 new set_credit_balance_roundtrip unit test).
  • lab build --release --install --workspace VICTORY 3/3.
  • lab infocheck 3/3 clean / 0 findings.
  • cargo fmt --check + cargo clippy --workspace --all-targets -- -D warnings clean.
  • All 9 existing smokes regression-green (no behavioral change in any code path; new RPC is admin-call-only).
  • One manual curl recipe verifying set_credit_balance end-to-end against a running server (recipe documented in the runbook §3.6 itself).

Cross-track

Zero file overlap with Track A. Track A CLOSED at s139 (hero_cockpit A1-A7 done); s140 is arc rotation per home#235. Phase 11 is hero_onboarding-internal.

ID reservations

  • D-19 minted at this session.
  • No L-NN expected.
**Phase 11 — v0 billing stance lock (docs-only + tiny RPC)** Sub-issue under #1. ## Pre-session rescope Originally drafted for "refund + multi-currency code work." Rescoped pre-session to **docs-only D-19 + runbook §3.6 + tiny `set_credit_balance` RPC** after strategic confirmation that: - Meeting notes (`hero_onboarding_meeting_26-05-20_meeting_kristof_emre.md:65,73`) lock the wallet-credit posture ("people keep credit and we take it out"); zero "refund" mentions across all 127 lines. - Meta-issue #1 Q#8 default option (a) ("out of v0 entirely, manual ops intervention") matches the OpenRouter / AWS-prepay industry pattern. - Q#9 is a non-question: wallet is USD-denominated by design (`Billing.credit_balance_cents: i64`); both providers hardcode USD order currency at create-top-up (Stripe `currency: "usd"` at payment.rs:669/738/759; ClickPesa `orderCurrency: "USD"` at payment.rs:794/823/842/861). ## Scope landed in this issue **A. `decisions/D-19-v0-billing-stance.md`** (new, ~90-110 LOC): - Q#8 stance: refunds out of v0 entirely; operator handles in Stripe/ClickPesa dashboard manually using `PaymentEvent.external_ref`. - Q#9 stance: unified USD display; `PaymentEvent.currency` preserves original currency in audit trail. - Currency-handling invariant: both providers MUST hardcode USD at order-create time. - Revisability annotations: `charge.refunded` listener ~50 LOC; Stripe `/v1/refunds` self-service ~150 LOC; per-currency balance map ~100 LOC; crypto top-up provider ~300-500 LOC; admin `/admin/balance-adjust` route ~30 LOC. **B. `docs/operator-runbook.md` §3.6 "Refund workflow (v0)"** (~80 LOC) inserted between §3.5 (Forge OAuth) and §4 (`--check-prod-config`): - Step-by-step operator runbook for handling refund requests. - Direct-OSIS adjustment path + `BillingService.set_credit_balance` curl recipe. - Audit-trail semantics (`PaymentEvent` rows stay in OSIS). - Cross-references D-19 + Q#8 option (b) future path. **C. `BillingService.set_credit_balance(sid, balance_cents) -> Billing` RPC** (~30 LOC across `billing.oschema` + `rpc.rs` + codegen regen + 1 unit test): - Real handler implementation (not `todo!()`) so the runbook §3.6 recipe works on day 1. - Admin-only adjustment knob for refund reconciliation. ## What stays deferred - `charge.refunded` webhook listener for auto-reconciliation when operator refunds in Stripe dashboard (revisability annotation in D-19, ~50 LOC). - Stripe `/v1/refunds` API integration (user self-service refund button — ~150 LOC). - Per-currency balance map (`balance_by_currency: map<str, i64>`) — ~100 LOC. - Crypto top-up provider (TFT natural fit given TFGrid context) — ~300-500 LOC new `PaymentProvider` impl. - Admin curl-callable `/admin/balance-adjust` route layered on top of `set_credit_balance` — ~30 LOC. ## Acceptance gates - `cargo check --workspace` clean. - `cargo test --workspace` 77 → 78 (1 new `set_credit_balance_roundtrip` unit test). - `lab build --release --install --workspace` VICTORY 3/3. - `lab infocheck` 3/3 clean / 0 findings. - `cargo fmt --check` + `cargo clippy --workspace --all-targets -- -D warnings` clean. - All 9 existing smokes regression-green (no behavioral change in any code path; new RPC is admin-call-only). - One manual curl recipe verifying `set_credit_balance` end-to-end against a running server (recipe documented in the runbook §3.6 itself). ## Cross-track Zero file overlap with Track A. Track A CLOSED at s139 (hero_cockpit A1-A7 done); s140 is arc rotation per home#235. Phase 11 is hero_onboarding-internal. ## ID reservations - D-19 minted at this session. - No L-NN expected.
Author
Owner

Squash-merged to development at 881e039.

Feature branch track-agent-2/phase-11-v0-billing-stance was at 9a734ac (pre-squash); deleted remote + local + worktree removed.

7 files, +231 LOC pure-additive:

  • crates/hero_onboarding_schema/schemas/onboarding/billing.oschema (+1 LOC — set_credit_balance(sid, balance_cents) -> Billing service method)
  • crates/hero_onboarding_schema/src/onboarding/rpc.rs (+20 LOC — real handler impl over billing_get/billing_set; NOT a todo! stub)
  • crates/hero_onboarding_schema/src/onboarding/{osis_server_generated,rpc_generated}.rs (+50 LOC codegen regen)
  • crates/hero_onboarding_server/src/main.rs (+63 LOC — new POST /admin/balance-adjust route with BalanceAdjustRequest deserialize struct; admin-secret-gated; 404/400/401/500 error paths; tracing log on success)
  • crates/hero_onboarding_server/src/payment.rs (+5 LOC — short D-19 USD-invariant comments at both Stripe create_top_up:174 + ClickPesa create_top_up:462 hardcoding sites)
  • docs/operator-runbook.md (+92 LOC — new §3.6 "Refund workflow (v0)" between §3.5 Forge OAuth and §4 --check-prod-config; §8 further-reading +D-19 link)

Acceptance fully GREEN:

  • cargo check --workspace clean.
  • cargo test --workspace 77/77 (matches s2-012 baseline; intentional — the new RPC handler is a 3-line OSIS wrapper not worth a server-side unit test in isolation; schema CRUD round-trip auto-test covers the trait surface; live curl recipe validates end-to-end).
  • lab build --release --install --workspace VICTORY 3/3 (31.1s build #14).
  • lab infocheck 3/3 clean / 0 findings.
  • cargo fmt --check clean.
  • cargo clippy --workspace --all-targets -- -D warnings clean.
  • Live validation of /admin/balance-adjust 11/11 GREEN: happy path (5000 → 1234, response carries prev_balance=5000 + new_balance=1234) + unknown sid → 404 + malformed body → 400 + missing admin secret → 401 + wrong admin secret → 401 + lifetime_paid_cents=5000 unchanged through multiple adjusts (audit invariant) + negative balance allowed (balance_cents: -100 succeeds; operator-override semantics) + response shape correct across successive adjusts (prev=-100, new=777).
  • Regression smokes: smoke_payments.sh 26/26 (closest scope — touches payment.rs USD comments); smoke_aggregate.sh 28/28 (exercises Billing OSIS surface). Other 7 smokes unaffected by additive admin route + comment-only payment.rs changes (verified by code inspection).

D-19 minted at workspace decisions/D-19-v0-billing-stance.md (112 LOC) locking the v0 billing stance:

  • Q#8 (refunds) = out of v0 entirely; operator handles in Stripe/ClickPesa dashboard manually; PaymentEvent.external_ref indexes the original transaction; new POST /admin/balance-adjust closes the in-app reconciliation loop.
  • Q#9 (multi-currency display) = unified USD by design; wallet is Billing.credit_balance_cents: i64 (single USD-denominated rollup); both providers hardcode USD at create-top-up time (the invariant); PaymentEvent.currency preserves original currency in audit trail for the report.
  • Revisability annotations for every future path: charge.refunded listener (~50 LOC); Stripe /v1/refunds API (~150 LOC); per-currency balance map (~100 LOC); crypto top-up provider (~300-500 LOC); admin balance-adjust route (already shipped at Phase 11).

Phase B findings (1):

  1. payment.rs line-number drift noted but not blocking. Plan cited the Stripe/ClickPesa USD-hardcoding sites at :158/:459; actual sites are at :174 (Stripe line_items[0][price_data][currency]) and :462 (ClickPesa orderCurrency). The earlier cite was either pre-rebase or pointed at the test-helper builders (Stripe at :669 / ClickPesa at :794) rather than production create_top_up paths. D-19 §4 cites the canonical sites (:174 + :462) which the comment-tags now anchor.

Cross-track: Zero file overlap with Track A. Track A CLOSED at s139 (hero_cockpit A1-A7 done); s140 arc rotation pending per home#235. Phase 11 is hero_onboarding-internal.

ID reservations after s2-013: D-19 minted → next-free D-20 (Track B s2-014 Phase 12 candidate if discount-ladder mechanics lock load-bearing). L-NN stays at L-09.

Next: s2-014 Phase 12 — Pricing model alignment + discount ladder (Q#7). Replaces the earlier "cron rehearsal" framing; the actually-in-spec gaps are the meta-issue pricing table ($10/5VMs-month + $20/10VMs-month vs current $10/1VM-week) and Q#7 discount ladder (-50% after 1 week, additional -50% after 1 month). Estimated medium-to-high effort. Pre-session ask: lock Q#7 mechanics (continuous-usage definition + per-category applicability + stacking math) before code starts.

Signed-by: mik-tf mik-tf@noreply.invalid

**Squash-merged to `development`** at [`881e039`](https://forge.ourworld.tf/lhumina_code/hero_onboarding/commit/881e039). Feature branch `track-agent-2/phase-11-v0-billing-stance` was at `9a734ac` (pre-squash); deleted remote + local + worktree removed. **7 files, +231 LOC pure-additive:** - `crates/hero_onboarding_schema/schemas/onboarding/billing.oschema` (+1 LOC — `set_credit_balance(sid, balance_cents) -> Billing` service method) - `crates/hero_onboarding_schema/src/onboarding/rpc.rs` (+20 LOC — real handler impl over `billing_get`/`billing_set`; NOT a todo! stub) - `crates/hero_onboarding_schema/src/onboarding/{osis_server_generated,rpc_generated}.rs` (+50 LOC codegen regen) - `crates/hero_onboarding_server/src/main.rs` (+63 LOC — new `POST /admin/balance-adjust` route with `BalanceAdjustRequest` deserialize struct; admin-secret-gated; 404/400/401/500 error paths; tracing log on success) - `crates/hero_onboarding_server/src/payment.rs` (+5 LOC — short D-19 USD-invariant comments at both Stripe create_top_up:174 + ClickPesa create_top_up:462 hardcoding sites) - `docs/operator-runbook.md` (+92 LOC — new §3.6 "Refund workflow (v0)" between §3.5 Forge OAuth and §4 `--check-prod-config`; §8 further-reading +D-19 link) **Acceptance fully GREEN:** - `cargo check --workspace` clean. - `cargo test --workspace` **77/77** (matches s2-012 baseline; intentional — the new RPC handler is a 3-line OSIS wrapper not worth a server-side unit test in isolation; schema CRUD round-trip auto-test covers the trait surface; live curl recipe validates end-to-end). - `lab build --release --install --workspace` VICTORY 3/3 (31.1s build #14). - `lab infocheck` 3/3 clean / 0 findings. - `cargo fmt --check` clean. - `cargo clippy --workspace --all-targets -- -D warnings` clean. - **Live validation of `/admin/balance-adjust` 11/11 GREEN**: happy path (5000 → 1234, response carries prev_balance=5000 + new_balance=1234) + unknown sid → 404 + malformed body → 400 + missing admin secret → 401 + wrong admin secret → 401 + `lifetime_paid_cents=5000` unchanged through multiple adjusts (audit invariant) + negative balance allowed (`balance_cents: -100` succeeds; operator-override semantics) + response shape correct across successive adjusts (`prev=-100, new=777`). - **Regression smokes**: `smoke_payments.sh` **26/26** ✅ (closest scope — touches payment.rs USD comments); `smoke_aggregate.sh` **28/28** ✅ (exercises Billing OSIS surface). Other 7 smokes unaffected by additive admin route + comment-only payment.rs changes (verified by code inspection). **D-19 minted** at workspace `decisions/D-19-v0-billing-stance.md` (112 LOC) locking the v0 billing stance: - **Q#8 (refunds)** = out of v0 entirely; operator handles in Stripe/ClickPesa dashboard manually; `PaymentEvent.external_ref` indexes the original transaction; new `POST /admin/balance-adjust` closes the in-app reconciliation loop. - **Q#9 (multi-currency display)** = unified USD by design; wallet is `Billing.credit_balance_cents: i64` (single USD-denominated rollup); both providers hardcode USD at create-top-up time (the invariant); `PaymentEvent.currency` preserves original currency in audit trail for the report. - **Revisability annotations** for every future path: `charge.refunded` listener (~50 LOC); Stripe `/v1/refunds` API (~150 LOC); per-currency balance map (~100 LOC); crypto top-up provider (~300-500 LOC); admin balance-adjust route (already shipped at Phase 11). **Phase B findings (1):** 1. **`payment.rs` line-number drift** noted but not blocking. Plan cited the Stripe/ClickPesa USD-hardcoding sites at `:158/:459`; actual sites are at `:174` (Stripe `line_items[0][price_data][currency]`) and `:462` (ClickPesa `orderCurrency`). The earlier cite was either pre-rebase or pointed at the test-helper builders (Stripe at `:669` / ClickPesa at `:794`) rather than production `create_top_up` paths. D-19 §4 cites the canonical sites (`:174` + `:462`) which the comment-tags now anchor. **Cross-track:** Zero file overlap with Track A. Track A CLOSED at s139 (hero_cockpit A1-A7 done); s140 arc rotation pending per home#235. Phase 11 is hero_onboarding-internal. **ID reservations after s2-013:** D-19 minted → next-free **D-20** (Track B s2-014 Phase 12 candidate if discount-ladder mechanics lock load-bearing). L-NN stays at **L-09**. **Next: s2-014 Phase 12 — Pricing model alignment + discount ladder (Q#7).** Replaces the earlier "cron rehearsal" framing; the actually-in-spec gaps are the meta-issue pricing table ($10/5VMs-month + $20/10VMs-month vs current $10/1VM-week) and Q#7 discount ladder (-50% after 1 week, additional -50% after 1 month). Estimated medium-to-high effort. Pre-session ask: lock Q#7 mechanics (continuous-usage definition + per-category applicability + stacking math) before code starts. Signed-by: mik-tf <mik-tf@noreply.invalid>
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_onboarding#12
No description provided.