import { EnvironmentPauseSource } from "@trigger.dev/database"; import pMap from "p-map"; import { prisma } from "~/db.server"; import type { BillingLimitResult } from "~/services/billingLimit.schemas"; import { logger } from "~/services/logger.server"; import { getActiveBillingLimits, getBillingLimit } from "~/services/platform.v3.server"; import { BILLING_LIMIT_RECONCILE_LOOKUP_CONCURRENCY, type BillingLimitConvergeTargetState, } from "./billingLimitConstants"; import { readBillingLimitReconcileQueue, removeFromBillingLimitReconcileQueue, } from "./billingLimitReconcileQueue.server"; export type OrgReconcileTarget = { organizationId: string; targetState: BillingLimitConvergeTargetState; }; export function resolveConvergeTargetFromBillingLimit( billingLimit: BillingLimitResult | undefined ): BillingLimitConvergeTargetState { if (!billingLimit?.isConfigured) { return "ok"; } if (billingLimit.limitState.status === "grace") { return "grace"; } if (billingLimit.limitState.status === "rejected") { return "rejected"; } return "ok"; } /** Reconcile path only — skip org when the platform lookup failed (undefined ≠ unconfigured). */ export function resolveReconcileTargetFromBillingLimit( billingLimit: BillingLimitResult | undefined ): BillingLimitConvergeTargetState | undefined { if (billingLimit === undefined) { return undefined; } return resolveConvergeTargetFromBillingLimit(billingLimit); } export async function getOrgIdsWithBillingPauseSource(): Promise { const rows = await prisma.runtimeEnvironment.findMany({ where: { pauseSource: EnvironmentPauseSource.BILLING_LIMIT, }, select: { organizationId: true, }, distinct: ["organizationId"], }); return rows.map((row) => row.organizationId); } export function collectOrgIdsNeedingBillingLimitLookup(options: { staleOrgIds: string[]; queuedOrgIds: string[]; excludeOrgIds: Set; coveredOrgIds: Set; }): string[] { const orgIds = new Set(); for (const organizationId of [...options.staleOrgIds, ...options.queuedOrgIds]) { if (options.excludeOrgIds.has(organizationId) || options.coveredOrgIds.has(organizationId)) { continue; } orgIds.add(organizationId); } return [...orgIds]; } export async function resolveReconcileTargetsForOrgLookups( organizationIds: string[], options?: { getBillingLimit?: (organizationId: string) => Promise; concurrency?: number; } ): Promise> { const lookupBillingLimit = options?.getBillingLimit ?? getBillingLimit; const concurrency = options?.concurrency ?? BILLING_LIMIT_RECONCILE_LOOKUP_CONCURRENCY; const targets = new Map(); await pMap( organizationIds, async (organizationId) => { try { const billingLimit = await lookupBillingLimit(organizationId); const targetState = resolveReconcileTargetFromBillingLimit(billingLimit); if (targetState === undefined) { logger.warn("Skipping billing limit reconcile — platform lookup unavailable", { organizationId, }); return; } targets.set(organizationId, targetState); } catch (error) { logger.error("Failed billing limit lookup for reconcile", { organizationId, error, }); } }, { concurrency } ); return targets; } export async function collectOrgsToReconcile(options?: { excludeOrgIds?: Set }): Promise<{ targets: OrgReconcileTarget[]; queuedOrgIds: string[]; }> { const excludeOrgIds = options?.excludeOrgIds ?? new Set(); const targetByOrgId = new Map(); const activeLimits = await getActiveBillingLimits(); if (activeLimits) { for (const org of activeLimits.orgs) { if (excludeOrgIds.has(org.orgId)) { continue; } targetByOrgId.set(org.orgId, org.limitState); } } const [staleOrgIds, queuedOrgIds] = await Promise.all([ getOrgIdsWithBillingPauseSource(), readBillingLimitReconcileQueue(), ]); const orgIdsNeedingLookup = collectOrgIdsNeedingBillingLimitLookup({ staleOrgIds, queuedOrgIds, excludeOrgIds, coveredOrgIds: new Set(targetByOrgId.keys()), }); const lookedUpTargets = await resolveReconcileTargetsForOrgLookups(orgIdsNeedingLookup); for (const [organizationId, targetState] of lookedUpTargets) { targetByOrgId.set(organizationId, targetState); } return { targets: Array.from(targetByOrgId.entries()).map(([organizationId, targetState]) => ({ organizationId, targetState, })), queuedOrgIds, }; } export async function clearProcessedReconcileQueueEntries( queuedOrgIds: string[], processedOrgIds: string[] ): Promise { const processed = new Set(processedOrgIds); const toRemove = queuedOrgIds.filter((orgId) => processed.has(orgId)); await removeFromBillingLimitReconcileQueue(toRemove); }