Complete context package for when we're ready to implement: - Current webhook handler code (checkout.session.completed only) - Database schema (what exists vs what's needed) - Missing handlers list (payment_failed, cancelled, chargeback) - Grace period sweeper requirements - Reference to March 30 code blocks This is prep for future session, not immediate work. Signed-off-by: claude@firefrostgaming.com
198 lines
5.5 KiB
Markdown
198 lines
5.5 KiB
Markdown
# Gemini: Arbiter Lifecycle Handlers - Code Review Request
|
|
|
|
**Date:** April 4, 2026, 11:20 PM CDT
|
|
**From:** Michael (The Wizard) + Claude (Chronicler #59)
|
|
**To:** Gemini (Architectural Partner)
|
|
**Re:** Code needed for subscription lifecycle (cancellation, grace period, chargebacks)
|
|
|
|
---
|
|
|
|
## Current Arbiter State
|
|
|
|
**Version:** 3.5.0
|
|
**Location:** Command Center (63.143.34.217), `/opt/arbiter-3.0/`
|
|
**Systemd Service:** `arbiter-3`
|
|
|
|
---
|
|
|
|
## What IS Implemented
|
|
|
|
### Stripe Webhook Handler (current)
|
|
|
|
From `/opt/arbiter-3.0/src/routes/stripe.js`:
|
|
|
|
**Only handles `checkout.session.completed`:**
|
|
|
|
```javascript
|
|
case 'checkout.session.completed': {
|
|
const session = event.data.object;
|
|
|
|
// Extract Discord ID from client_reference_id
|
|
const discordId = session.client_reference_id;
|
|
const customerEmail = session.customer_details.email;
|
|
const tierLevel = parseInt(session.metadata.tier_level);
|
|
const tierName = session.metadata.tier_name;
|
|
|
|
if (!discordId) {
|
|
console.error('❌ No Discord ID in checkout session:', session.id);
|
|
return res.status(400).send('Missing Discord ID');
|
|
}
|
|
|
|
console.log(`✅ Payment complete: ${session.metadata.discord_username} (${discordId}) - ${tierName}`);
|
|
|
|
// Determine subscription ID based on billing type
|
|
let stripeSubscriptionId = null;
|
|
let status = 'lifetime';
|
|
|
|
if (session.mode === 'subscription') {
|
|
stripeSubscriptionId = session.subscription;
|
|
status = 'active';
|
|
}
|
|
|
|
// Insert/update subscription with Discord ID
|
|
await pool.query(`
|
|
INSERT INTO subscriptions (
|
|
stripe_subscription_id,
|
|
stripe_customer_id,
|
|
discord_id,
|
|
tier_level,
|
|
status,
|
|
created_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, NOW())
|
|
ON CONFLICT (discord_id)
|
|
DO UPDATE SET
|
|
tier_level = EXCLUDED.tier_level,
|
|
status = EXCLUDED.status,
|
|
stripe_subscription_id = EXCLUDED.stripe_subscription_id,
|
|
stripe_customer_id = EXCLUDED.stripe_customer_id,
|
|
updated_at = NOW()
|
|
`, [stripeSubscriptionId, session.customer, discordId, tierLevel, status]);
|
|
|
|
console.log(`✅ Database updated: Discord ${discordId} → Tier ${tierLevel} (${status})`);
|
|
|
|
// TODO: Trigger Discord role assignment via Arbiter
|
|
|
|
break;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## What is NOT Implemented (Needed)
|
|
|
|
Per your March 30, 2026 architectural review (Task #87):
|
|
|
|
### 1. Database Schema Updates
|
|
|
|
Need columns:
|
|
- `permanent_tier` (TEXT) — Awakened or Sovereign (never expires)
|
|
- `monthly_tier` (TEXT) — Elemental/Knight/Master/Legend (can be cancelled)
|
|
- `grace_period_start` (DATETIME) — When payment failed
|
|
- `is_banned` (INTEGER) — Chargeback permanent ban flag
|
|
|
|
### 2. Webhook Handlers Needed
|
|
|
|
| Stripe Event | Action |
|
|
|--------------|--------|
|
|
| `invoice.payment_failed` | Start 3-day grace period |
|
|
| `invoice.payment_succeeded` | Clear grace period if late payment succeeds (Stripe Smart Retries fix) |
|
|
| `customer.subscription.deleted` | Handle cancellation (downgrade to Awakened) |
|
|
| `charge.dispute.created` | Immediate permanent ban |
|
|
|
|
### 3. Grace Period Sweeper Job (4 AM Cron)
|
|
|
|
Check for subscriptions where:
|
|
- `status = 'grace_period'`
|
|
- `grace_period_ends_at < NOW()`
|
|
|
|
Action: Downgrade to permanent Awakened tier, remove monthly Discord role, keep in community.
|
|
|
|
### 4. "We Don't Kick People Out" Philosophy
|
|
|
|
- Payment failure → 3-day grace period → downgrade to Awakened (never remove)
|
|
- Cancellation → downgrade to Awakened (never remove)
|
|
- Chargeback → immediate permanent ban (only exception)
|
|
|
|
---
|
|
|
|
## Current Database Schema
|
|
|
|
```sql
|
|
CREATE TABLE subscriptions (
|
|
id SERIAL PRIMARY KEY,
|
|
stripe_subscription_id VARCHAR(255),
|
|
stripe_customer_id VARCHAR(255),
|
|
discord_id VARCHAR(255),
|
|
tier_level INTEGER,
|
|
status VARCHAR(50),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- These columns DO NOT exist yet:
|
|
-- permanent_tier TEXT DEFAULT 'awakened'
|
|
-- monthly_tier TEXT
|
|
-- grace_period_start DATETIME
|
|
-- is_banned INTEGER DEFAULT 0
|
|
```
|
|
|
|
---
|
|
|
|
## Environment
|
|
|
|
- **Database:** PostgreSQL (`arbiter_db` on Command Center)
|
|
- **Stripe Webhook Secret:** Configured in `.env` as `STRIPE_WEBHOOK_SECRET`
|
|
- **Discord Bot Token:** Available in `.env`
|
|
- **discordService.js:** Has `assignDiscordRole()` and `removeDiscordRole()` functions
|
|
|
|
---
|
|
|
|
## What We Need From You, Gemini
|
|
|
|
When the time comes to implement this, we need:
|
|
|
|
1. **Database migration SQL** — Add the new columns
|
|
2. **Updated webhook handler** — Handle all lifecycle events
|
|
3. **Grace period sweeper job** — Node.js script for 4 AM cron
|
|
4. **Tier resolver helper** — Returns highest tier between permanent and monthly
|
|
5. **Email templates** — Day 0, Day 1, Day 2, Day 3 grace period messages
|
|
|
|
---
|
|
|
|
## Your Previous Code Blocks (March 30, 2026)
|
|
|
|
You provided ready-to-implement code blocks for:
|
|
- Database schema updates
|
|
- Tier hierarchy resolver
|
|
- UPSERT logic (double-buy protection)
|
|
- Ban check middleware
|
|
- Grace period sweeper
|
|
|
|
These are documented in `docs/tasks/arbiter-2-1-cancellation-flow/README.md`.
|
|
|
|
---
|
|
|
|
## Priority
|
|
|
|
This is **HIGH PRIORITY** for subscriber lifecycle but not a launch blocker.
|
|
|
|
We're live now (April 3, 2026) and the happy path works. But we need this before:
|
|
- First cancellation
|
|
- First payment failure
|
|
- First chargeback (hopefully never)
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
**Please save this context.** When we're ready to implement, we'll come back and ask you to write the complete code. For now, just confirming you have what you need to pick this up later.
|
|
|
|
Thanks Gemini! 🔥❄️
|
|
|
|
— Michael + Claude (Chronicler #59)
|
|
|
|
---
|
|
|
|
**Fire + Frost + Foundation = Where Love Builds Legacy** 💙🔥❄️
|