← back to AbramsOS

routes/savings.js

88 lines

// Savings — the life-optimizer dashboard: cheaper/better substitutes for what Steve
// buys, plus coupon leads at the merchants he shops. Read + curate; never auto-buys.
const express = require('express');
const db = require('../lib/db');
const advisor = require('../lib/savings-advisor');
const billAudit = require('../lib/bill-audit');
const reorderTiming = require('../lib/reorder-timing');
const audit = require('../lib/audit');

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

async function listSuggestions(userId) {
  const r = await db.query(
    `SELECT id, kind, title, current_item, current_price, suggested_item, suggested_price,
            est_savings, savings_basis, quality_note, merchant, source_url, rationale,
            confidence, status, created_at
       FROM savings_suggestion
      WHERE user_id = $1 AND status IN ('new','saved')
      ORDER BY (status='saved') DESC, est_savings DESC NULLS LAST, confidence DESC
      LIMIT 200`,
    [userId]
  );
  return r.rows;
}

async function listCoupons(userId) {
  const r = await db.query(
    `SELECT id, merchant, merchant_domain, title, code, discount_text, description,
            url, expires_on, source, created_at
       FROM merchant_coupon
      WHERE user_id = $1 AND status = 'active'
        AND (expires_on IS NULL OR expires_on >= current_date)
      ORDER BY created_at DESC LIMIT 200`,
    [userId]
  );
  return r.rows;
}

router.get('/savings', async (_req, res) => {
  const [suggestions, coupons] = await Promise.all([
    listSuggestions(DEV_USER_ID),
    listCoupons(DEV_USER_ID),
  ]);
  const totalSavings = suggestions.reduce((s, x) => s + (Number(x.est_savings) || 0), 0);
  res.render('savings', { suggestions, coupons, totalSavings });
});

router.get('/api/savings', async (_req, res) => {
  res.json(await listSuggestions(DEV_USER_ID));
});

router.get('/api/coupons', async (_req, res) => {
  res.json(await listCoupons(DEV_USER_ID));
});

// Generate fresh substitute suggestions from reorder_item data (local LLM, $0).
router.post('/api/savings/run', async (_req, res) => {
  try {
    const out = await advisor.generateSuggestions(DEV_USER_ID, { limit: 50 });
    const bills = await billAudit.generateBillAudit(DEV_USER_ID, { model: 'gemma3:12b' });
    const reorders = await reorderTiming.generateReorderNudges(DEV_USER_ID);
    const created = out.created + bills.created + reorders.created;
    await audit.log({
      actorType: 'system', actorId: 'savings-advisor', objectType: 'savings_suggestion',
      objectId: null, eventType: 'savings_generated',
      metadata: { substitutes: out.created, bill_audit: bills.created, reorder_timing: reorders.created },
    }).catch(() => {});
    res.json({ ok: true, created, substitutes: out.created, billAudit: bills.created, reorderTiming: reorders.created });
  } catch (err) {
    res.status(500).json({ ok: false, error: err.message });
  }
});

router.post('/api/savings/:id/status', async (req, res) => {
  const status = ['new', 'saved', 'dismissed', 'done'].includes(req.body.status) ? req.body.status : null;
  if (!status) return res.status(400).json({ error: 'bad status' });
  const r = await db.query(
    `UPDATE savings_suggestion SET status = $1, updated_at = now()
      WHERE id = $2 AND user_id = $3 RETURNING id, status`,
    [status, req.params.id, DEV_USER_ID]
  );
  if (!r.rows.length) return res.status(404).json({ error: 'not found' });
  res.json(r.rows[0]);
});

module.exports = router;