feat: due-date reminders

Every hour the API reminds whoever has a ticket, and whoever follows it,
the day before its due date, on the day and once when it is overdue, in
the app and by e-mail for those who asked. Cards on the board flag what
is due soon or late.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 18:36:19 +02:00
parent d6a434ca1d
commit 71b807b94b
19 changed files with 371 additions and 7 deletions

View File

@@ -0,0 +1,43 @@
import {
IssueReadModel,
NotificationKind,
} from '@task-stream/api/tracker/domain';
import { dueStage, nextDay, remindersFor } from './reminders';
const issue = (id: string, dueDate: string, watchers: string[] = []) =>
({
id,
key: `WEB-${id}`,
title: `T${id}`,
projectId: 'p',
projectKey: 'WEB',
dueDate,
assignee: { id: 'ana', name: 'Ana' },
watchers: watchers.map((w) => ({ id: w, name: w })),
}) as IssueReadModel;
describe('due reminders', () => {
it('tells the stage of a due date', () => {
expect(nextDay('2026-12-31')).toBe('2027-01-01');
expect(dueStage('2026-10-09', '2026-10-08')).toBe('tomorrow');
expect(dueStage('2026-10-08', '2026-10-08')).toBe('today');
expect(dueStage('2026-10-01', '2026-10-08')).toBe('overdue');
expect(dueStage('2026-10-20', '2026-10-08')).toBeNull();
});
it('reminds whoever has it and whoever follows it, once per stage', () => {
const reminders = remindersFor(
[issue('1', '2026-10-08', ['ana', 'bob']), issue('2', '2026-10-30')],
'2026-10-08',
'2026-10-08T07:00:00Z',
);
expect(reminders.map((r) => [r.userId, r.summary])).toEqual([
['ana', 'WEB-1 vence hoy: T1'],
['bob', 'WEB-1 vence hoy: T1'],
]);
expect(reminders[0]).toMatchObject({
kind: NotificationKind.REMINDER,
id: 'reminder:1:2026-10-08:today:ana',
});
});
});