feat(reminders): cadence-aware framework with auto/manual modes

isReminderDue now keys off doc.remindersDisabled and the effective
cadence (per-doc override → template default), dropping the implicit
interests.reminderEnabled gate so non-EOI docs auto-remind correctly.
sendReminderIfAllowed gains an options bag — auto:true keeps the 9-16
window + cadence cooldown for the cron, auto:false bypasses both for
manual UI sends. signerId targets a specific pending signer (must be
next in sequential mode). 7 unit tests cover the cadence math.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-28 02:50:00 +02:00
parent df0b408b7a
commit 978df1c4d7
3 changed files with 252 additions and 78 deletions

View File

@@ -1,14 +1,30 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { errorResponse } from '@/lib/errors';
import { sendReminderIfAllowed } from '@/lib/services/document-reminders';
const remindBodySchema = z
.object({
signerId: z.string().optional(),
})
.optional();
export const POST = withAuth(
withPermission('documents', 'edit', async (req, ctx, params) => {
try {
const sent = await sendReminderIfAllowed(params.id!, ctx.portId);
return NextResponse.json({ data: { sent } });
let signerId: string | undefined;
const text = await req.text();
if (text) {
const parsed = remindBodySchema.safeParse(JSON.parse(text));
if (parsed.success && parsed.data) signerId = parsed.data.signerId;
}
const result = await sendReminderIfAllowed(params.id!, ctx.portId, {
auto: false,
signerId,
});
return NextResponse.json({ data: result });
} catch (error) {
return errorResponse(error);
}