rowboat/apps/x/packages/shared/src/billing.ts
Ramnique Singh f65f7e8fc8 feat(billing): consume plan variant catalog
Fetch the public billing catalog during account config load and use durable plan IDs from /v1/me for display, analytics, and upgrade/manage labels.

Renderer billing surfaces now resolve plan display data from the catalog and show Unknown when the backend returns an unmapped plan ID.
2026-06-19 16:52:22 +05:30

52 lines
1.7 KiB
TypeScript

import { z } from 'zod';
export const BillingPlanCategorySchema = z.enum(['free', 'starter', 'pro']);
export type BillingPlanCategory = z.infer<typeof BillingPlanCategorySchema>;
export const BillingPlanIdSchema = z.string().min(1);
export type BillingPlanId = z.infer<typeof BillingPlanIdSchema>;
export const BillingCatalogPlanSchema = z.object({
id: BillingPlanIdSchema,
category: BillingPlanCategorySchema,
displayName: z.string(),
monthlyCredits: z.number(),
dailyCredits: z.number(),
monthlyPriceCents: z.number().nullable(),
archived: z.boolean().optional(),
});
export type BillingCatalogPlan = z.infer<typeof BillingCatalogPlanSchema>;
export const BillingCatalogSchema = z.object({
plans: z.array(BillingCatalogPlanSchema),
});
export type BillingCatalog = z.infer<typeof BillingCatalogSchema>;
export const BillingUsageBucketSchema = z.object({
sanctionedCredits: z.number(),
usedCredits: z.number(),
availableCredits: z.number(),
});
export type BillingUsageBucket = z.infer<typeof BillingUsageBucketSchema>;
export const BillingInfoSchema = z.object({
userEmail: z.string().nullable(),
userId: z.string().nullable(),
subscriptionPlanId: BillingPlanIdSchema.nullable(),
subscriptionStatus: z.string().nullable(),
trialExpiresAt: z.string().nullable(),
catalog: BillingCatalogSchema,
monthly: BillingUsageBucketSchema,
daily: BillingUsageBucketSchema.extend({
usageDay: z.string(),
}),
});
export type BillingInfo = z.infer<typeof BillingInfoSchema>;
export function getBillingPlanData(
catalog: BillingCatalog,
planId: string | null | undefined,
): BillingCatalogPlan | null {
if (!planId) return null;
return catalog.plans.find((plan) => plan.id === planId) ?? null;
}