diff --git a/docs/consultations/gemini-arbiter-lifecycle-code-request-2026-04-04.md b/docs/consultations/gemini-arbiter-lifecycle-code-request-2026-04-04.md new file mode 100644 index 0000000..fdea854 --- /dev/null +++ b/docs/consultations/gemini-arbiter-lifecycle-code-request-2026-04-04.md @@ -0,0 +1,197 @@ +# 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** 💙🔥❄️