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