diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index 7316a4e7..444b4600 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -14,7 +14,6 @@ import type * as auth from "../auth.js"; import type * as billing from "../billing.js"; import type * as billing_checkoutIntent from "../billing/checkoutIntent.js"; import type * as billing_polar from "../billing/polar.js"; -import type * as billingMigrations from "../billingMigrations.js"; import type * as billingModel from "../billingModel.js"; import type * as billingRetention from "../billingRetention.js"; import type * as billing_webhook_model from "../billing_webhook_model.js"; @@ -102,7 +101,6 @@ declare const fullApi: ApiFromModules<{ billing: typeof billing; "billing/checkoutIntent": typeof billing_checkoutIntent; "billing/polar": typeof billing_polar; - billingMigrations: typeof billingMigrations; billingModel: typeof billingModel; billingRetention: typeof billingRetention; billing_webhook_model: typeof billing_webhook_model; diff --git a/packages/backend/convex/billingMigrations.test.ts b/packages/backend/convex/billingMigrations.test.ts deleted file mode 100644 index 434741d9..00000000 --- a/packages/backend/convex/billingMigrations.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - migrateBillingSnapshotDocument, - migrateSeatSyncOperationDocument, - migrateWorkspaceEntitlementDocument, -} from "./billingMigrations"; - -describe("billing domain migration", () => { - test("removes legacy entitlement seat fields", () => { - const migrated = migrateWorkspaceEntitlementDocument({ - organizationId: "org-1", - plan: "plus", - subscriptionStatus: "entitled", - statusReason: "polar:active", - plusEnabled: true, - paidSeatCapacity: 8, - billableSeatCount: 8, - effectiveFrom: 1, - policyVersion: "polar-entitlements-v1", - derivedAt: 1, - updatedAt: 1, - }); - - expect(migrated).toEqual({ - organizationId: "org-1", - plan: "plus", - subscriptionStatus: "entitled", - statusReason: "polar:active", - plusEnabled: true, - effectiveFrom: 1, - policyVersion: "polar-entitlements-v1", - derivedAt: 1, - updatedAt: 1, - }); - }); - - test("separates workspace membership from the provider seat quantity", () => { - const migrated = migrateBillingSnapshotDocument({ - organizationId: "org-1", - membershipRevision: '["m1","m2"]', - memberIds: ["m2", "m1"], - billableSeatCount: 2, - source: "checkout", - observedAt: 1, - }); - - expect(migrated.workspaceMemberCount).toBe(2); - expect(migrated.seatQuantity).toBe(2); - expect(migrated.memberIds).toEqual(["m1", "m2"]); - expect(migrated).not.toHaveProperty("billableSeatCount"); - }); - - test("normalizes duplicate legacy members before deriving quantities", () => { - const migrated = migrateBillingSnapshotDocument({ - organizationId: "org-1", - membershipRevision: '["m1","m2"]', - memberIds: ["m2", "m1", "m2"], - billableSeatCount: 3, - source: "checkout", - observedAt: 1, - }); - - expect(migrated.memberIds).toEqual(["m1", "m2"]); - expect(migrated.workspaceMemberCount).toBe(2); - expect(migrated.seatQuantity).toBe(2); - }); - - test("renames seat sync quantities without changing the operation", () => { - const migrated = migrateSeatSyncOperationDocument({ - organizationId: "org-1", - subscriptionId: "subscription-1" as never, - membershipRevision: "revision-1", - previousSeats: 2, - targetSeats: 3, - idempotencyKey: "seat:subscription-1:revision-1", - status: "pending", - attemptCount: 0, - createdAt: 1, - updatedAt: 1, - }); - - expect(migrated.previousSeatQuantity).toBe(2); - expect(migrated.targetSeatQuantity).toBe(3); - expect(migrated).not.toHaveProperty("previousSeats"); - expect(migrated).not.toHaveProperty("targetSeats"); - }); -}); diff --git a/packages/backend/convex/billingMigrations.ts b/packages/backend/convex/billingMigrations.ts deleted file mode 100644 index 6bff29e4..00000000 --- a/packages/backend/convex/billingMigrations.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { internalMutation } from "./_generated/server"; -import { v } from "convex/values"; -import type { Doc } from "./_generated/dataModel"; - -const BATCH_SIZE = 100; - -type WorkspaceEntitlementValue = Omit< - Doc<"workspaceEntitlements">, - "_id" | "_creationTime" ->; -type BillingSnapshotValue = Omit< - Doc<"billingSeatSnapshots">, - "_id" | "_creationTime" ->; -type SeatSyncOperationValue = Omit< - Doc<"billingSeatSyncOperations">, - "_id" | "_creationTime" ->; - -type LegacyWorkspaceEntitlement = WorkspaceEntitlementValue & { - paidSeatCapacity?: number; - billableSeatCount?: number; -}; - -type LegacyBillingSnapshot = Omit< - BillingSnapshotValue, - "workspaceMemberCount" | "seatQuantity" -> & - Partial< - Pick - > & { - billableSeatCount?: number; - }; - -type LegacySeatSyncOperation = Omit< - SeatSyncOperationValue, - "previousSeatQuantity" | "targetSeatQuantity" -> & - Partial< - Pick - > & { - previousSeats?: number; - targetSeats?: number; - }; - -export function migrateWorkspaceEntitlementDocument( - document: LegacyWorkspaceEntitlement, -): WorkspaceEntitlementValue { - return { - organizationId: document.organizationId, - plan: document.plan, - subscriptionStatus: document.subscriptionStatus, - statusReason: document.statusReason, - plusEnabled: document.plusEnabled, - sourceSubscriptionId: document.sourceSubscriptionId, - sourceEventId: document.sourceEventId, - effectiveFrom: document.effectiveFrom, - effectiveThrough: document.effectiveThrough, - policyVersion: document.policyVersion, - derivedAt: document.derivedAt, - updatedAt: document.updatedAt, - }; -} - -export function migrateBillingSnapshotDocument( - document: LegacyBillingSnapshot, -): BillingSnapshotValue { - const memberIds = [...new Set(document.memberIds)].sort(); - const workspaceMemberCount = memberIds.length; - const seatQuantity = - document.seatQuantity ?? Math.max(1, workspaceMemberCount); - return { - organizationId: document.organizationId, - subscriptionId: document.subscriptionId, - membershipRevision: document.membershipRevision, - memberIds, - workspaceMemberCount, - seatQuantity, - source: document.source, - observedAt: document.observedAt, - }; -} - -export function migrateSeatSyncOperationDocument( - document: LegacySeatSyncOperation, -): SeatSyncOperationValue { - const previousSeatQuantity = - document.previousSeatQuantity ?? document.previousSeats; - const targetSeatQuantity = - document.targetSeatQuantity ?? document.targetSeats; - if (previousSeatQuantity === undefined || targetSeatQuantity === undefined) { - throw new Error("Seat sync operation has no seat quantities"); - } - return { - organizationId: document.organizationId, - subscriptionId: document.subscriptionId, - membershipRevision: document.membershipRevision, - previousSeatQuantity, - targetSeatQuantity, - idempotencyKey: document.idempotencyKey, - status: document.status, - attemptCount: document.attemptCount, - leaseExpiresAt: document.leaseExpiresAt, - nextAttemptAt: document.nextAttemptAt, - failureCode: document.failureCode, - providerModifiedAt: document.providerModifiedAt, - createdAt: document.createdAt, - updatedAt: document.updatedAt, - }; -} - -const migrationTable = v.union( - v.literal("workspaceEntitlements"), - v.literal("billingSeatSnapshots"), - v.literal("billingSeatSyncOperations"), -); - -export const migrateBatch = internalMutation({ - args: { - table: migrationTable, - cursor: v.optional(v.string()), - batchSize: v.optional(v.number()), - }, - handler: async (ctx, args) => { - const batchSize = Math.max(1, Math.min(args.batchSize ?? BATCH_SIZE, 100)); - const cursor = args.cursor ?? null; - - if (args.table === "workspaceEntitlements") { - const page = await ctx.db - .query("workspaceEntitlements") - .paginate({ numItems: batchSize, cursor }); - for (const document of page.page) { - await ctx.db.replace( - document._id, - migrateWorkspaceEntitlementDocument( - document as LegacyWorkspaceEntitlement, - ), - ); - } - return { - table: args.table, - processed: page.page.length, - isDone: page.isDone, - nextCursor: page.isDone ? null : page.continueCursor, - }; - } - - if (args.table === "billingSeatSnapshots") { - const page = await ctx.db - .query("billingSeatSnapshots") - .paginate({ numItems: batchSize, cursor }); - for (const document of page.page) { - await ctx.db.replace( - document._id, - migrateBillingSnapshotDocument(document as LegacyBillingSnapshot), - ); - } - return { - table: args.table, - processed: page.page.length, - isDone: page.isDone, - nextCursor: page.isDone ? null : page.continueCursor, - }; - } - - const page = await ctx.db - .query("billingSeatSyncOperations") - .paginate({ numItems: batchSize, cursor }); - for (const document of page.page) { - await ctx.db.replace( - document._id, - migrateSeatSyncOperationDocument(document as LegacySeatSyncOperation), - ); - } - return { - table: args.table, - processed: page.page.length, - isDone: page.isDone, - nextCursor: page.isDone ? null : page.continueCursor, - }; - }, -}); diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 989e08c4..7059176b 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -15,7 +15,7 @@ import { billingTables } from "./schema/billing"; import { aiCreditTables } from "./schema/aiCredits"; import { storageTelemetryTables } from "./schema/storageTelemetry"; -const schema = { +export default defineSchema({ ...workspaceTables, ...billingTables, ...aiCreditTables, @@ -562,6 +562,4 @@ const schema = { .index("by_connection", ["connectionId"]) .index("by_connection_external", ["connectionId", "externalId"]) .index("by_organization_provider", ["organizationId", "provider"]), -}; - -export default defineSchema(schema, { schemaValidation: false }); +});