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

@@ -60,9 +60,8 @@ export class Notifier {
? { id: SYSTEM_ACTOR_ID, name: SYSTEM_ACTOR_NAME }
: refs.of(event.actorId);
const delivered: NotificationReadModel[] = [];
for (const draft of wanted) {
const notification: NotificationReadModel = {
await this.send(
wanted.map((draft) => ({
id: `${event.aggregateId}:${versionOf(event)}:${draft.userId}`,
userId: draft.userId,
kind: draft.kind,
@@ -71,13 +70,22 @@ export class Notifier {
summary: draft.summary,
readAt: null,
createdAt: event.occurredAt,
};
})),
);
}
/**
* Stores, pushes and e-mails ready-made notifications. Their id makes them
* idempotent: one that is already there is not sent again.
*/
async send(notifications: NotificationReadModel[]): Promise<void> {
const delivered: NotificationReadModel[] = [];
for (const notification of notifications) {
// Delivered again (at-least-once): already told, nothing to repeat.
if (!(await this.notifications.insert(notification))) continue;
await this.changeFeed.publishNotification(notification);
delivered.push(notification);
}
if (this.mailer.enabled && delivered.length) {
await this.email(delivered);
}