diff --git a/docs/tasks/arbiter-2-1-cancellation-flow/README.md b/docs/tasks/arbiter-2-1-cancellation-flow/README.md new file mode 100644 index 0000000..8c14ad2 --- /dev/null +++ b/docs/tasks/arbiter-2-1-cancellation-flow/README.md @@ -0,0 +1,1061 @@ +# Task #87: Arbiter 2.1 - Subscription Cancellation & Grace Period System + +**Status:** IDENTIFIED - Critical for soft launch +**Owner:** Michael "Frostystyle" Krause +**Priority:** Tier 1 - SOFT LAUNCH BLOCKER +**Created:** March 30, 2026 +**Time Estimate:** 4-6 hours +**Architecture Partner:** Gemini AI (review requested) + +--- + +## ⚠️ CRITICAL: Soft Launch Blocker + +**We have a subscription process, but NO unsubscribe process.** + +**Current state:** +- ✅ Subscribe → Paymenter → Arbiter 2.0 → Discord role → Access granted +- ❌ Cancel → ??? (undefined) +- ❌ Payment fails → ??? (undefined) +- ❌ Subscription expires → ??? (undefined) +- ❌ Chargeback → ??? (undefined) + +**Cannot launch without defining offboarding flow.** + +--- + +## Problem Statement + +### What We Built (Arbiter 2.0) +- Subscription onboarding complete +- Email → OAuth → Discord role assignment +- Admin panel for manual operations +- Audit logging + +### What We're Missing (Arbiter 2.1) +- **Cancellation handling** - What happens when someone cancels? +- **Payment failure handling** - Grace period for failed payments? +- **Subscription expiration** - When does access actually end? +- **Discord role removal** - Automated or manual? +- **Whitelist management** - Keep forever, remove immediately, or grace period? +- **Grace period tracking** - How long between cancel and full removal? +- **Email notifications** - Cancellation confirmations, payment failure warnings +- **Automated cleanup** - Remove roles/whitelists after grace periods + +--- + +## Policy Decisions (Made March 30, 2026) + +### Decision 1: Discord Role Removal Timing +**DECISION: Remove at end of billing period** + +**Rationale:** +- User paid through end of month → they get what they paid for +- Fair to customer +- Industry standard practice + +**Example:** +- Subscribed March 1 → March 31 ($10) +- Cancels March 15 +- Discord role stays active until March 31 23:59 +- April 1 00:00 → role removed automatically + +### Decision 2: Whitelist Grace Period +**DECISION: 30-day grace period after cancellation** + +**Rationale:** +- Goodwill gesture (might come back) +- Gives them time to reconsider +- Not so long that whitelist gets cluttered +- Clean removal after reasonable time + +**Example:** +- Cancels March 15 +- Billing period ends March 31 +- Whitelist stays until April 30 +- May 1 → whitelist removed automatically + +### Decision 3: Payment Failure Grace Period +**DECISION: 7-day grace with email reminders** + +**Rationale:** +- Cards expire, people forget to update +- 3 days = not enough time +- 30 days = too generous +- 7 days = industry standard + +**Timeline:** +- Day 0: Payment fails → email sent +- Day 3: Reminder email (4 days left) +- Day 6: Final warning (24 hours) +- Day 7: Treat as cancellation (Discord role removed at billing end) + +### Decision 4: Chargeback Handling +**DECISION: Immediate removal + flag account** + +**Rationale:** +- Fraud protection +- Chargebacks cost us money +- No grace period justified + +**Action:** +- Immediate Discord role removal +- Immediate whitelist removal +- Flag account (prevent re-subscription without manual review) +- Manual review by Michael/Meg required to unblock + +--- + +## Architecture: Arbiter 2.0 → 2.1 + +**THIS IS NOT A REWRITE - IT'S AN ENHANCEMENT** + +### What Stays the Same +✅ All Arbiter 2.0 code (OAuth, linking, role assignment, admin panel) +✅ Database structure (just adding tables) +✅ Webhook handler architecture (just adding event types) +✅ Email system (just adding templates) +✅ Discord integration (just adding role removal) + +### What Gets Added +➕ New database tables (subscriptions, grace_periods) +➕ New webhook event handlers (4 new functions) +➕ Scheduled cleanup job (new file) +➕ Email templates (5 new files) +➕ Grace period logic +➕ Whitelist Manager API integration + +**Estimated new code: ~1,000 lines** +**Existing code preserved: ~2,000 lines** + +--- + +## Technical Implementation Plan + +### 1. Database Schema Enhancement + +**Add two new tables to existing SQLite database:** + +```sql +-- Track subscription lifecycle and status +CREATE TABLE subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + paymenter_subscription_id TEXT NOT NULL UNIQUE, + tier TEXT NOT NULL, + discord_user_id TEXT, + status TEXT NOT NULL, -- active, cancelled, expired, suspended, chargeback + billing_period_start DATETIME, + billing_period_end DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Track grace periods for automated cleanup +CREATE TABLE grace_periods ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + subscription_id INTEGER NOT NULL, + type TEXT NOT NULL, -- payment_failure, discord_removal, whitelist_removal + started_at DATETIME NOT NULL, + ends_at DATETIME NOT NULL, + resolved BOOLEAN DEFAULT 0, + resolved_at DATETIME, + FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) +); + +-- Index for performance +CREATE INDEX idx_grace_periods_ends_at ON grace_periods(ends_at, resolved); +CREATE INDEX idx_subscriptions_status ON subscriptions(status); +``` + +**Why these tables:** +- `subscriptions` - Single source of truth for subscription state +- `grace_periods` - Automated cleanup jobs query this +- Separate from `link_tokens` table (different lifecycle) + +### 2. New Webhook Event Handlers + +**Enhance existing webhook router:** + +```javascript +// routes/webhook.js (EXISTING FILE - ADDING CASES) +app.post('/webhook/paymenter', verifyWebhook, async (req, res) => { + const { event, data } = req.body; + + try { + switch(event) { + // EXISTING (Arbiter 2.0) + case 'subscription.created': + await handleSubscriptionCreated(data); + break; + + // NEW (Arbiter 2.1) + case 'subscription.cancelled': + await handleSubscriptionCancelled(data); + break; + + case 'subscription.expired': + await handleSubscriptionExpired(data); + break; + + case 'payment.failed': + await handlePaymentFailed(data); + break; + + case 'subscription.renewed': + await handleSubscriptionRenewed(data); + break; + + case 'chargeback.received': + await handleChargeback(data); + break; + + default: + console.log(`Unknown event: ${event}`); + } + + res.status(200).send('OK'); + } catch (error) { + console.error('Webhook error:', error); + res.status(500).send('Error processing webhook'); + } +}); +``` + +**NEW handler functions to create:** + +#### handlers/subscriptionCancelled.js +```javascript +async function handleSubscriptionCancelled(data) { + const { email, subscription_id, tier, billing_period_end } = data; + + // 1. Update subscription status + await db.run(` + UPDATE subscriptions + SET status = 'cancelled', + billing_period_end = ?, + updated_at = CURRENT_TIMESTAMP + WHERE paymenter_subscription_id = ? + `, [billing_period_end, subscription_id]); + + // 2. Create grace period for Discord role removal (until billing end) + await db.run(` + INSERT INTO grace_periods (subscription_id, type, started_at, ends_at) + SELECT id, 'discord_removal', CURRENT_TIMESTAMP, ? + FROM subscriptions WHERE paymenter_subscription_id = ? + `, [billing_period_end, subscription_id]); + + // 3. Create grace period for whitelist removal (30 days after billing end) + const whitelistGraceEnd = new Date(billing_period_end); + whitelistGraceEnd.setDate(whitelistGraceEnd.getDate() + 30); + + await db.run(` + INSERT INTO grace_periods (subscription_id, type, started_at, ends_at) + SELECT id, 'whitelist_removal', CURRENT_TIMESTAMP, ? + FROM subscriptions WHERE paymenter_subscription_id = ? + `, [whitelistGraceEnd.toISOString(), subscription_id]); + + // 4. Send cancellation confirmation email + await sendEmail(email, 'subscription_cancelled', { + tier, + access_until: billing_period_end, + whitelist_until: whitelistGraceEnd.toISOString() + }); + + // 5. Log to audit trail + await logAudit('system', email, 'subscription_cancelled', + `Tier: ${tier}, Access until: ${billing_period_end}`); +} +``` + +#### handlers/paymentFailed.js +```javascript +async function handlePaymentFailed(data) { + const { email, subscription_id, tier, amount, reason } = data; + + // 1. Update subscription status + await db.run(` + UPDATE subscriptions + SET status = 'payment_failed', + updated_at = CURRENT_TIMESTAMP + WHERE paymenter_subscription_id = ? + `, [subscription_id]); + + // 2. Create 7-day grace period + const graceEnd = new Date(); + graceEnd.setDate(graceEnd.getDate() + 7); + + await db.run(` + INSERT INTO grace_periods (subscription_id, type, started_at, ends_at) + SELECT id, 'payment_failure', CURRENT_TIMESTAMP, ? + FROM subscriptions WHERE paymenter_subscription_id = ? + `, [graceEnd.toISOString(), subscription_id]); + + // 3. Send payment failure notification + await sendEmail(email, 'payment_failed', { + tier, + amount, + reason, + update_url: 'https://billing.firefrostgaming.com/payment-methods', + grace_days: 7 + }); + + // 4. Log to audit trail + await logAudit('system', email, 'payment_failed', + `Tier: ${tier}, Amount: ${amount}, Reason: ${reason}`); +} +``` + +#### handlers/chargeback.js +```javascript +async function handleChargeback(data) { + const { email, subscription_id, tier, amount } = data; + + // 1. Update subscription status + await db.run(` + UPDATE subscriptions + SET status = 'chargeback', + updated_at = CURRENT_TIMESTAMP + WHERE paymenter_subscription_id = ? + `, [subscription_id]); + + // 2. Get Discord user ID + const sub = await db.get( + 'SELECT discord_user_id FROM subscriptions WHERE paymenter_subscription_id = ?', + [subscription_id] + ); + + // 3. IMMEDIATE removal - Discord role + if (sub.discord_user_id) { + await removeDiscordRole(sub.discord_user_id, tier); + } + + // 4. IMMEDIATE removal - Whitelist + await removeFromWhitelist(email); + + // 5. Send notification email + await sendEmail(email, 'account_suspended_chargeback', { + tier, + amount, + contact_email: 'support@firefrostgaming.com' + }); + + // 6. Log to audit trail (high priority) + await logAudit('system', email, 'chargeback_immediate_removal', + `Tier: ${tier}, Amount: ${amount} - MANUAL REVIEW REQUIRED`); + + // 7. Notify Michael/Meg via Discord webhook + await sendDiscordAlert('🚨 CHARGEBACK ALERT', + `Email: ${email}\nTier: ${tier}\nAmount: ${amount}\n\nAccount suspended. Manual review required.`); +} +``` + +#### handlers/subscriptionExpired.js +```javascript +async function handleSubscriptionExpired(data) { + const { email, subscription_id, tier } = data; + + // 1. Update subscription status + await db.run(` + UPDATE subscriptions + SET status = 'expired', + updated_at = CURRENT_TIMESTAMP + WHERE paymenter_subscription_id = ? + `, [subscription_id]); + + // 2. Get Discord user ID + const sub = await db.get( + 'SELECT discord_user_id FROM subscriptions WHERE paymenter_subscription_id = ?', + [subscription_id] + ); + + // 3. Remove Discord role (billing period already ended) + if (sub.discord_user_id) { + await removeDiscordRole(sub.discord_user_id, tier); + } + + // 4. Create 30-day whitelist grace period + const whitelistGraceEnd = new Date(); + whitelistGraceEnd.setDate(whitelistGraceEnd.getDate() + 30); + + await db.run(` + INSERT INTO grace_periods (subscription_id, type, started_at, ends_at) + SELECT id, 'whitelist_removal', CURRENT_TIMESTAMP, ? + FROM subscriptions WHERE paymenter_subscription_id = ? + `, [whitelistGraceEnd.toISOString(), subscription_id]); + + // 5. Send expiration email + await sendEmail(email, 'subscription_expired', { + tier, + whitelist_until: whitelistGraceEnd.toISOString(), + resubscribe_url: 'https://firefrostgaming.com/join' + }); + + // 6. Log to audit trail + await logAudit('system', email, 'subscription_expired', + `Tier: ${tier}, Whitelist grace until: ${whitelistGraceEnd.toISOString()}`); +} +``` + +### 3. Scheduled Cleanup Job + +**NEW FILE: jobs/cleanupExpiredSubscriptions.js** + +```javascript +const cron = require('node-cron'); +const db = require('../database'); +const { removeDiscordRole } = require('../services/discordService'); +const { removeFromWhitelist } = require('../services/whitelistService'); +const { sendEmail } = require('../services/emailService'); +const { logAudit } = require('../services/auditService'); + +// Run daily at 4:00 AM +cron.schedule('0 4 * * *', async () => { + console.log('[Cleanup Job] Starting subscription cleanup...'); + + try { + // 1. Remove Discord roles for expired billing periods + const expiredBilling = await db.all(` + SELECT s.*, gp.id as grace_id + FROM subscriptions s + JOIN grace_periods gp ON s.id = gp.subscription_id + WHERE gp.type = 'discord_removal' + AND gp.ends_at < datetime('now') + AND gp.resolved = 0 + AND s.discord_user_id IS NOT NULL + `); + + console.log(`[Cleanup Job] Found ${expiredBilling.length} expired billing periods`); + + for (const sub of expiredBilling) { + try { + await removeDiscordRole(sub.discord_user_id, sub.tier); + await db.run('UPDATE grace_periods SET resolved = 1, resolved_at = CURRENT_TIMESTAMP WHERE id = ?', + [sub.grace_id]); + await logAudit('system', sub.email, 'discord_role_removed_billing_expired', + `Tier: ${sub.tier}`); + console.log(`[Cleanup Job] Removed Discord role: ${sub.email}`); + } catch (error) { + console.error(`[Cleanup Job] Error removing Discord role for ${sub.email}:`, error); + } + } + + // 2. Remove whitelists after 30-day grace period + const expiredWhitelists = await db.all(` + SELECT s.*, gp.id as grace_id + FROM subscriptions s + JOIN grace_periods gp ON s.id = gp.subscription_id + WHERE gp.type = 'whitelist_removal' + AND gp.ends_at < datetime('now') + AND gp.resolved = 0 + `); + + console.log(`[Cleanup Job] Found ${expiredWhitelists.length} expired whitelists`); + + for (const sub of expiredWhitelists) { + try { + await removeFromWhitelist(sub.email); + await db.run('UPDATE grace_periods SET resolved = 1, resolved_at = CURRENT_TIMESTAMP WHERE id = ?', + [sub.grace_id]); + await logAudit('system', sub.email, 'whitelist_removed_grace_expired', + `Grace period: 30 days`); + console.log(`[Cleanup Job] Removed whitelist: ${sub.email}`); + } catch (error) { + console.error(`[Cleanup Job] Error removing whitelist for ${sub.email}:`, error); + } + } + + // 3. Send payment failure reminders (Day 3 - 4 days left) + const day3Reminders = await db.all(` + SELECT s.*, gp.id as grace_id + FROM subscriptions s + JOIN grace_periods gp ON s.id = gp.subscription_id + WHERE gp.type = 'payment_failure' + AND gp.ends_at BETWEEN datetime('now', '+3 days') AND datetime('now', '+4 days') + AND gp.resolved = 0 + `); + + console.log(`[Cleanup Job] Found ${day3Reminders.length} Day 3 payment failure reminders`); + + for (const sub of day3Reminders) { + try { + await sendEmail(sub.email, 'payment_failure_reminder_day3', { + tier: sub.tier, + days_remaining: 4, + update_url: 'https://billing.firefrostgaming.com/payment-methods' + }); + console.log(`[Cleanup Job] Sent Day 3 reminder: ${sub.email}`); + } catch (error) { + console.error(`[Cleanup Job] Error sending Day 3 reminder to ${sub.email}:`, error); + } + } + + // 4. Send payment failure final warnings (Day 6 - 24 hours left) + const finalWarnings = await db.all(` + SELECT s.*, gp.id as grace_id + FROM subscriptions s + JOIN grace_periods gp ON s.id = gp.subscription_id + WHERE gp.type = 'payment_failure' + AND gp.ends_at BETWEEN datetime('now', '+1 day') AND datetime('now', '+25 hours') + AND gp.resolved = 0 + `); + + console.log(`[Cleanup Job] Found ${finalWarnings.length} final payment warnings`); + + for (const sub of finalWarnings) { + try { + await sendEmail(sub.email, 'payment_failure_final_warning', { + tier: sub.tier, + hours_remaining: 24, + update_url: 'https://billing.firefrostgaming.com/payment-methods' + }); + console.log(`[Cleanup Job] Sent final warning: ${sub.email}`); + } catch (error) { + console.error(`[Cleanup Job] Error sending final warning to ${sub.email}:`, error); + } + } + + // 5. Convert expired payment failures to cancellations + const expiredPaymentFailures = await db.all(` + SELECT s.*, gp.id as grace_id + FROM subscriptions s + JOIN grace_periods gp ON s.id = gp.subscription_id + WHERE gp.type = 'payment_failure' + AND gp.ends_at < datetime('now') + AND gp.resolved = 0 + `); + + console.log(`[Cleanup Job] Found ${expiredPaymentFailures.length} expired payment failures`); + + for (const sub of expiredPaymentFailures) { + try { + // Treat as cancellation + await db.run(` + UPDATE subscriptions + SET status = 'cancelled', + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `, [sub.id]); + + // Mark grace period resolved + await db.run('UPDATE grace_periods SET resolved = 1, resolved_at = CURRENT_TIMESTAMP WHERE id = ?', + [sub.grace_id]); + + // Create Discord role removal grace (until billing end) and whitelist grace (30 days) + if (sub.billing_period_end) { + await db.run(` + INSERT INTO grace_periods (subscription_id, type, started_at, ends_at) + VALUES (?, 'discord_removal', CURRENT_TIMESTAMP, ?) + `, [sub.id, sub.billing_period_end]); + + const whitelistGraceEnd = new Date(sub.billing_period_end); + whitelistGraceEnd.setDate(whitelistGraceEnd.getDate() + 30); + + await db.run(` + INSERT INTO grace_periods (subscription_id, type, started_at, ends_at) + VALUES (?, 'whitelist_removal', CURRENT_TIMESTAMP, ?) + `, [sub.id, whitelistGraceEnd.toISOString()]); + } + + // Send access removed email + await sendEmail(sub.email, 'access_removed_payment_failure', { + tier: sub.tier, + resubscribe_url: 'https://firefrostgaming.com/join' + }); + + await logAudit('system', sub.email, 'payment_failure_converted_to_cancellation', + `Tier: ${sub.tier}, 7-day grace expired`); + + console.log(`[Cleanup Job] Converted payment failure to cancellation: ${sub.email}`); + } catch (error) { + console.error(`[Cleanup Job] Error converting payment failure for ${sub.email}:`, error); + } + } + + console.log('[Cleanup Job] Subscription cleanup complete.'); + } catch (error) { + console.error('[Cleanup Job] Fatal error:', error); + } +}); + +console.log('[Cleanup Job] Scheduled daily cleanup at 4:00 AM'); +``` + +### 4. Whitelist Manager API Integration + +**NEW FILE: services/whitelistService.js** + +```javascript +const axios = require('axios'); + +const WHITELIST_MANAGER_URL = process.env.WHITELIST_MANAGER_URL || 'https://whitelist.firefrostgaming.com'; +const WHITELIST_API_KEY = process.env.WHITELIST_API_KEY; + +async function removeFromWhitelist(emailOrUsername) { + try { + const response = await axios.post( + `${WHITELIST_MANAGER_URL}/api/bulk-remove`, + { + player: emailOrUsername + }, + { + headers: { + 'Authorization': `Bearer ${WHITELIST_API_KEY}`, + 'Content-Type': 'application/json' + } + } + ); + + return response.data; + } catch (error) { + console.error('Whitelist removal error:', error); + throw error; + } +} + +module.exports = { + removeFromWhitelist +}; +``` + +**NOTE:** This requires Whitelist Manager to expose an API endpoint for Arbiter to call. May need to add `/api/bulk-remove` endpoint to Whitelist Manager. + +### 5. Email Templates + +**NEW FILES: emails/** + +#### subscription_cancelled.html +```html + + + + + Subscription Cancelled + + +

🔥 Subscription Cancelled

+ +

Hi there,

+ +

We've received your cancellation request for your {{tier}} subscription.

+ +

What Happens Next:

+ + +

We're sorry to see you go! If you change your mind, you can re-subscribe anytime at firefrostgaming.com/join.

+ +

+ Fire + Frost + Foundation
+ The Firefrost Gaming Team 💙 +

+ + +``` + +#### payment_failed.html +```html + + + + + Payment Failed + + +

⚠️ Payment Failed

+ +

Hi there,

+ +

We weren't able to process your payment for your {{tier}} subscription.

+ +

Reason: {{reason}}

+

Amount: ${{amount}}

+ +

What You Need to Do:

+

Please update your payment method within {{grace_days}} days to keep your access.

+ +

+ Update Payment Method +

+ +

What Happens If You Don't Update:

+ + +

If you have any questions, join us in Discord: firefrostgaming.com/discord

+ +

+ Fire + Frost + Foundation
+ The Firefrost Gaming Team 💙 +

+ + +``` + +#### payment_failure_reminder_day3.html +```html + + + + + Payment Failure Reminder + + +

⏰ Reminder: Payment Still Pending

+ +

Hi there,

+ +

This is a friendly reminder that your payment for {{tier}} is still pending.

+ +

You have {{days_remaining}} days left to update your payment method.

+ +

+ Update Payment Method +

+ +

Need help? Join us in Discord: firefrostgaming.com/discord

+ +

+ Fire + Frost + Foundation
+ The Firefrost Gaming Team 💙 +

+ + +``` + +#### payment_failure_final_warning.html +```html + + + + + Final Warning - Payment Required + + +

🚨 Final Warning: {{hours_remaining}} Hours Left

+ +

Hi there,

+ +

This is your final warning. Your payment for {{tier}} is still pending.

+ +

+ You have {{hours_remaining}} hours to update your payment method or your subscription will be cancelled. +

+ +

+ Update Payment Method Now +

+ +

If you don't update your payment:

+ + +

Need help urgently? Join Discord: firefrostgaming.com/discord

+ +

+ Fire + Frost + Foundation
+ The Firefrost Gaming Team 💙 +

+ + +``` + +#### access_removed_payment_failure.html +```html + + + + + Access Removed + + +

Access Removed

+ +

Hi there,

+ +

Your {{tier}} subscription has been cancelled due to non-payment.

+ +

What's Changed:

+ + +

Want to Come Back?

+

We'd love to have you back! You can re-subscribe anytime:

+ +

+ Re-Subscribe +

+ +

Questions? Join us in Discord: firefrostgaming.com/discord

+ +

+ Fire + Frost + Foundation
+ The Firefrost Gaming Team 💙 +

+ + +``` + +#### account_suspended_chargeback.html +```html + + + + + Account Suspended + + +

🚨 Account Suspended - Chargeback Detected

+ +

Hi there,

+ +

A chargeback was filed for your {{tier}} subscription (Amount: ${{amount}}).

+ +

Immediate Actions Taken:

+ + +

What You Should Know:

+

Chargebacks are costly and disrupt our small community. If this was filed in error or there was a billing issue, please contact us directly:

+ +

Email: {{contact_email}}
+ Discord: firefrostgaming.com/discord

+ +

We're here to resolve any legitimate billing concerns, but chargebacks require manual review before account access can be restored.

+ +

+ Fire + Frost + Foundation
+ The Firefrost Gaming Team +

+ + +``` + +--- + +## Paymenter Webhook Events Research + +**CRITICAL: Verify what events Paymenter actually sends** + +**Research needed (when home):** +1. Log into Paymenter admin panel +2. Navigate to webhook settings +3. Document available webhook events +4. Test each event by triggering it (test subscription) +5. Log the exact payload format + +**Expected events (to verify):** +- `subscription.created` ✅ (already handling) +- `subscription.cancelled` ❓ +- `subscription.expired` ❓ +- `subscription.renewed` ❓ +- `payment.failed` ❓ +- `payment.succeeded` ❓ +- `chargeback.received` ❓ + +**If Paymenter doesn't send an event we need:** +- Option A: Request feature from Paymenter developers +- Option B: Build polling system (check subscription status every hour) +- Option C: Manual admin actions via Arbiter admin panel + +--- + +## Testing Procedure + +### 1. Unit Testing (Handlers) + +**Test each handler in isolation:** + +```javascript +// Test cancellation handler +const testCancellation = { + email: 'test@example.com', + subscription_id: 'sub_test123', + tier: 'Elemental', + billing_period_end: '2026-04-30T23:59:59Z' +}; + +await handleSubscriptionCancelled(testCancellation); + +// Verify database state +const sub = await db.get('SELECT * FROM subscriptions WHERE email = ?', + ['test@example.com']); +assert(sub.status === 'cancelled'); + +// Verify grace periods created +const gracePeriods = await db.all( + 'SELECT * FROM grace_periods WHERE subscription_id = ?', + [sub.id] +); +assert(gracePeriods.length === 2); // Discord + Whitelist +``` + +### 2. Integration Testing (Full Flow) + +**Test complete cancellation flow:** + +1. Create test subscription in Paymenter +2. Verify Arbiter 2.0 onboarding works +3. Cancel subscription in Paymenter +4. Verify webhook received by Arbiter +5. Verify database updated correctly +6. Verify email sent +7. Verify grace periods created +8. Fast-forward system time (testing only) +9. Run cleanup job manually +10. Verify Discord role removed +11. Verify whitelist removed + +### 3. Edge Case Testing + +**Test failure scenarios:** + +- Webhook signature invalid +- Database write fails +- Discord API down +- Email service down +- Whitelist Manager API down +- Multiple rapid cancellations (race conditions) +- Cancel before OAuth linking completes +- Re-subscribe during grace period + +--- + +## Deployment Strategy + +### Phase 1: Arbiter 2.0 Deployment (Current) +- Deploy existing onboarding code +- Validate with test subscriptions +- Confirm OAuth flow works +- Verify Discord role assignment + +### Phase 2: Arbiter 2.1 Development (This Task) +- Add database tables +- Implement new handlers +- Create email templates +- Build cleanup job +- Unit test all handlers + +### Phase 3: Arbiter 2.1 Staging Test +- Deploy to test environment +- Create test subscriptions +- Test cancellation flows +- Verify grace periods +- Test cleanup job + +### Phase 4: Arbiter 2.1 Production Deployment +- Deploy to Command Center +- Monitor logs carefully +- Test with real subscription (Michael's test account) +- Verify all flows work +- Document any issues + +--- + +## Dependencies + +**Blocks:** +- Soft launch (cannot launch without cancellation flow) + +**Blocked By:** +- Arbiter 2.0 deployment (must validate Phase 1 first) +- Paymenter webhook event research (need to know what events exist) +- Whitelist Manager API endpoint (needs `/api/bulk-remove`) + +**Related Tasks:** +- Task #83: Paymenter → Pterodactyl integration (auto-provisioning) +- Task #7: Whitelist Manager (needs API enhancement) +- Task #86: Whitelist Manager Panel compatibility (should fix first) +- Task #2: LuckPerms rank system (Discord → in-game sync) + +--- + +## Success Criteria + +**Arbiter 2.1 is complete when:** + +- ✅ All 6 webhook events handled (cancelled, expired, failed, renewed, chargeback, succeeded) +- ✅ Database tables created and indexed +- ✅ Grace period logic working (7-day payment failure, 30-day whitelist) +- ✅ Discord role removal automated +- ✅ Whitelist removal automated (via API call) +- ✅ 5 email templates created and sending correctly +- ✅ Scheduled cleanup job running daily at 4 AM +- ✅ Payment failure reminders sending (Day 3, Day 6) +- ✅ Chargeback immediate removal working +- ✅ Admin panel shows subscription status +- ✅ Audit logging for all state changes +- ✅ Complete test flow successful (subscribe → cancel → grace → cleanup) + +--- + +## Future Enhancements (Arbiter 2.2+) + +**Not in scope for 2.1, but nice to have later:** + +- Re-subscription detection (welcome back bonus?) +- Subscription pause/resume feature +- Downgrade/upgrade handling (tier changes) +- Family/group subscriptions +- Referral tracking (who invited who?) +- Lifetime subscription support +- Gift subscriptions +- Subscription analytics dashboard +- Churn prediction (at-risk subscriber detection) +- Win-back campaigns (automated re-engagement emails) + +--- + +## Related Documentation + +- **Arbiter 2.0:** `docs/implementation/discord-oauth-arbiter/` +- **Gemini Consultation:** `docs/consultations/gemini-discord-oauth-2026-03-30/` +- **Task Master List:** `docs/core/tasks.md` +- **Infrastructure Manifest:** `docs/core/infrastructure-manifest.md` + +--- + +## Architecture Review Request + +**CRITICAL: Before building, get Gemini's review** + +**Questions for Gemini:** +1. Is the grace period architecture sound? +2. Are the database tables properly designed? +3. Should we use separate cleanup job or integrate into webhook handlers? +4. Is the chargeback handling appropriate? +5. Any edge cases we're missing? +6. Security concerns with automated role/whitelist removal? +7. Better approach to Whitelist Manager integration? +8. Should grace periods be configurable (admin panel)? + +**After Gemini review:** +- Incorporate feedback +- Update this document +- Begin implementation + +--- + +**Fire + Frost + Foundation = Where Love Builds Legacy** 💙🔥❄️ + +--- + +**Document Status:** ACTIVE - AWAITING GEMINI REVIEW +**Task Status:** IDENTIFIED - Ready for architecture review +**Ready to Build:** After Gemini consultation