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