Skip to content
Merged
480 changes: 354 additions & 126 deletions apps/web/src/app/admin/cloud-billing-skus/UsageRecordsContent.tsx

Large diffs are not rendered by default.

162 changes: 160 additions & 2 deletions apps/web/src/routers/admin/cloud-billing-skus-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,22 +209,180 @@ describe('admin.cloudBillingSkus usage records', () => {
await insertUsageInterval({ id: 'interval-b', subjectId: 'subject-1' });
await insertUsageInterval({ id: 'interval-a', subjectId: 'subject-1' });
await insertUsageInterval({ id: 'interval-other', subjectId: 'subject-2' });
await db.insert(container_usage_segment).values([
{
interval_id: 'interval-b',
seq: 1,
idempotency_key: 'interval-b-segment',
reported_seconds: 1,
usage_seconds: 1,
received_at: '2026-07-22T10:01:00.000Z',
},
{
interval_id: 'interval-a',
seq: 1,
idempotency_key: 'interval-a-segment',
reported_seconds: 1,
usage_seconds: 1,
received_at: '2026-07-22T10:01:00.000Z',
},
{
interval_id: 'interval-other',
seq: 1,
idempotency_key: 'interval-other-segment',
reported_seconds: 1,
usage_seconds: 1,
received_at: '2026-07-22T10:01:00.000Z',
},
]);
const caller = await createCallerForUser(admin.id);
const first = await caller.admin.cloudBillingSkus.searchUsageIntervals({
search: { kind: 'subject', subjectType: 'user', subjectId: 'subject-1' },
search: {
kind: 'subject',
subjectType: 'user',
subjectId: 'subject-1',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
},
limit: 1,
});
expect(first.items.map(item => item.id)).toEqual(['interval-b']);
expect(first.nextCursor).not.toBeNull();
if (!first.nextCursor) throw new Error('Expected an interval cursor');
const second = await caller.admin.cloudBillingSkus.searchUsageIntervals({
search: { kind: 'subject', subjectType: 'user', subjectId: 'subject-1' },
search: {
kind: 'subject',
subjectType: 'user',
subjectId: 'subject-1',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
},
limit: 1,
cursor: first.nextCursor,
});
expect(second.items.map(item => item.id)).toEqual(['interval-a']);
});

it('summarizes only an exact subject and bounded activity window by SKU', async () => {
await db.insert(cloud_billing_sku).values({
...validInput('usage-summary-sku'),
rate_cents_per_unit: '0.125',
created_by_user_id: admin.id,
});
await insertUsageInterval({
id: 'summary-in-window',
subjectId: 'summary-subject',
startedAt: '2026-07-22T10:00:00.000Z',
});
await db
.update(container_usage_interval)
.set({ cloud_billing_sku_id: 'usage-summary-sku', confirmed_seconds: 999 })
.where(eq(container_usage_interval.id, 'summary-in-window'));
await db.insert(container_usage_segment).values([
{
interval_id: 'summary-in-window',
seq: 1,
idempotency_key: 'summary-start-boundary',
reported_seconds: 12,
usage_seconds: 12,
received_at: '2026-07-22T09:00:00.000Z',
},
{
interval_id: 'summary-in-window',
seq: 2,
idempotency_key: 'summary-end-boundary',
reported_seconds: 100,
usage_seconds: 100,
received_at: '2026-07-22T11:00:00.000Z',
},
]);
await insertUsageInterval({
id: 'summary-other-subject',
subjectId: 'other-subject',
startedAt: '2026-07-22T10:00:00.000Z',
});
await db
.update(container_usage_interval)
.set({ confirmed_seconds: 999 })
.where(eq(container_usage_interval.id, 'summary-other-subject'));
await db.insert(container_usage_segment).values({
interval_id: 'summary-other-subject',
seq: 1,
idempotency_key: 'summary-other-subject',
reported_seconds: 999,
usage_seconds: 999,
received_at: '2026-07-22T10:00:00.000Z',
});
await insertUsageInterval({
id: 'summary-outside-window',
subjectId: 'summary-subject',
startedAt: '2026-07-23T10:00:00.000Z',
});
await db
.update(container_usage_interval)
.set({ confirmed_seconds: 999 })
.where(eq(container_usage_interval.id, 'summary-outside-window'));
await db.insert(container_usage_segment).values({
interval_id: 'summary-outside-window',
seq: 1,
idempotency_key: 'summary-outside-window',
reported_seconds: 999,
usage_seconds: 999,
received_at: '2026-07-23T10:00:00.000Z',
});

const caller = await createCallerForUser(admin.id);
const summary = await caller.admin.cloudBillingSkus.getUsageSummary({
subjectType: 'user',
subjectId: 'summary-subject',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
});

expect(summary).toMatchObject({
acceptedSeconds: 12,
estimatedCents: '1.5',
items: [
{
skuId: 'usage-summary-sku',
acceptedSeconds: 12,
estimatedCents: '1.5',
intervals: 1,
},
],
});

const nonAdminCaller = await createCallerForUser(nonAdmin.id);
await expect(
nonAdminCaller.admin.cloudBillingSkus.getUsageSummary({
subjectType: 'user',
subjectId: 'summary-subject',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
})
).rejects.toMatchObject({ code: 'FORBIDDEN' });
});

it('rejects invalid and oversized usage summary windows', async () => {
const caller = await createCallerForUser(admin.id);
await expect(
caller.admin.cloudBillingSkus.getUsageSummary({
subjectType: 'user',
subjectId: 'summary-subject',
start: '2026-07-22T11:00:00.000Z',
end: '2026-07-22T10:00:00.000Z',
})
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
await expect(
caller.admin.cloudBillingSkus.getUsageSummary({
subjectType: 'user',
subjectId: 'summary-subject',
start: '2026-06-01T00:00:00.000Z',
end: '2026-07-03T00:00:00.000Z',
})
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
});

it('returns ordered, safe segment details and rejects unknown intervals', async () => {
await insertUsageInterval({ id: 'interval-segments' });
await db
Expand Down
107 changes: 105 additions & 2 deletions apps/web/src/routers/admin/cloud-billing-skus-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const usageSearchSchema = z
kind: z.literal('subject'),
subjectType: z.enum(['user', 'org']),
subjectId: z.string().trim().min(1).max(256),
start: z.iso.datetime(),
end: z.iso.datetime(),
}),
]),
status: z.enum(['open', 'closed']).optional(),
Expand All @@ -58,7 +60,11 @@ const usageSearchSchema = z
.optional(),
limit: z.number().int().min(1).max(100).default(25),
})
.strict();
.strict()
.superRefine((input, context) => {
if (input.search.kind !== 'subject') return;
validateUsageWindow(input.search.start, input.search.end, context);
});

const segmentSearchSchema = z
.object({
Expand All @@ -68,6 +74,18 @@ const segmentSearchSchema = z
})
.strict();

const usageSummarySchema = z
.object({
subjectType: z.enum(['user', 'org']),
subjectId: z.string().trim().min(1).max(256),
start: z.iso.datetime(),
end: z.iso.datetime(),
})
.strict()
.superRefine((input, context) => {
validateUsageWindow(input.start, input.end, context);
});

const BILLING_HEALTH_WINDOW_MS = 24 * 60 * 60 * 1_000;
const STALE_OPEN_INTERVAL_MS = 15 * 60 * 1_000;
const usageMetadataSchema = z
Expand All @@ -77,6 +95,22 @@ const usageMetadataSchema = z
'Metadata may contain at most 16 entries'
);

function validateUsageWindow(startValue: string, endValue: string, context: z.RefinementCtx): void {
const start = new Date(startValue).getTime();
const end = new Date(endValue).getTime();
if (end <= start) {
context.addIssue({ code: 'custom', path: ['end'], message: 'End must be after start' });
return;
}
if (end - start > 31 * 24 * 60 * 60 * 1_000) {
context.addIssue({
code: 'custom',
path: ['end'],
message: 'Usage windows may not exceed 31 days',
});
}
}

export type SerializedUsageInterval = Pick<
ContainerUsageInterval,
| 'id'
Expand Down Expand Up @@ -212,7 +246,13 @@ export const cloudBillingSkusRouter = createTRPCRouter({
} else {
predicates.push(
eq(container_usage_interval.subject_type, input.search.subjectType),
eq(container_usage_interval.subject_id, input.search.subjectId)
eq(container_usage_interval.subject_id, input.search.subjectId),
sql`exists (
select 1 from ${container_usage_segment}
where ${container_usage_segment.interval_id} = ${container_usage_interval.id}
and ${container_usage_segment.received_at} >= ${input.search.start}
and ${container_usage_segment.received_at} < ${input.search.end}
)`
);
}
if (input.status) predicates.push(eq(container_usage_interval.status, input.status));
Expand Down Expand Up @@ -278,6 +318,69 @@ export const cloudBillingSkusRouter = createTRPCRouter({
};
}),

getUsageSummary: adminProcedure.input(usageSummarySchema).query(async ({ input }) => {
const rows = await db
.select({
skuId: container_usage_interval.cloud_billing_sku_id,
skuName: cloud_billing_sku.name,
rateCentsPerSecond: cloud_billing_sku.rate_cents_per_unit,
acceptedSeconds:
sql<number>`coalesce(sum(${container_usage_segment.usage_seconds}), 0)`.mapWith(Number),
intervals: sql<number>`count(distinct ${container_usage_interval.id})`.mapWith(Number),
estimatedCents:
sql<string>`coalesce(sum(${container_usage_segment.usage_seconds}::numeric * ${cloud_billing_sku.rate_cents_per_unit}), 0)::text`.mapWith(
String
),
totalAcceptedSeconds:
sql<number>`sum(sum(${container_usage_segment.usage_seconds})) over ()`.mapWith(Number),
totalEstimatedCents:
sql<string>`sum(sum(${container_usage_segment.usage_seconds}::numeric * ${cloud_billing_sku.rate_cents_per_unit})) over ()::text`.mapWith(
String
),
})
.from(container_usage_segment)
.innerJoin(
container_usage_interval,
eq(container_usage_segment.interval_id, container_usage_interval.id)
)
.innerJoin(
cloud_billing_sku,
eq(container_usage_interval.cloud_billing_sku_id, cloud_billing_sku.id)
)
.where(
and(
eq(container_usage_interval.subject_type, input.subjectType),
eq(container_usage_interval.subject_id, input.subjectId),
sql`${container_usage_segment.received_at} >= ${input.start}`,
lt(container_usage_segment.received_at, input.end)
)
)
.groupBy(
container_usage_interval.cloud_billing_sku_id,
cloud_billing_sku.name,
cloud_billing_sku.rate_cents_per_unit
)
.orderBy(container_usage_interval.cloud_billing_sku_id);
const items = rows.map(row => ({
skuId: row.skuId,
skuName: row.skuName,
rateCentsPerSecond: normalizeCloudBillingSkuRate(row.rateCentsPerSecond),
acceptedSeconds: row.acceptedSeconds,
estimatedCents: normalizeCloudBillingSkuRate(row.estimatedCents),
intervals: row.intervals,
}));
const totals = rows[0];
return {
subjectType: input.subjectType,
subjectId: input.subjectId,
start: input.start,
end: input.end,
items,
acceptedSeconds: totals?.totalAcceptedSeconds ?? 0,
estimatedCents: normalizeCloudBillingSkuRate(totals?.totalEstimatedCents ?? '0'),
};
}),

usageHealth: adminProcedure.query(async () => {
const end = new Date();
const start = new Date(end.getTime() - BILLING_HEALTH_WINDOW_MS);
Expand Down
26 changes: 26 additions & 0 deletions services/container-usage-meter/src/meter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ beforeEach(() => {

describe('ContainerUsageMeter', () => {
it('writes an admitted start directly to PostgreSQL', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
await expect(createMeter().recordStart(validStart())).resolves.toEqual({
success: true,
ack: { intervalId: 'cloud-agent-next:instance-1:123', durable: 'pg', dedup: false },
Expand All @@ -95,9 +96,20 @@ describe('ContainerUsageMeter', () => {
expect.stringMatching(/^[a-f0-9]{64}$/),
expect.any(Number)
);
expect(log).toHaveBeenCalledWith(
JSON.stringify({
message: 'Container usage meter RPC completed',
event: 'container_usage_rpc',
operation: 'start',
service: 'cloud-agent-next',
outcome: 'accepted',
dedup: false,
})
);
});

it('returns permanent SKU admission failures', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
vi.mocked(applyStart).mockResolvedValue({
kind: 'rejected',
code: 'sku_not_accepting_new_usage',
Expand All @@ -110,6 +122,9 @@ describe('ContainerUsageMeter', () => {
message: 'Billing SKU is not accepting new usage',
},
});
expect(log).toHaveBeenCalledWith(
expect.stringContaining('"rejectionCode":"sku_not_accepting_new_usage"')
);
});

it('writes heartbeats directly and returns the shadow budget verdict', async () => {
Expand All @@ -132,9 +147,20 @@ describe('ContainerUsageMeter', () => {
});

it('propagates transient PostgreSQL failures for bounded client retry', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
vi.mocked(applyHeartbeat).mockRejectedValue(new Error('postgres unavailable'));
await expect(createMeter().recordHeartbeat(validHeartbeat())).rejects.toThrow(
'postgres unavailable'
);
expect(log).toHaveBeenCalledWith(
JSON.stringify({
message: 'Container usage meter RPC completed',
event: 'container_usage_rpc',
operation: 'heartbeat',
service: 'cloud-agent-next',
outcome: 'failed',
errorName: 'Error',
})
);
});
});
Loading
Loading