Ben Senescu 739b3f0b6a
Add autumn metered pricing for hosted mode (#44)
* feat: add hosted billing gate and backlinks metering

* refine hosted billing onboarding flow

* fix: align Autumn credit billing flows

* refactor: simplify Autumn billing flow

* refactor: narrow backlinks billing context

* refactor: require billing identity for backlinks profiles

* refactor: colocate backlinks billing flow

* clean

* refactor: centralize DataForSEO billing client

* refactor: simplify dataforseo billing flow

* chore: fix ci check lint issues

* fix: stabilize backlinks chart sizing

Measure chart container width before rendering the backlinks charts so Recharts does not mount at 0x0 inside responsive layouts.

* docs: document hosted DataForSEO metering

Capture the Autumn credit model and require hosted code to go through the metered client path so provider usage is harder to bypass.

* fix: enforce hosted billing access checks

* fix: preserve hosted billing subscription access

* fix: handle hosted billing UI failures
2026-03-25 13:02:12 -04:00

58 lines
1.4 KiB
TypeScript

import type { EnsuredUserContext } from "@/middleware/ensure-user/types";
import { AUTUMN_PAID_PLAN_ID } from "@/shared/billing";
import { autumn } from "@/server/billing/autumn";
import { AppError } from "@/server/lib/errors";
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
export type BillingCustomerContext = Pick<
EnsuredUserContext,
"organizationId" | "userEmail"
>;
export async function getOrCreateOrganizationCustomer(
context: BillingCustomerContext,
) {
const customer = await autumn.customers.getOrCreate({
customerId: context.organizationId,
name: context.organizationId,
});
if (!customer.id) {
throw new AppError("INTERNAL_ERROR", "Failed to resolve billing customer");
}
return {
...customer,
id: customer.id,
};
}
export function hasActivePaidPlan(customer: {
subscriptions: Array<{
planId: string;
status: string;
pastDue: boolean;
canceledAt: number | null;
}>;
}) {
return customer.subscriptions.some(
(subscription) =>
subscription.planId === AUTUMN_PAID_PLAN_ID &&
subscription.status === "active" &&
!subscription.pastDue,
);
}
export async function requireHostedPaidSubscription(
context: BillingCustomerContext,
) {
if (!(await isHostedServerAuthMode())) {
return;
}
const customer = await getOrCreateOrganizationCustomer(context);
if (!hasActivePaidPlan(customer)) {
throw new AppError("PAYMENT_REQUIRED");
}
}