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