| 384 | eq(coding_plan_subscriptions.user_id, userId), |
| 385 | eq(coding_plan_subscriptions.status, 'active') |
| 386 | ) |
| 387 | ); |
| 388 | if ((result.rowCount ?? 0) === 0) { |
| 389 | throw new Error('No active subscription found.'); |
| 390 | } |
| 391 | logInfo('Coding plan cancellation scheduled', { user_id: userId, subscriptionId }); |
| 392 | } |
| 393 | |
| 394 | export async function terminateCodingPlanImmediately( |
| 395 | subscriptionId: string, |
| 396 | reason: CancellationReason = 'administrative_termination' |
| 397 | ): Promise<void> { |
| 398 | await db.transaction(async tx => { |
| 399 | // Lock the subscription row before reading it so this can't act on a |
| 400 | // stale installed_byok_key_id / key_inventory_id snapshot if it races |
| 401 | // with a concurrent credential reassignment for the same subscription. |
| 402 | const [subscription] = await tx |
| 403 | .select({ |
| 404 | id: coding_plan_subscriptions.id, |
| 405 | status: coding_plan_subscriptions.status, |
| 406 | installed_byok_key_id: coding_plan_subscriptions.installed_byok_key_id, |
| 407 | key_inventory_id: coding_plan_subscriptions.key_inventory_id, |
| 408 | }) |
| 409 | .from(coding_plan_subscriptions) |
| 410 | .where(eq(coding_plan_subscriptions.id, subscriptionId)) |
| 411 | .for('update'); |
| 412 | if (!subscription || (subscription.status !== 'active' && subscription.status !== 'past_due')) { |
| 413 | throw new Error('No live subscription found.'); |
| 414 | } |
| 415 | |
| 416 | await tx |
| 417 | .update(coding_plan_subscriptions) |
| 418 | .set({ |
| 419 | status: 'canceled', |
| 420 | canceled_at: sql`now()`, |
| 421 | cancellation_reason: reason, |
| 422 | installed_byok_key_id: null, |
| 423 | cancel_at_period_end: false, |
| 424 | past_due_started_at: null, |
| 425 | payment_grace_expires_at: null, |
| 426 | auto_top_up_attempted_for_due: null, |
| 427 | }) |
| 428 | .where(eq(coding_plan_subscriptions.id, subscription.id)); |
| 429 | if (subscription.installed_byok_key_id) { |
| 430 | await tx |
| 431 | .delete(byok_api_keys) |
| 432 | .where( |
| 433 | and( |
| 434 | eq(byok_api_keys.id, subscription.installed_byok_key_id), |
| 435 | eq(byok_api_keys.management_source, 'coding_plan') |
| 436 | ) |
| 437 | ); |
| 438 | } |
| 439 | if (subscription.key_inventory_id) { |
| 440 | await tx |
| 441 | .update(coding_plan_key_inventory) |
| 442 | .set({ |
| 443 | status: 'revocation_pending', |