← back to AbramsOS

routes/settlements.js

159 lines

// routes/settlements.js — class-action settlement claim tracker (Mode Class Actions feed).
// Resilient: if the DB/table isn't available yet it falls back to the backfill JSON so the page
// still renders. Mutations (eligibility, fill) require the DB. Auth is enforced upstream (requireAuth).
const express = require('express');
const fs = require('fs');
const path = require('path');
const db = require('../lib/db');
const filler = require('../lib/openclaw-claim-filler');
const { extractAddress } = require('../lib/claims-autopilot');
const { rankClaims } = require('../lib/settlement-score');
const { runFill } = require('../lib/claim-openclaw-run');
const { findDocs } = require('../lib/claim-docs');

const router = express.Router();
const USER = 'user_steve';
const BACKFILL = path.join(__dirname, '..', 'data', 'mode-claims-backfill.json');

async function loadRows() {
  try {
    const r = await db.query(
      `SELECT * FROM settlement_claim WHERE user_id=$1
        ORDER BY (fill_state='expired'), (eligibility_state<>'eligible'),
                 (deadline IS NULL), deadline, payout_max_cents DESC NULLS LAST`, [USER]);
    return { source: 'db', rows: r.rows };
  } catch (_e) {
    // DB offline / pre-migration → show the parsed backfill so the page isn't empty.
    try {
      const j = JSON.parse(fs.readFileSync(BACKFILL, 'utf8'));
      return { source: 'backfill', rows: (j.settlements || []).map(s => ({ ...s, eligibility_state: 'unreviewed', fill_state: 'none', first_seen_at: j.generated_at })) };
    } catch (_e2) { return { source: 'none', rows: [] }; }
  }
}

router.get('/settlements', async (_req, res) => {
  const { source, rows } = await loadRows();
  // Compute per-item ratings + composite priority server-side, split open vs recently-expired,
  // rank the open set priority-first. The view renders ranks/tiers/bars off these fields.
  const { open, expired } = rankClaims(rows);
  res.render('settlements', { open, expired, source });
});

router.get('/api/settlements', async (_req, res) => {
  const { source, rows } = await loadRows();
  const { open, expired } = rankClaims(rows);
  res.json({ source, open, expired, count: open.length, expired_count: expired.length });
});

// Steve's eligibility decision — the ONLY thing that unlocks a fill.
router.post('/api/settlements/:id/eligibility', async (req, res) => {
  const state = String(req.body.state || '');
  if (!['unreviewed', 'eligible', 'not_eligible', 'maybe'].includes(state)) return res.status(400).json({ error: 'bad state' });
  try {
    await db.query(`UPDATE settlement_claim SET eligibility_state=$1,updated_at=now() WHERE id=$2 AND user_id=$3`, [state, req.params.id, USER]);
    res.json({ ok: true, state });
  } catch (e) { res.status(503).json({ error: 'db offline: ' + e.message }); }
});

// Stage an openclaw fill brief (fills, never submits). Only for eligible rows.
router.post('/api/settlements/:id/fill', async (req, res) => {
  try {
    const r = await db.query(`SELECT * FROM settlement_claim WHERE id=$1 AND user_id=$2`, [req.params.id, USER]);
    if (!r.rows.length) return res.status(404).json({ error: 'not found' });
    const claim = r.rows[0];
    if (claim.eligibility_state !== 'eligible') return res.status(409).json({ error: 'mark eligible first (never file where you are not a class member)' });
    const prof = await db.query(`SELECT full_name,email,phone,address FROM person WHERE user_id=$1 AND relation='self' LIMIT 1`, [USER]).then(x => x.rows[0] || {}).catch(() => ({}));
    const { local } = filler.stage(claim, prof);
    await db.query(`UPDATE settlement_claim SET fill_state='queued',updated_at=now() WHERE id=$1`, [claim.id]);
    res.json({ ok: true, staged: local, note: 'openclaw brief staged — it will prefill and STOP before submit for your review.' });
  } catch (e) { res.status(503).json({ error: 'db offline: ' + e.message }); }
});

// ---- profile (identity + payment) used to prefill every claim ----
// Payment default: PayPal -> steveabramsdesigns@gmail.com (Steve's choice 2026-08-18).
const PAYMENT_DEFAULT = { method: process.env.CLAIM_PAY_METHOD || 'PayPal', paypal_email: process.env.CLAIM_PAY_PAYPAL || 'steveabramsdesigns@gmail.com' };
async function loadProfile() {
  const r = await db.query(`SELECT full_name,email,phone,notes FROM person WHERE user_id=$1 AND relation='self' LIMIT 1`, [USER]).catch(() => ({ rows: [] }));
  const p = r.rows[0] || {};
  return {
    full_name: p.full_name || null,
    email: p.email || null,
    phone: p.phone || null,
    address: extractAddress(p.notes),
    payment: PAYMENT_DEFAULT,
  };
}
// The flat field->value map an autofiller (or the UI copy-card) uses for ANY administrator's form.
// Keys are the near-universal labels the research found across A.B.Data/Angeion/Epiq/JND/etc.
function fieldMap(profile) {
  const a = (profile.address || '').match(/^(.*?),\s*([^,]+),\s*([A-Z]{2})\s*(\d{5})/) || [];
  const parts = String(profile.full_name || '').trim().split(/\s+/);
  return {
    first_name: parts[0] || '', last_name: parts.slice(1).join(' ') || '',
    full_name: profile.full_name || '',
    street: a[1] || profile.address || '', city: a[2] || '', state: a[3] || '', zip: a[4] || '',
    email: profile.email || '', phone: (profile.phone || '').replace(/\D/g, ''),
    payment_method: profile.payment.method, paypal_email: profile.payment.paypal_email,
  };
}

// ONE-CLICK "Claim it & Go": mark eligible + stage the openclaw prefill brief, and return
// everything the UI needs to finish — the claim portal URL, the fill values, and the field map.
// Never submits. Works for every claim (that's Steve's requirement: all have a finish path).
router.post('/api/settlements/:id/claim-and-go', async (req, res) => {
  try {
    const r = await db.query(`SELECT * FROM settlement_claim WHERE id=$1 AND user_id=$2`, [req.params.id, USER]);
    if (!r.rows.length) return res.status(404).json({ error: 'not found' });
    const claim = r.rows[0];
    await db.query(`UPDATE settlement_claim SET eligibility_state='eligible',updated_at=now() WHERE id=$1`, [claim.id]);
    claim.eligibility_state = 'eligible';
    const profile = await loadProfile();
    let staged = null;
    try { staged = filler.stage(claim, profile).local; await db.query(`UPDATE settlement_claim SET fill_state='queued',updated_at=now() WHERE id=$1`, [claim.id]); } catch (_e) {}
    res.json({
      ok: true,
      mode_url: claim.mode_url || claim.admin_url || null,
      prefill: profile,
      fields: fieldMap(profile),
      staged,
      note: 'Prefill staged. Opening the claim portal — enter/verify the values below, then YOU check the perjury attestation and submit. Nothing was submitted for you.',
    });
  } catch (e) { res.status(503).json({ error: 'db offline: ' + e.message }); }
});

// LIVE auto-fill: open the claim form in the openclaw Chrome and type the identity+payment
// fields (zero-click for Steve), stopping before the perjury attestation. Never submits.
router.post('/api/settlements/:id/run', async (req, res) => {
  try {
    const r = await db.query(`SELECT * FROM settlement_claim WHERE id=$1 AND user_id=$2`, [req.params.id, USER]);
    if (!r.rows.length) return res.status(404).json({ error: 'not found' });
    const claim = r.rows[0];
    const url = claim.admin_url || claim.mode_url;
    if (!url) return res.status(400).json({ error: 'no claim URL on record' });
    await db.query(`UPDATE settlement_claim SET eligibility_state='eligible',updated_at=now() WHERE id=$1`, [claim.id]).catch(() => {});
    const profile = await loadProfile();
    const out = await runFill(claim.id, url, fieldMap(profile));
    if (out.ok) {
      await db.query(`UPDATE settlement_claim SET fill_state='prefilled_awaiting_submit',updated_at=now() WHERE id=$1`, [claim.id]).catch(() => {});
      // SAVE the real form URL we walked to, so the next run goes straight there.
      if (out.on && /^https?:/.test(out.on) && !/modeclassactionsdaily\.com/i.test(out.on) && out.on !== claim.admin_url) {
        await db.query(`UPDATE settlement_claim SET admin_url=$1,updated_at=now() WHERE id=$2`, [out.on, claim.id]).catch(() => {});
      }
    }
    res.json(out);
  } catch (e) { res.status(503).json({ error: e.message }); }
});

// Find the claim's SUPPORTING DOCS in Steve's email via George (home-sale/closing paperwork,
// receipts, breach notices). Read-only. Needs George config; no-ops cleanly without it.
router.get('/api/settlements/:id/docs', async (req, res) => {
  try {
    const r = await db.query(`SELECT * FROM settlement_claim WHERE id=$1 AND user_id=$2`, [req.params.id, USER]);
    if (!r.rows.length) return res.status(404).json({ error: 'not found' });
    const out = await findDocs(r.rows[0]);
    res.json(out);
  } catch (e) { res.status(503).json({ error: e.message }); }
});

module.exports = router;