Skip to content

Commit 8f65b96

Browse files
fix(billing): make forced plan overrides indefinite so the expiry cron can't undo them
forceTenantPlanChange created its manual override row with a +1 month current_period_end, which put it squarely in the path of the expire-platform-subscriptions cron (#471): the school would get renewal reminders for a payment it never requested, then be silently downgraded back to free a month later. The reactivation branch had the same bug via a stale period end on a previously-canceled row. Every cron phase filters on `.not('current_period_end', 'is', null)`, so a NULL period end is the natural "indefinite override" marker: the row is never reminded, never lapses, never auto-downgrades. Keep a real period only when reactivating a sub already on a live paid cycle; a school that later pays via confirmManualPayment upserts a dated cycle over the row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HveVmDShzscMQVQdd9LuDZ
1 parent 31805d3 commit 8f65b96

1 file changed

Lines changed: 35 additions & 9 deletions

File tree

app/actions/platform/plans.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export async function forceTenantPlanChange(tenantId: string, planSlug: string)
225225
// override.
226226
const { data: existingSub } = await adminClient
227227
.from('platform_subscriptions')
228-
.select('subscription_id')
228+
.select('subscription_id, status, current_period_end')
229229
.eq('tenant_id', tenantId)
230230
.maybeSingle()
231231

@@ -244,18 +244,44 @@ export async function forceTenantPlanChange(tenantId: string, planSlug: string)
244244
.eq('tenant_id', tenantId)
245245
}
246246
} else if (existingSub) {
247-
// Point the existing subscription at the forced plan; preserve its period
248-
// and payment method.
247+
// Point the existing subscription at the forced plan. Preserve the billing
248+
// period only when the sub is on a live paid cycle (active with a future
249+
// period end); otherwise — canceled/expired row, or a lapsed period —
250+
// reactivating with the stale current_period_end would hand the row
251+
// straight to the expire-platform-subscriptions cron (past_due, then
252+
// auto-downgrade after grace), silently undoing the override. Clear the
253+
// period instead so the override is indefinite, like the insert below.
254+
const liveCycle =
255+
existingSub.status === 'active' &&
256+
existingSub.current_period_end != null &&
257+
new Date(existingSub.current_period_end) > new Date()
249258
await adminClient
250259
.from('platform_subscriptions')
251-
.update({ plan_id: plan.plan_id, status: 'active', updated_at: nowIso })
260+
.update({
261+
plan_id: plan.plan_id,
262+
status: 'active',
263+
updated_at: nowIso,
264+
...(liveCycle
265+
? {}
266+
: {
267+
current_period_end: null,
268+
grace_period_end: null,
269+
cancel_at_period_end: false,
270+
canceled_at: null,
271+
}),
272+
})
252273
.eq('tenant_id', tenantId)
253274
} else {
254275
// No subscription yet (e.g. tenant was on free): create a manual override
255276
// row so the paid plan is backed by an active subscription.
256-
const periodStart = new Date()
257-
const periodEnd = new Date(periodStart)
258-
periodEnd.setMonth(periodEnd.getMonth() + 1)
277+
//
278+
// current_period_end stays NULL on purpose: a super-admin override is
279+
// indefinite, not a one-month grant. The expire-platform-subscriptions
280+
// cron filters every phase on `.not('current_period_end', 'is', null)`,
281+
// so a NULL period end is never reminded, never lapses to past_due, and
282+
// never auto-downgrades — the override holds until a super admin changes
283+
// it again or the school starts paying (confirmManualPayment then upserts
284+
// a real dated cycle over this row).
259285
await adminClient
260286
.from('platform_subscriptions')
261287
.insert({
@@ -264,8 +290,8 @@ export async function forceTenantPlanChange(tenantId: string, planSlug: string)
264290
status: 'active',
265291
payment_method: 'manual_transfer',
266292
interval: 'monthly',
267-
current_period_start: periodStart.toISOString(),
268-
current_period_end: periodEnd.toISOString(),
293+
current_period_start: nowIso,
294+
current_period_end: null,
269295
})
270296
}
271297

0 commit comments

Comments
 (0)