← 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;