← back to AbramsOS
routes/warranties.js
105 lines
// Warranties — manual entry of warranties / guarantees / return windows.
// Rows land in service_commitment (source='manual'), same table the receipt
// extractor populates, so they share the Deadlines engine + claims flow.
const express = require('express');
const db = require('../lib/db');
const audit = require('../lib/audit');
const { id } = require('../lib/ids');
const { daysUntil } = require('../lib/recurrence');
const router = express.Router();
const DEV_USER_ID = 'user_steve';
const TYPES = ['warranty', 'return', 'guarantee', 'service', 'other'];
function decorate(w) {
const d = w.refund_window_ends_at ? daysUntil(w.refund_window_ends_at) : null;
return {
...w,
days_until: d,
is_expired: d != null && d < 0,
is_expiring_soon: d != null && d >= 0 && d <= 30,
};
}
async function listWarranties(userId) {
const r = await db.query(
`SELECT * FROM service_commitment
WHERE user_id = $1
ORDER BY (refund_window_ends_at IS NULL), refund_window_ends_at ASC, created_at DESC`,
[userId]
);
return r.rows.map(decorate);
}
router.get('/warranties', async (_req, res) => {
const warranties = await listWarranties(DEV_USER_ID);
res.render('warranties', { warranties, types: TYPES });
});
router.get('/api/warranties', async (_req, res) => {
res.json(await listWarranties(DEV_USER_ID));
});
router.post('/api/warranties', async (req, res) => {
try {
const b = req.body || {};
if (!b.provider_name && !b.guarantee_text) return res.status(400).json({ error: 'provider or coverage description required' });
const wId = id('warranty');
await db.query(
`INSERT INTO service_commitment
(id, user_id, provider_name, commitment_type, guarantee_text, promised_outcome, window_days, refund_window_ends_at, conditions, source, confidence)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,'manual',1.0)`,
[
wId, DEV_USER_ID, b.provider_name || null,
TYPES.includes(b.commitment_type) ? b.commitment_type : 'warranty',
b.guarantee_text || b.provider_name || 'Warranty',
b.promised_outcome || null,
b.window_days ? parseInt(b.window_days, 10) : null,
b.refund_window_ends_at || null,
b.conditions || null,
]
);
await audit.log({ actorType: 'user', actorId: DEV_USER_ID, objectType: 'service_commitment', objectId: wId, eventType: 'warranty_created', metadata: { provider: b.provider_name, type: b.commitment_type } });
const r = await db.query(`SELECT * FROM service_commitment WHERE id = $1`, [wId]);
res.status(201).json(decorate(r.rows[0]));
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/api/warranties/:id', async (req, res) => {
try {
const b = req.body || {};
const fields = ['provider_name', 'commitment_type', 'guarantee_text', 'promised_outcome', 'window_days', 'refund_window_ends_at', 'conditions'];
const sets = [];
const vals = [];
let i = 1;
for (const f of fields) {
if (b[f] === undefined) continue;
sets.push(`${f} = $${i++}`);
vals.push(b[f] === '' ? null : b[f]);
}
if (!sets.length) return res.status(400).json({ error: 'no fields to update' });
vals.push(req.params.id, DEV_USER_ID);
const r = await db.query(
`UPDATE service_commitment 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: 'service_commitment', objectId: req.params.id, eventType: 'warranty_updated' });
res.json(decorate(r.rows[0]));
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/api/warranties/:id/delete', async (req, res) => {
await db.query(`DELETE FROM calendar_reminder WHERE user_id = $1 AND owner_table = 'service_commitment' AND owner_id = $2`, [DEV_USER_ID, req.params.id]);
await db.query(`DELETE FROM service_commitment WHERE id = $1 AND user_id = $2`, [req.params.id, DEV_USER_ID]);
await audit.log({ actorType: 'user', actorId: DEV_USER_ID, objectType: 'service_commitment', objectId: req.params.id, eventType: 'warranty_deleted' });
res.json({ ok: true });
});
module.exports = router;