feat(billing): sizes move up one; 1080p is the reference and the default

xs is a box with no stream, sm 720p, md 1080p, lg 1440p and xl 4K. Each
resolution keeps its price, so md is now 1x. New boxes default to md, and
existing boxes move up one size so none of them drops a resolution.
This commit is contained in:
Wanjohi
2026-10-10 15:18:23 +03:00
parent 0f38898fd9
commit ac713e62ae
9 changed files with 3882 additions and 15 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ export namespace BoxApi {
'json', 'json',
z.object({ z.object({
label: z.string().trim().min(1).max(64).default('box'), label: z.string().trim().min(1).max(64).default('box'),
tier: z.enum(BoxTier.enumValues).default('sm'), tier: z.enum(BoxTier.enumValues).default('md'),
on: z.enum(['own', 'fleet']).default('own') on: z.enum(['own', 'fleet']).default('own')
}) })
), ),
@@ -0,0 +1,5 @@
ALTER TABLE "box" ALTER COLUMN "tier" SET DEFAULT 'md';--> statement-breakpoint
-- Every size moved up one and each resolution kept its price: a box that was
-- `sm` (1080p) is now `md`, and so on up to `lg` (4K), now `xl`. An `xl` box
-- stays `xl`: it was 4K at 120 Hz and is 4K.
UPDATE "box" SET "tier" = (CASE "tier"::text WHEN 'xs' THEN 'sm' WHEN 'sm' THEN 'md' WHEN 'md' THEN 'lg' ELSE 'xl' END)::"box_tier";
File diff suppressed because it is too large Load Diff
@@ -169,6 +169,13 @@
"when": 1791629955619, "when": 1791629955619,
"tag": "0023_session_tier", "tag": "0023_session_tier",
"breakpoints": true "breakpoints": true
},
{
"idx": 24,
"version": "7",
"when": 1791634593049,
"tag": "0024_box_sizes_move_up",
"breakpoints": true
} }
] ]
} }
+2 -2
View File
@@ -233,12 +233,12 @@ describe('The counters', () => {
describe('Rates', () => { describe('Rates', () => {
test('on our hardware a bigger tier costs more, superlinearly', () => { test('on our hardware a bigger tier costs more, superlinearly', () => {
const rate = (tier: Burn.Tier) => Burn.baseRateMilli({ tier, hostClass: 'fleet' }); const rate = (tier: Burn.Tier) => Burn.baseRateMilli({ tier, hostClass: 'fleet' });
expect(rate('sm')).toBe(Burn.SCALE); expect(rate('md')).toBe(Burn.SCALE);
expect(rate('xl')).toBeGreaterThan(rate('lg')); expect(rate('xl')).toBeGreaterThan(rate('lg'));
// A tier buys a share of a card, and the ladder has to pinch harder // A tier buys a share of a card, and the ladder has to pinch harder
// than the share grows or running a small title at the top of it is // than the share grows or running a small title at the top of it is
// cheaper than it costs us. // cheaper than it costs us.
expect(rate('xl') / rate('sm')).toBeGreaterThan(4); expect(rate('xl') / rate('md')).toBeGreaterThan(4);
}); });
test('on the caller\u2019s own hardware the tier changes nothing', () => { test('on the caller\u2019s own hardware the tier changes nothing', () => {
+1 -1
View File
@@ -111,7 +111,7 @@ export namespace Billing {
// add, rather than as a multiplier on any of them. // add, rather than as a multiplier on any of them.
const rateMilli = open.reduce((total, segment) => total + segment.rateMilli, 0); const rateMilli = open.reduce((total, segment) => total + segment.rateMilli, 0);
const next = Burn.baseRateMilli({ const next = Burn.baseRateMilli({
tier: input.nextTier ?? 'sm', tier: input.nextTier ?? 'md',
hostClass: input.nextHostClass ?? 'byo' hostClass: input.nextHostClass ?? 'byo'
}); });
+2 -2
View File
@@ -68,11 +68,11 @@ describe('The nesting rule: every window has to bind', () => {
}); });
test('the reference tier has to cost exactly one unit a second', () => { test('the reference tier has to cost exactly one unit a second', () => {
// The unit *is* a second of a reference session, so moving `sm` off 1 // The unit *is* a second of a reference session, so moving `md` off 1
// would silently redefine every allowance — the same stored number // would silently redefine every allowance — the same stored number
// would be a different number of hours. // would be a different number of hours.
expect(() => expect(() =>
Limits.checkFactors({ size: { xs: 500, sm: 900, md: 2200, lg: 5000, xl: 12000 } }) Limits.checkFactors({ size: { xs: 250, sm: 500, md: 900, lg: 2200, xl: 5000 } })
).toThrow(/reference tier/); ).toThrow(/reference tier/);
expect(() => Limits.checkFactors(Limits.PLACEHOLDER.factors)).not.toThrow(); expect(() => Limits.checkFactors(Limits.PLACEHOLDER.factors)).not.toThrow();
}); });
+10 -8
View File
@@ -100,12 +100,14 @@ export namespace Limits {
sevenDay: 900 * 60 * 60, sevenDay: 900 * 60 * 60,
thirtyDay: 3000 * 60 * 60 thirtyDay: 3000 * 60 * 60
}, },
// Superlinear, and no more principled than that. `sm` is the reference // Superlinear, and no more principled than that. `md`, a 1080p box, is
// and is 1 by definition; the rest roughly double per step so the shape // the reference and is 1 by definition; the rest roughly double per step
// is visible in tests. Real values come from what a card-hour costs us // so the shape is visible in tests. Each resolution kept its price when
// divided by the share a tier holds. // the sizes moved up one (`xs` became a box with no stream, `sm` 720p).
// Real values come from what a card-hour costs us divided by the share
// a tier holds.
factors: { factors: {
size: { xs: 500, sm: 1000, md: 2200, lg: 5000, xl: 12000 } size: { xs: 250, sm: 500, md: 1000, lg: 2200, xl: 5000 }
} }
}; };
@@ -159,15 +161,15 @@ export namespace Limits {
* The reference tier costs exactly one unit a second, by definition. * The reference tier costs exactly one unit a second, by definition.
* *
* The unit *is* a second of a reference session, so a size factor that made * The unit *is* a second of a reference session, so a size factor that made
* `sm` anything other than 1 would silently redefine what every allowance * `md` anything other than 1 would silently redefine what every allowance
* means — the same stored number would be a different number of hours. * means — the same stored number would be a different number of hours.
*/ */
export function checkFactors(factors: Factors): void { export function checkFactors(factors: Factors): void {
if (factors.size.sm !== 1000) { if (factors.size.md !== 1000) {
throw new VisibleError( throw new VisibleError(
'internal', 'internal',
ErrorCodes.Server.INTERNAL_ERROR, ErrorCodes.Server.INTERNAL_ERROR,
`the reference tier must cost exactly one unit a second (1000), not ${factors.size.sm} \u2014 it is what every allowance is denominated in` `the reference tier must cost exactly one unit a second (1000), not ${factors.size.md} \u2014 it is what every allowance is denominated in`
); );
} }
} }
+1 -1
View File
@@ -58,7 +58,7 @@ export const BoxTable = pgTable(
// it is deliberately not unique: two boxes called "living room" are the // it is deliberately not unique: two boxes called "living room" are the
// owner's problem, not an error. ref(d-0019) // owner's problem, not an error. ref(d-0019)
label: text('label').notNull(), label: text('label').notNull(),
tier: BoxTier('tier').notNull().default('sm'), tier: BoxTier('tier').notNull().default('md'),
state: BoxState('state').notNull().default('created'), state: BoxState('state').notNull().default('created'),
/** Why it stopped, verbatim from the agent. Null while it has never run. */ /** Why it stopped, verbatim from the agent. Null while it has never run. */
stopReason: text('stop_reason'), stopReason: text('stop_reason'),