diff --git a/services/arbiter-3.0/src/routes/stripe.js b/services/arbiter-3.0/src/routes/stripe.js index df34fba..e15eec0 100644 --- a/services/arbiter-3.0/src/routes/stripe.js +++ b/services/arbiter-3.0/src/routes/stripe.js @@ -11,7 +11,7 @@ const router = express.Router(); const cors = require('cors'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const db = require('../database'); -const { syncRole, removeAllRoles } = require('../services/discordRoleSync'); +const { syncRole, removeAllRoles, downgradeToAwakened } = require('../services/discordRoleSync'); // CORS configuration for checkout endpoint const corsOptions = { @@ -300,20 +300,45 @@ router.post('/webhook', express.raw({ type: 'application/json' }), async (req, r } case 'customer.subscription.deleted': { + // Task #126: "We don't kick people out." + // Awakened ($1) is lifetime. When a paid subscription ends (cancellation + // or Stripe gave up on retries), demote to Awakened rather than remove. + // This event fires at actual period end, so timing is already correct. const subscription = event.data.object; const subId = subscription.id; - // Start grace period (don't remove immediately) - const gracePeriodEnds = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000); // 3 days + // Find the Discord ID for this subscription before updating status + const subRow = await client.query( + 'SELECT discord_id FROM subscriptions WHERE stripe_subscription_id = $1', + [subId] + ); await client.query(` UPDATE subscriptions - SET status = 'grace_period', - grace_period_started_at = CURRENT_TIMESTAMP, - grace_period_ends_at = $1, + SET status = 'lifetime', + tier_level = 1, + is_lifetime = TRUE, + grace_period_started_at = NULL, + grace_period_ends_at = NULL, updated_at = CURRENT_TIMESTAMP - WHERE stripe_subscription_id = $2 AND is_lifetime = FALSE - `, [gracePeriodEnds, subId]); + WHERE stripe_subscription_id = $1 AND is_lifetime = FALSE + `, [subId]); + + // Demote Discord role to Awakened (strips tier role, assigns Awakened) + if (subRow.rows.length > 0) { + const discordId = subRow.rows[0].discord_id; + const roleResult = await downgradeToAwakened(discordId); + console.log(`⬇️ Subscription ended for ${discordId}, demoted to Awakened: ${roleResult.message}`); + + await client.query(` + INSERT INTO admin_audit_log (action_type, target_identifier, details) + VALUES ('SUBSCRIPTION_ENDED_DEMOTED', $1, $2) + `, [discordId, JSON.stringify({ + stripe_subscription_id: subId, + reason: 'subscription_deleted', + policy: 'we_dont_kick_people_out' + })]); + } break; } @@ -412,6 +437,74 @@ router.post('/webhook', express.raw({ type: 'application/json' }), async (req, r break; } + case 'charge.refunded': { + // Task #126: Refund requested = hard ban, with Trinity appeal process. + // Policy: "We only remove all access for actionable hard bans." + // A refund is the customer asking for their money back — mirrors chargeback. + const charge = event.data.object; + const customerId = charge.customer; + const paymentIntentId = charge.payment_intent; + + if (!customerId) { + console.log(`⚠️ charge.refunded with no customer_id (likely guest checkout), skipping`); + break; + } + + // Find the subscription by customer ID + const subResult = await client.query( + 'SELECT discord_id FROM subscriptions WHERE stripe_customer_id = $1 LIMIT 1', + [customerId] + ); + + let discordId = null; + if (subResult.rows.length > 0) { + discordId = subResult.rows[0].discord_id; + } + + // Mark subscription as refund_ban + await client.query(` + UPDATE subscriptions + SET status = 'refund_ban', + updated_at = CURRENT_TIMESTAMP + WHERE stripe_customer_id = $1 + `, [customerId]); + + // Add to banned_users with appeal eligibility noted + if (discordId) { + await client.query(` + INSERT INTO banned_users (discord_id, ban_reason, notes) + VALUES ($1, 'refund', $2) + ON CONFLICT (discord_id) DO UPDATE SET + ban_reason = 'refund', + notes = $2, + banned_at = CURRENT_TIMESTAMP + `, [discordId, JSON.stringify({ + payment_intent: paymentIntentId, + charge_id: charge.id, + amount_refunded: charge.amount_refunded, + appeal_eligible: true, + appeal_process: 'Trinity review' + })]); + + // Remove ALL roles including Awakened (hard ban) + const roleResult = await removeAllRoles(discordId); + console.log(`🚫 Refund ban for ${discordId}: ${roleResult.message}`); + } + + await client.query(` + INSERT INTO admin_audit_log (action_type, target_identifier, details) + VALUES ('REFUND_BAN', $1, $2) + `, [discordId || 'unknown', JSON.stringify({ + payment_intent: paymentIntentId, + customer_id: customerId, + amount_refunded: charge.amount_refunded, + reason: 'Refund processed', + appeal_eligible: true + })]); + + break; + } + default: console.log(`Unhandled event type: ${event.type}`); }