fix(billing): downgrade orgs when Polar subscription is revoked (#759)
* fix(billing): downgrade orgs when Polar subscription is revoked The Polar webhook handler had subscription.revoked as a no-op, so subscriptions that expired or failed to renew never got the org's organization_plan row deactivated, leaving them stuck on a paid plan indefinitely. Handle subscription.revoked the same way as an inactive subscription.updated event. Also add a one-time backfill script (db:downgrade-expired-subscriptions) that re-checks every currently-active org plan against Polar directly and downgrades any whose subscription already ended, to fix orgs affected before this webhook fix shipped. * Update packages/db/src/scripts/downgrade-expired-subscriptions.ts ---------
This commit is contained in:
@@ -8,7 +8,21 @@ import type {
|
||||
TOrganization,
|
||||
TOrganizationPlan
|
||||
} from '@db/types';
|
||||
import { and, asc, count, desc, eq, gt, ilike, inArray, isNotNull, notInArray, or, sql } from 'drizzle-orm';
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
getTableColumns,
|
||||
gt,
|
||||
ilike,
|
||||
inArray,
|
||||
isNotNull,
|
||||
notInArray,
|
||||
or,
|
||||
sql
|
||||
} from 'drizzle-orm';
|
||||
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
@@ -1007,6 +1021,33 @@ export const getOrganizationPlanBySubscriptionId = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets every active organization plan that has a Polar subscription ID attached.
|
||||
* Used by one-off backfills that need to re-verify subscription status against Polar.
|
||||
* @param dbClient Optional DB or transaction client for use within transactions
|
||||
* @returns Active organization plans with org name/siteName for readable logging
|
||||
*/
|
||||
export const getActiveOrganizationPlansWithSubscription = async (
|
||||
dbClient: DbOrTxClient = db
|
||||
): Promise<Array<TOrganizationPlan & { orgName: string; orgSiteName: string | null }>> => {
|
||||
try {
|
||||
return await dbClient
|
||||
.select({
|
||||
...getTableColumns(schema.organizationPlan),
|
||||
orgName: schema.organization.name,
|
||||
orgSiteName: schema.organization.siteName
|
||||
})
|
||||
.from(schema.organizationPlan)
|
||||
.innerJoin(schema.organization, eq(schema.organization.id, schema.organizationPlan.orgId))
|
||||
.where(and(eq(schema.organizationPlan.isActive, true), isNotNull(schema.organizationPlan.subscriptionId)));
|
||||
} catch (error) {
|
||||
console.error('getActiveOrganizationPlansWithSubscription error:', error);
|
||||
throw new Error(
|
||||
`Failed to fetch active organization plans: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const createOrganizationPlan = async (
|
||||
data: TNewOrganizationPlan,
|
||||
dbClient: DbOrTxClient = db
|
||||
|
||||
Reference in New Issue
Block a user