← back to AbramsOS

routes/claims.js

148 lines

const express = require('express');
const db = require('../lib/db');
const audit = require('../lib/audit');
const strategist = require('../lib/claim-strategist');

const router = express.Router();
const DEV_USER_ID = 'user_steve';

// HTML — list view
router.get('/claims', async (_req, res) => {
  const r = await db.query(
    `SELECT id, claim_type, routing, state, draft_subject, due_at, created_at, asset_table, asset_id
       FROM claim_case WHERE user_id = $1 ORDER BY created_at DESC LIMIT 200`,
    [DEV_USER_ID]
  );
  // Household names to suggest against the CA unclaimed-property search.
  // Defensive: never let a missing/renamed table break the claims page.
  let household = [];
  try {
    const p = await db.query(
      `SELECT full_name FROM person WHERE user_id = $1 AND full_name IS NOT NULL ORDER BY (relation <> 'self'), full_name`,
      [DEV_USER_ID]
    );
    household = p.rows.map(x => x.full_name).filter(Boolean);
  } catch (_e) { household = []; }
  // Unclaimed property — defensive: a missing/renamed table must never break the page.
  let properties = [];
  let ucpTotals = { count: 0, staged: 0, filed: 0, paid: 0, found: 0, totalCents: 0, totalDisplay: '$0.00' };
  try {
    const up = await db.query(
      `SELECT * FROM unclaimed_property WHERE user_id = $1 ORDER BY (amount_cents IS NULL), amount_cents DESC`,
      [DEV_USER_ID]
    );
    properties = up.rows;
    const totalCents = properties.reduce((sum, p) => sum + (p.amount_cents || 0), 0);
    ucpTotals = {
      count: properties.length,
      staged: properties.filter(p => p.status === 'staged').length,
      filed: properties.filter(p => p.status === 'filed').length,
      paid: properties.filter(p => p.status === 'paid').length,
      found: properties.filter(p => p.status === 'found').length,
      totalCents,
      totalDisplay: '$' + (totalCents / 100).toFixed(2),
    };
  } catch (_e) {
    properties = [];
    ucpTotals = { count: 0, staged: 0, filed: 0, paid: 0, found: 0, totalCents: 0, totalDisplay: '$0.00' };
  }
  res.render('claims', { claims: r.rows, household, properties, ucpTotals });
});

// HTML — detail view
router.get('/claims/:id', async (req, res) => {
  const c = await db.query(`SELECT * FROM claim_case WHERE id = $1 AND user_id = $2`, [req.params.id, DEV_USER_ID]);
  if (!c.rows.length) return res.status(404).render('error', { error: 'claim not found' });
  const a = await db.query(`SELECT * FROM action_queue WHERE case_id = $1 ORDER BY created_at`, [req.params.id]);
  res.render('claim-detail', { claim: c.rows[0], actions: a.rows });
});

router.get('/api/claims', async (_req, res) => {
  const r = await db.query(
    `SELECT id, claim_type, routing, state, draft_subject, due_at, created_at, asset_table, asset_id
       FROM claim_case WHERE user_id = $1 ORDER BY created_at DESC LIMIT 200`,
    [DEV_USER_ID]
  );
  res.json(r.rows);
});

router.get('/api/claims/:id', async (req, res) => {
  const r = await db.query(`SELECT * FROM claim_case WHERE id = $1 AND user_id = $2`, [req.params.id, DEV_USER_ID]);
  if (!r.rows.length) return res.status(404).json({ error: 'claim not found' });
  const actions = await db.query(`SELECT * FROM action_queue WHERE case_id = $1 ORDER BY created_at`, [req.params.id]);
  res.json({ ...r.rows[0], actions: actions.rows });
});

router.post('/api/claims/from-reminder/:id', async (req, res) => {
  try {
    const { caseId, claim } = await strategist.fromReminder(req.params.id, DEV_USER_ID);
    res.json({ ok: true, case_id: caseId, claim });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Approve an action — but DON'T execute. Steve still has to physically click "send" elsewhere.
// This just flips the gate; sending happens in a future tick when we wire George email.
router.post('/api/claims/:caseId/actions/:actionId/approve', async (req, res) => {
  const { caseId, actionId } = req.params;
  await db.query(
    `UPDATE action_queue SET state = 'approved' WHERE id = $1 AND case_id = $2 AND state = 'pending'`,
    [actionId, caseId]
  );
  await audit.log({
    actorType: 'user',
    actorId: DEV_USER_ID,
    objectType: 'action_queue',
    objectId: actionId,
    eventType: 'action_approved',
    metadata: { case_id: caseId },
  });
  res.json({ ok: true });
});

// JSON — unclaimed property list
router.get('/api/unclaimed', async (req, res) => {
  try {
    const userId = req.userId || DEV_USER_ID;
    const r = await db.query(
      `SELECT * FROM unclaimed_property WHERE user_id = $1 ORDER BY (amount_cents IS NULL), amount_cents DESC`,
      [userId]
    );
    res.json(r.rows);
  } catch (err) {
    res.status(500).json({ error: 'db error' });
  }
});

// Update an unclaimed-property record's status (and optionally link a claim_id).
router.post('/api/unclaimed/:id/status', async (req, res) => {
  try {
    const userId = req.userId || DEV_USER_ID;   // authorize the write against the session user, not a constant
    const { id } = req.params;
    const { status, claim_id } = req.body || {};
    const allowed = ['found', 'staged', 'filed', 'paid', 'not_mine'];
    if (!allowed.includes(status)) {
      return res.status(400).json({ error: 'invalid status' });
    }
    await db.query(
      `UPDATE unclaimed_property SET status = $1, claim_id = COALESCE($2, claim_id), updated_at = now()
         WHERE id = $3 AND user_id = $4`,
      [status, claim_id || null, id, userId]
    );
    await audit.log({
      actorType: 'user',
      actorId: userId,
      objectType: 'unclaimed_property',
      objectId: id,
      eventType: 'ucp_status_changed',
      metadata: { status, claim_id },
    });
    res.json({ ok: true });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

module.exports = router;