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