← back to AbramsOS

routes/reviews.js

67 lines

'use strict';
/**
 * Reviews — generate Amazon review + seller thank-you drafts from AbramsOS
 * purchases, batch-approve, and post per-item through the gated executor.
 * Auth: mounted after requireAuth. /api/* is CSRF-exempt (same-origin JSON) per
 * middleware/csrf. Nothing posts without approve + seal + REVIEWS_EXECUTORS_LIVE
 * + per-item confirm; reviews never auto-submit (openclaw returns needs_verify).
 */
const express = require('express');
const model = require('../lib/reviews/model');
const executors = require('../lib/reviews/executors');

const router = express.Router();
const DEV_USER_ID = 'user_steve';
const uid = (req) => req.userId || DEV_USER_ID;

// Page
router.get('/reviews', async (req, res) => {
  const items = await model.listWithDrafts(uid(req));
  res.render('reviews', { items, summary: model.summarize(items), executorsLive: executors.liveEnabled() });
});

// Data
router.get('/api/reviews', async (req, res) => {
  const items = await model.listWithDrafts(uid(req));
  res.json({ ok: true, items, summary: model.summarize(items), executorsLive: executors.liveEnabled() });
});

// Generate drafts from purchases (all, or a selected subset)
router.post('/api/reviews/generate', async (req, res) => {
  try {
    const out = await model.generate(uid(req), Array.isArray(req.body?.purchaseIds) ? req.body.purchaseIds : null);
    res.json({ ok: true, ...out });
  } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
});

// Batch approve (seals each row's current content — same seal path as single)
router.post('/api/reviews/approve-batch', async (req, res) => {
  const ids = Array.isArray(req.body?.ids) ? req.body.ids : [];
  if (!ids.length) return res.status(400).json({ ok: false, error: 'no ids' });
  const out = await model.approveBatch(uid(req), ids);
  res.json({ ok: true, ...out });
});

router.post('/api/reviews/unapprove', async (req, res) => { const r = await model.setStatus(uid(req), req.body?.id, 'draft'); res.json({ ok: !!r, item: r }); });
router.post('/api/reviews/skip', async (req, res) => { const r = await model.setStatus(uid(req), req.body?.id, 'skipped'); res.json({ ok: !!r, item: r }); });
router.post('/api/reviews/seller-email-to', async (req, res) => { const r = await model.setSellerEmailTo(uid(req), req.body?.id, req.body?.to || ''); res.json({ ok: !!r, item: r }); });

// Post approved — PACED, one at a time, each through the gated executor.
// NO bulk fire and NO auto-approve: the caller supplies exactly the ids the human
// selected, and each still needs approve + seal + live + confirm.
router.post('/api/reviews/post-approved', async (req, res) => {
  const ids = Array.isArray(req.body?.ids) ? req.body.ids : [];
  if (!ids.length) return res.status(400).json({ ok: false, error: 'no ids' });
  if (!executors.liveEnabled()) return res.json({ ok: true, live: false, results: ids.map((id) => ({ id, status: 'gated', reason: 'REVIEWS_EXECUTORS_LIVE not set' })) });
  const delay = Math.min(Number(req.body?.delayMs) || 1500, 8000);
  const results = [];
  for (const id of ids) {
    const r = await executors.executeDraft(uid(req), id, { live: true, confirm: id });
    results.push({ id, status: r.status || (r.gated ? 'gated' : 'error'), reason: r.reason });
    await new Promise((rz) => setTimeout(rz, delay));
  }
  res.json({ ok: true, live: true, results });
});

module.exports = router;