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:
70
tests/unit/services/document-reminders-cadence.test.ts
Normal file
70
tests/unit/services/document-reminders-cadence.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isReminderDue } from '@/lib/services/document-reminders';
|
||||
|
||||
const now = new Date('2026-04-28T12:00:00Z');
|
||||
|
||||
function args(overrides: Partial<Parameters<typeof isReminderDue>[0]> = {}) {
|
||||
return {
|
||||
status: 'sent' as const,
|
||||
documensoId: 'doc-1',
|
||||
remindersDisabled: false,
|
||||
reminderCadenceOverride: null,
|
||||
templateCadenceDays: 7,
|
||||
lastReminderAt: null,
|
||||
now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('isReminderDue', () => {
|
||||
it('returns true when no prior reminder exists and cadence is set', () => {
|
||||
expect(isReminderDue(args())).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when document is completed', () => {
|
||||
expect(isReminderDue(args({ status: 'completed' }))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when document has no Documenso id', () => {
|
||||
expect(isReminderDue(args({ documensoId: null }))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when reminders are disabled per-doc', () => {
|
||||
expect(isReminderDue(args({ remindersDisabled: true }))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when neither override nor template cadence is set', () => {
|
||||
expect(isReminderDue(args({ templateCadenceDays: null }))).toBe(false);
|
||||
});
|
||||
|
||||
it('respects per-doc override over template default', () => {
|
||||
// 1-day override, last fired 12h ago → not due
|
||||
const lastReminderAt = new Date(now.getTime() - 12 * 60 * 60 * 1000);
|
||||
expect(
|
||||
isReminderDue(args({ templateCadenceDays: 7, reminderCadenceOverride: 1, lastReminderAt })),
|
||||
).toBe(false);
|
||||
|
||||
// 1-day override, last fired 25h ago → due
|
||||
const earlier = new Date(now.getTime() - 25 * 60 * 60 * 1000);
|
||||
expect(
|
||||
isReminderDue(
|
||||
args({
|
||||
templateCadenceDays: 7,
|
||||
reminderCadenceOverride: 1,
|
||||
lastReminderAt: earlier,
|
||||
}),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('treats template cadence as the fallback when no override', () => {
|
||||
// 7-day template, last fired 6 days ago → not due
|
||||
const lastReminderAt = new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000);
|
||||
expect(isReminderDue(args({ lastReminderAt }))).toBe(false);
|
||||
|
||||
// 7 days exactly → due
|
||||
const sevenDays = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
expect(isReminderDue(args({ lastReminderAt: sevenDays }))).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user