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>
112 lines
3.4 KiB
JavaScript
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 };
|