Files
unknownandClaude Opus 5 0d08df0bff feat(erinnerungen): e-mail, wenn ein wunschlisten-termin naeher rueckt
Ruecken Termin und offene Wuensche zusammen, geht eine Nachricht an den
Besitzer der Liste - standardmaessig 14 Tage vorher, hoechstens einmal
pro Woche je Liste. Ist alles reserviert, bleibt es still: eine
Erinnerung ohne Handlungsbedarf ist nur Laerm.

Der Zeitplan haengt an Next' instrumentation-Hook und prueft stuendlich,
nicht taeglich - so kann ein Neustart keinen Tag ueberspringen, und die
Sperrfrist je Liste verhindert trotzdem Mehrfachversand. Der Zeitstempel
wird erst nach erfolgreichem Versand gesetzt, damit ein Fehlversuch
wiederholt wird.

Ohne SMTP_HOST und SMTP_FROM ist die Funktion komplett aus: kein
Zeitplan, kein Versand, und die Verwaltungsseite erklaert stattdessen,
was in die .env gehoert. Mit Konfiguration gibt es dort Knoepfe fuer
Verbindungstest, Testnachricht und sofortige Pruefung.

12 Tests fuer die Entscheidungsregeln. Einer davon hat einen echten
Fehler gefunden: die Sperrfrist rechnete mit umgedrehtem Vorzeichen und
haette jede Erinnerung dauerhaft unterdrueckt.

Refs Phase 3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 09:22:29 +02:00

132 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
daysUntil,
reminderSubject,
reminderText,
shouldRemind,
type RemindableWishlist,
} from '@/lib/reminders';
const now = new Date('2026-12-10T09:00:00');
const on = (iso: string) => new Date(`${iso}T00:00:00`);
function list(overrides: Partial<RemindableWishlist> = {}): RemindableWishlist {
return {
id: 'w-1',
title: 'Weihnachten 2026',
eventDate: on('2026-12-24'),
openItemCount: 3,
lastReminderAt: null,
...overrides,
};
}
describe('Tage bis zum Termin', () => {
it('zaehlt ganze Tage, unabhaengig von der Uhrzeit', () => {
expect(daysUntil(on('2026-12-24'), now)).toBe(14);
expect(daysUntil(on('2026-12-10'), now)).toBe(0);
expect(daysUntil(on('2026-12-09'), now)).toBe(-1);
});
});
describe('Wann erinnert wird', () => {
it('erinnert, wenn der Termin im Fenster liegt und etwas offen ist', () => {
const decision = shouldRemind(list(), { now, daysBefore: 14 });
expect(decision).toEqual({ remind: true, daysLeft: 14 });
});
it('schweigt, solange der Termin weit weg ist', () => {
const decision = shouldRemind(list({ eventDate: on('2027-06-01') }), {
now,
daysBefore: 14,
});
expect(decision).toEqual({ remind: false, reason: 'too-early' });
});
it('erinnert am Tag selbst noch', () => {
const decision = shouldRemind(list({ eventDate: on('2026-12-10') }), {
now,
daysBefore: 14,
});
expect(decision).toEqual({ remind: true, daysLeft: 0 });
});
it('schweigt, wenn der Termin vorbei ist', () => {
const decision = shouldRemind(list({ eventDate: on('2026-12-01') }), {
now,
daysBefore: 14,
});
expect(decision).toEqual({ remind: false, reason: 'past' });
});
it('schweigt ohne Termin', () => {
const decision = shouldRemind(list({ eventDate: null }), { now, daysBefore: 14 });
expect(decision).toEqual({ remind: false, reason: 'no-date' });
});
it('schweigt, wenn alles reserviert ist', () => {
const decision = shouldRemind(list({ openItemCount: 0 }), { now, daysBefore: 14 });
expect(decision).toEqual({ remind: false, reason: 'nothing-open' });
});
it('wiederholt sich nicht innerhalb einer Woche', () => {
const decision = shouldRemind(list({ lastReminderAt: on('2026-12-08') }), {
now,
daysBefore: 14,
});
expect(decision).toEqual({ remind: false, reason: 'already-sent' });
});
it('erinnert wieder, wenn die Sperrfrist vorbei ist', () => {
const decision = shouldRemind(list({ lastReminderAt: on('2026-12-01') }), {
now,
daysBefore: 14,
});
expect(decision).toMatchObject({ remind: true });
});
});
describe('Betreff und Text', () => {
it('formuliert die Naehe im Betreff', () => {
expect(reminderSubject(list(), 0)).toBe('Heute: Weihnachten 2026');
expect(reminderSubject(list(), 1)).toBe('Morgen: Weihnachten 2026');
expect(reminderSubject(list(), 5)).toBe('In 5 Tagen: Weihnachten 2026');
});
it('nennt Termin, offene Wuensche und beide Links', () => {
const text = reminderText({
title: 'Weihnachten 2026',
daysLeft: 3,
openItemCount: 2,
shareUrl: 'https://bricktrack.example.de/w/abc',
appUrl: 'https://bricktrack.example.de/wunschlisten/w-1',
});
expect(text).toContain('ist in 3 Tagen');
expect(text).toContain('stehen noch 2 Wünsche offen');
expect(text).toContain('https://bricktrack.example.de/w/abc');
expect(text).toContain('https://bricktrack.example.de/wunschlisten/w-1');
});
it('bleibt im Singular, wenn nur ein Wunsch offen ist', () => {
const text = reminderText({
title: 'Geburtstag',
daysLeft: 1,
openItemCount: 1,
shareUrl: 'x',
appUrl: 'y',
});
expect(text).toContain('ist morgen');
expect(text).toContain('steht noch 1 Wunsch offen');
});
});