53 lines · 2.2 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Seat-count reporting — the WorkOS → Autumn reconciliation for seat billing// --------------------------------------------------------------------------- import { Effect } from "effect"; import { WorkOSClient } from "../../auth/workos";import { AutumnService } from "./service"; /** * Report the organization's billable seat count to Autumn: active members * only — a pending invite occupies a seat for the plan gate but is not * billed until the person joins. * * Seats change through paths the app never sees a mutation for (invitation * acceptance in AuthKit, SSO JIT provisioning, join by domain, WorkOS * dashboard edits), so this reconciles from a full recount rather than * tracking deltas. It runs after in-app membership mutations AND on every * login callback, so drift from out-of-band changes heals on the next * sign-in. Fire-and-forget-safe: errors are logged, never surfaced. */export const reportMemberSeats = (  organizationId: string,): Effect.Effect<void, never, WorkOSClient | AutumnService> =>  Effect.gen(function* () {    const workos = yield* WorkOSClient;    const autumn = yield* AutumnService;    const memberships = yield* workos.listOrgMembers(organizationId);    const seats = memberships.data.filter((m) => m.status === "active").length;    yield* autumn.setMemberSeats(organizationId, seats);  }).pipe(    Effect.catch((error) =>      Effect.logWarning("reportMemberSeats: seat recount failed", { organizationId, error }),    ),    Effect.withSpan("billing.reportMemberSeats"),  ); /** * Fork `reportMemberSeats` off the calling request, mirroring how execution * tracking is forked: billing must never stall or fail a user-facing * request. Only boot-scoped services are captured (WorkOS + Autumn — no * request-scoped resources), so the forked fiber cannot outlive anything it * depends on. */export const forkReportMemberSeats = (  organizationId: string,): Effect.Effect<void, never, WorkOSClient | AutumnService> =>  Effect.gen(function* () {    const ctx = yield* Effect.context<WorkOSClient | AutumnService>();    yield* Effect.sync(() => {      Effect.runForkWith(ctx)(reportMemberSeats(organizationId));    });  });