/** * @ai-summary Grace period expiration job * @ai-context Processes expired grace periods and downgrades subscriptions to free tier */ import { Pool } from 'pg'; import { logger } from '../../../core/logging/logger'; let jobPool: Pool | null = null; export function setGracePeriodJobPool(pool: Pool): void { jobPool = pool; } interface GracePeriodResult { processed: number; downgraded: number; errors: string[]; } /** * Process grace period expirations * Finds subscriptions with expired grace periods and downgrades them to free tier */ export async function processGracePeriodExpirations(): Promise { if (!jobPool) { throw new Error('Grace period job pool not initialized'); } const result: GracePeriodResult = { processed: 0, downgraded: 0, errors: [], }; const client = await jobPool.connect(); try { // Find subscriptions with expired grace periods const query = ` SELECT id, user_id, tier, stripe_subscription_id FROM subscriptions WHERE status = 'past_due' AND grace_period_end < NOW() ORDER BY grace_period_end ASC `; const queryResult = await client.query(query); const expiredSubscriptions = queryResult.rows; result.processed = expiredSubscriptions.length; logger.info('Processing expired grace periods', { count: expiredSubscriptions.length, }); // Process each expired subscription for (const subscription of expiredSubscriptions) { try { // Start transaction for this subscription await client.query('BEGIN'); // Update subscription to free tier and unpaid status const updateQuery = ` UPDATE subscriptions SET tier = 'free', status = 'unpaid', stripe_subscription_id = NULL, billing_cycle = NULL, current_period_start = NULL, current_period_end = NULL, grace_period_end = NULL, cancel_at_period_end = false, updated_at = NOW() WHERE id = $1 `; await client.query(updateQuery, [subscription.id]); // Sync tier to user_profiles table const syncQuery = ` UPDATE user_profiles SET subscription_tier = 'free', updated_at = NOW() WHERE user_id = $1 `; await client.query(syncQuery, [subscription.user_id]); // Commit transaction await client.query('COMMIT'); result.downgraded++; logger.info('Grace period expired - downgraded to free', { subscriptionId: subscription.id, userId: subscription.user_id, previousTier: subscription.tier, }); } catch (error: any) { // Rollback transaction on error await client.query('ROLLBACK'); const errorMsg = `Failed to downgrade subscription ${subscription.id}: ${error.message}`; result.errors.push(errorMsg); logger.error('Failed to process grace period expiration', { subscriptionId: subscription.id, userId: subscription.user_id, error: error.message, }); } } logger.info('Grace period expiration job completed', { processed: result.processed, downgraded: result.downgraded, errors: result.errors.length, }); } catch (error: any) { logger.error('Grace period job failed', { error: error.message, }); throw error; } finally { client.release(); } return result; }