← back to AbramsOS
lib/reorder-timing.js
53 lines
// reorder-timing.js — "reorder soon" nudges for repeat-buy items whose cadence says
// they're about due. Deterministic (no LLM): next_due = last_ordered_at + cadence_days;
// if that's within the look-ahead window (or already past), draft a savings_suggestion
// (kind='reorder-timing'). Deduped per reorder item; never fabricates a date.
const db = require('./db');
const { id } = require('./ids');
const LOOKAHEAD_DAYS = 7;
async function alreadyOpen(userId, reorderId) {
const r = await db.query(
`SELECT 1 FROM savings_suggestion WHERE user_id=$1 AND kind='reorder-timing'
AND reorder_item_id=$2 AND status IN ('new','saved')`, [userId, reorderId]);
return r.rows.length > 0;
}
async function generateReorderNudges(userId, opts = {}) {
const items = (await db.query(
`SELECT id, name, merchant, reorder_cadence_days, last_ordered_at, next_due_date, best_price, typical_price, currency
FROM reorder_item WHERE user_id=$1 AND status='active'
AND reorder_cadence_days IS NOT NULL AND last_ordered_at IS NOT NULL`,
[userId])).rows;
const now = Date.now(), DAY = 86400000;
let created = 0;
for (const it of items) {
const due = it.next_due_date
? new Date(it.next_due_date).getTime()
: new Date(it.last_ordered_at).getTime() + it.reorder_cadence_days * DAY;
const daysUntil = Math.round((due - now) / DAY);
if (daysUntil > (opts.lookaheadDays || LOOKAHEAD_DAYS)) continue; // not due yet
if (await alreadyOpen(userId, it.id)) continue;
const when = daysUntil <= 0 ? 'now' : `in ${daysUntil}d`;
await db.query(
`INSERT INTO savings_suggestion
(id,user_id,kind,title,current_item,suggested_item,savings_basis,merchant,rationale,confidence,reorder_item_id,metadata_jsonb)
VALUES ($1,$2,'reorder-timing',$3,$4,$5,$6,$7,$8,$9,$10,$11)`,
[
id('saving'), userId, `Reorder soon: ${it.name}`.slice(0, 200),
it.name, `Reorder ${when}`, 'timing', it.merchant || null,
`Based on your ~${it.reorder_cadence_days}-day cycle, you're due to reorder ${when}. Buy at your best-known price${it.best_price ? ` (${it.currency || '$'}${it.best_price})` : ''}.`,
0.7, it.id,
JSON.stringify({ days_until: daysUntil, next_due: new Date(due).toISOString().slice(0, 10) }),
]);
created++;
}
return { scanned: items.length, created };
}
module.exports = { generateReorderNudges, LOOKAHEAD_DAYS };