# 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** 💙🔥❄️
