← back to AbramsOS
routes/assets.js
108 lines
// Assets — net-worth tracker: home (by address) + vehicles/accounts with current value,
// finance category, details + notes. Values are user-entered. Read + curate.
const express = require('express');
const db = require('../lib/db');
const audit = require('../lib/audit');
const { id } = require('../lib/ids');
const homeValue = require('../lib/home-value');
const router = express.Router();
const DEV_USER_ID = 'user_steve';
async function listAssets(userId) {
return (await db.query(
`SELECT id, kind, name, address, current_value, value_source, category, details, notes,
valued_at, status, latitude, longitude, created_at
FROM asset WHERE user_id = $1 AND status = 'active'
ORDER BY current_value DESC NULLS LAST, created_at DESC`,
[userId]
)).rows;
}
router.get('/assets', async (_req, res) => {
const assets = await listAssets(DEV_USER_ID);
const netWorth = assets.reduce((s, a) => s + (Number(a.current_value) || 0), 0);
const byCategory = {};
for (const a of assets) {
const c = a.category || 'uncategorized';
byCategory[c] = (byCategory[c] || 0) + (Number(a.current_value) || 0);
}
res.render('assets', { assets, netWorth, byCategory, valuationConfigured: homeValue.isConfigured() });
});
// Estimate a property's value from its address via RentCast (if a key is set).
router.post('/api/assets/:id/estimate', async (req, res) => {
const a = await db.query(`SELECT id, address, kind FROM asset WHERE id = $1 AND user_id = $2`, [req.params.id, DEV_USER_ID]);
if (!a.rows.length) return res.status(404).json({ ok: false, error: 'not found' });
if (!a.rows[0].address) return res.status(400).json({ ok: false, error: 'asset has no address' });
const est = await homeValue.estimateValue(a.rows[0].address);
if (!est.ok) return res.status(est.reason === 'no_key' ? 501 : 502).json({ ok: false, reason: est.reason });
await db.query(
`UPDATE asset SET current_value = $1, value_source = 'estimate', valued_at = current_date,
metadata_jsonb = metadata_jsonb || $2::jsonb, updated_at = now()
WHERE id = $3 AND user_id = $4`,
[est.price, JSON.stringify({ estimate: { source: 'rentcast', low: est.low, high: est.high, at: new Date().toISOString() } }), req.params.id, DEV_USER_ID]
);
await audit.log({ actorType: 'system', actorId: DEV_USER_ID, objectType: 'asset', objectId: req.params.id, eventType: 'asset_estimated', metadata: { source: 'rentcast', price: est.price } }).catch(() => {});
res.json({ ok: true, ...est });
});
router.get('/api/assets', async (_req, res) => {
res.json(await listAssets(DEV_USER_ID));
});
function clean(b) {
const num = (v) => (v === '' || v == null ? null : (Number.isFinite(Number(v)) ? Number(v) : null));
return {
kind: (b.kind || 'property').slice(0, 40),
name: (b.name || '').slice(0, 200),
address: b.address ? String(b.address).slice(0, 400) : null,
current_value: num(b.current_value),
category: b.category ? String(b.category).slice(0, 80) : null,
details: b.details ? String(b.details).slice(0, 2000) : null,
notes: b.notes ? String(b.notes).slice(0, 2000) : null,
valued_at: b.valued_at || null,
latitude: num(b.latitude),
longitude: num(b.longitude),
};
}
router.post('/api/assets', async (req, res) => {
const a = clean(req.body || {});
if (!a.name) return res.status(400).json({ error: 'name required' });
const aid = id('asset');
await db.query(
`INSERT INTO asset (id, user_id, kind, name, address, current_value, value_source, category, details, notes, valued_at, latitude, longitude)
VALUES ($1,$2,$3,$4,$5,$6,'manual',$7,$8,$9,$10,$11,$12)`,
[aid, DEV_USER_ID, a.kind, a.name, a.address, a.current_value, a.category, a.details, a.notes, a.valued_at, a.latitude, a.longitude]
);
await audit.log({ actorType: 'user', actorId: DEV_USER_ID, objectType: 'asset', objectId: aid, eventType: 'asset_created', metadata: { kind: a.kind, category: a.category } }).catch(() => {});
const r = await db.query(`SELECT * FROM asset WHERE id = $1`, [aid]);
res.json({ ok: true, asset: r.rows[0] });
});
router.post('/api/assets/:id', async (req, res) => {
const a = clean(req.body || {});
const sets = [], vals = [];
let i = 1;
for (const [k, v] of Object.entries(a)) {
if (v !== undefined) { sets.push(`${k} = $${i++}`); vals.push(v); }
}
if (!sets.length) return res.status(400).json({ error: 'nothing to update' });
sets.push(`updated_at = now()`);
vals.push(req.params.id, DEV_USER_ID);
const r = await db.query(
`UPDATE asset SET ${sets.join(', ')} WHERE id = $${i++} AND user_id = $${i} RETURNING *`, vals
);
if (!r.rows.length) return res.status(404).json({ error: 'not found' });
await audit.log({ actorType: 'user', actorId: DEV_USER_ID, objectType: 'asset', objectId: req.params.id, eventType: 'asset_updated', metadata: {} }).catch(() => {});
res.json({ ok: true, asset: r.rows[0] });
});
router.post('/api/assets/:id/delete', async (req, res) => {
await db.query(`UPDATE asset SET status = 'archived', updated_at = now() WHERE id = $1 AND user_id = $2`, [req.params.id, DEV_USER_ID]);
res.json({ ok: true });
});
module.exports = router;