* 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
58 lines
1.4 KiB
TypeScript
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");
|
|
}
|
|
}
|