Files
firefrost-operations-manual/docs/consultations/gemini-arbiter-lifecycle-code-request-2026-04-04.md
Claude (Chronicler #59) 3b792cf97e docs: Prepare Arbiter lifecycle code request for Gemini
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
2026-04-04 03:56:54 +00:00

5.5 KiB

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:

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

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