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