Files
firefrost-services/services/arbiter-3.0/src/sync/graceExpiration.js
Claude (Chronicler #62) 1a97e82ec8 feat(arbiter): implement Task #87 - Lifecycle handlers with Discord role sync
WHAT THIS ADDS:
- Discord role sync on new subscriptions (checkout.session.completed)
- Discord role removal on chargebacks (charge.dispute.created)
- Grace period expiration job (hourly cron check)
- Automatic downgrade to Awakened when grace period expires

NEW FILES:
- src/services/discordRoleSync.js - Role add/remove/sync functions
- src/sync/graceExpiration.js - Grace period expiration processor

MODIFIED FILES:
- src/routes/stripe.js - Added role sync calls to webhook handlers
- src/discord/events.js - Initialize role sync service on bot ready
- src/sync/cron.js - Added grace period check to hourly job
- src/index.js - Import discordRoleSync service

PHILOSOPHY:
'We Don't Kick People Out' - expired grace periods downgrade to
permanent Awakened tier (tier 1, lifetime). Users keep community
access, just lose premium perks.

ROLE MAPPING (tier_level -> role key):
1=the-awakened, 2=fire-elemental, 3=frost-elemental,
4=fire-knight, 5=frost-knight, 6=fire-master, 7=frost-master,
8=fire-legend, 9=frost-legend, 10=the-sovereign

CHARGEBACKS:
- Immediate role removal
- Added to banned_users table
- Full audit logging

Signed-off-by: Claude (Chronicler #62) <claude@firefrostgaming.com>
2026-04-05 14:25:41 +00:00

112 lines
3.4 KiB
JavaScript

/**
* Grace Period Expiration Job
* Checks for expired grace periods and downgrades users to Awakened
*
* Philosophy: "We Don't Kick People Out"
* - Expired grace periods downgrade to permanent Awakened tier
* - Users keep community access, just lose premium perks
*
* Task #87: Arbiter Lifecycle Handlers
* Date: April 6, 2026
*/
const db = require('../database');
const { downgradeToAwakened } = require('../services/discordRoleSync');
/**
* Process all expired grace periods
* Called hourly from cron.js
*
* @returns {Promise<{processed: number, errors: number}>}
*/
async function processExpiredGracePeriods() {
console.log('🔍 Checking for expired grace periods...');
const client = await db.pool.connect();
let processed = 0;
let errors = 0;
try {
// Find all expired grace periods
const { rows: expired } = await client.query(`
SELECT s.discord_id, s.tier_level, u.minecraft_username
FROM subscriptions s
LEFT JOIN users u ON s.discord_id = u.discord_id
WHERE s.status = 'grace_period'
AND s.grace_period_ends_at < NOW()
AND s.is_lifetime = FALSE
`);
if (expired.length === 0) {
console.log('✅ No expired grace periods found');
return { processed: 0, errors: 0 };
}
console.log(`📋 Found ${expired.length} expired grace period(s)`);
for (const sub of expired) {
try {
await client.query('BEGIN');
// Record the tier change in history
await client.query(`
INSERT INTO player_history
(discord_id, previous_tier, new_tier, change_reason)
VALUES ($1, $2, 1, 'grace_period_expired')
`, [sub.discord_id, sub.tier_level]);
// Downgrade to Awakened (tier 1, lifetime)
await client.query(`
UPDATE subscriptions
SET tier_level = 1,
status = 'lifetime',
is_lifetime = TRUE,
mrr_value = 0,
grace_period_started_at = NULL,
grace_period_ends_at = NULL,
payment_failure_reason = NULL,
stripe_subscription_id = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE discord_id = $1
`, [sub.discord_id]);
// Log in audit
await client.query(`
INSERT INTO admin_audit_log
(action_type, target_identifier, details)
VALUES ('GRACE_PERIOD_EXPIRED', $1, $2)
`, [sub.discord_id, JSON.stringify({
previous_tier: sub.tier_level,
new_tier: 1,
minecraft_username: sub.minecraft_username,
reason: 'Automatic downgrade after grace period expiration'
})]);
await client.query('COMMIT');
// Sync Discord role to Awakened
const syncResult = await downgradeToAwakened(sub.discord_id);
if (!syncResult.success) {
console.warn(`⚠️ Role sync failed for ${sub.discord_id}: ${syncResult.message}`);
}
console.log(`✅ Downgraded ${sub.minecraft_username || sub.discord_id} to Awakened`);
processed++;
} catch (error) {
await client.query('ROLLBACK');
console.error(`❌ Error processing ${sub.discord_id}:`, error.message);
errors++;
}
}
} finally {
client.release();
}
console.log(`📊 Grace period processing complete: ${processed} processed, ${errors} errors`);
return { processed, errors };
}
module.exports = { processExpiredGracePeriods };