feat(levelcode): denominate the customer-facing balance in credits, not dollars#380
Merged
Conversation
…ot dollars Balances are now shown in credits: $1 = 100 credits, so $12.79 left reads as 1,279 credits and the $100 Ultra plan is a 10,000-credit allowance. The reason is behavioural, not technical — a shrinking DOLLAR balance reads as money being lost, so people ration it; an allowance reads as something to spend. PRESENTATION ONLY. The ledger, metering, Stripe prices and invoices all stay in micro-dollars, because dollars are what providers charge and what we actually bill; re-denominating stored balances would put Stripe reconciliation at risk for no gain, and would be painful to undo. Nothing about the economics moves. The mapping needed no new maths: budget_micros == price × CREDIT_COGS_RATIO and retail_micros divides that ratio back out, so a plan's retail budget already IS its price — which makes the credit allowance exactly the price in CENTS. Pro 2,000 / Pro+ 4,000 / Max 6,000 / Ultra 10,000, verified against the running app. - CREDITS_PER_DOLLAR + MICROS_PER_CREDIT: the unit, defined once beside the margin constant it derives from. - micros_to_credits / plan_credits: conversion + the derived allowance. - roster_for gains per_turn_micros — one turn's cost at RETAIL, the same unit as the balance, so the dashboard can print "N credits/turn" beside "≈ turns left" using a single conversion. Extracted per_turn_cost_micros so that figure and turns_left are computed from the SAME cost and cannot drift. - Plan feature copy now leads with the credit allowance. Verified: 968 examples, 0 failures. New specs pin the allowance to price_cents (the numbers the marketing copy prints), prove it survives a CREDIT_COGS_RATIO change, and assert balance ÷ per_turn_micros reproduces turns_left for every model on the plan. That last one is mutation-checked: dropping the routing fee from per_turn_micros so it diverges from turns_left fails the suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates Levelcode’s customer-facing display to denominate balances and plan allowances in “credits” (where $1 = 100 credits), while keeping all internal accounting/metering/Stripe reconciliation in micro-dollars.
Changes:
- Introduces credit denomination constants and helpers (
CREDITS_PER_DOLLAR,MICROS_PER_CREDIT,micros_to_credits,plan_credits). - Adds per-turn retail cost (
per_turn_micros) to the roster payload to support dashboard “credits/turn” display. - Updates plan feature copy and adds specs to pin allowance-to-price and per-turn/turns-left invariants.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
lib/levelcode.rb |
Adds credit conversion + plan allowance helpers and exposes per-turn retail cost in roster output; updates plan feature strings. |
spec/models/levelcode_plans_spec.rb |
Adds specs for credit conversion, allowance derivation, and per-turn economics invariants. |
spec/requests/api/levelcode/v1/account_spec.rb |
Extends API request coverage to assert per_turn_micros is present and reasonable. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…n (PR #380 review) Both review points were real, and the first was worse than it looked. 1. per_turn_micros was ROUNDED RETAIL while turns_left still came from COST micro-$ via turns_in_budget. The two therefore rounded independently, so "balance ÷ credits-per-turn" did not reproduce "≈ turns left" — the dashboard prints them side by side and a user divides one into the other by eye. Measured before fixing: 3,440 disagreements in 28,000 sampled balances (~12%). My spec had asserted a SINGLE balance and passed anyway, which is the actual lesson — one value proves nothing about a rounding boundary. There is now one definition of the per-turn figure (retail_per_turn_micros), and both turn counts divide it via turns_in_retail_budget. They agree by construction rather than by coincidence. The margin ratio cancels in the division, so the reported counts are unchanged. Re-measured after: 0 disagreements in the same 28,000 combinations. turns_in_budget (the COST-domain variant) is gone rather than left unused — keeping a second helper that computes the same thing in a different unit is exactly how this drift would come back. 2. per_turn_cost_micros returned a Float (integer cost x the float routing fee) despite a name promising micro-dollars. Now rounded to an Integer like cost_micros, so the imprecision cannot propagate into the counts. Verified: 969 examples, 0 failures. The invariant spec now sweeps 400 balances across every model on the plan instead of one, and a new spec pins the Integer types end to end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Balances are now shown in credits: $1 = 100 credits, so $12.79 left reads as 1,279 credits and the $100 Ultra plan is a 10,000-credit allowance. The reason is behavioural, not technical — a shrinking DOLLAR balance reads as money being lost, so people ration it; an allowance reads as something to spend.
PRESENTATION ONLY. The ledger, metering, Stripe prices and invoices all stay in micro-dollars, because dollars are what providers charge and what we actually bill; re-denominating stored balances would put Stripe reconciliation at risk for no gain, and would be painful to undo. Nothing about the economics moves.
The mapping needed no new maths: budget_micros == price × CREDIT_COGS_RATIO and retail_micros divides that ratio back out, so a plan's retail budget already IS its price — which makes the credit allowance exactly the price in CENTS. Pro 2,000 / Pro+ 4,000 / Max 6,000 / Ultra 10,000, verified against the running app.
Verified: 968 examples, 0 failures. New specs pin the allowance to price_cents (the numbers the marketing copy prints), prove it survives a CREDIT_COGS_RATIO change, and assert balance ÷ per_turn_micros reproduces turns_left for every model on the plan. That last one is mutation-checked: dropping the routing fee from per_turn_micros so it diverges from turns_left fails the suite.