diff --git a/database/migrations/2026_08_04_100000_das_register_zaehlt_jeden_versand_einmal.php b/database/migrations/2026_08_04_100000_das_register_zaehlt_jeden_versand_einmal.php new file mode 100644 index 0000000..24da987 --- /dev/null +++ b/database/migrations/2026_08_04_100000_das_register_zaehlt_jeden_versand_einmal.php @@ -0,0 +1,68 @@ +select('to', 'subject', 'mailable', 'sent_at') + ->selectRaw('COUNT(*) as anzahl') + ->groupBy('to', 'subject', 'mailable', 'sent_at') + ->having('anzahl', '>', 1) + ->get(); + + foreach ($gruppen as $gruppe) { + $ids = DB::table('sent_mails') + ->where('to', $gruppe->to) + ->where('subject', $gruppe->subject) + // Beide Spalten duerfen leer sein, und `= NULL` trifft nichts. + ->when($gruppe->mailable === null, + fn ($q) => $q->whereNull('mailable'), + fn ($q) => $q->where('mailable', $gruppe->mailable)) + ->when($gruppe->sent_at === null, + fn ($q) => $q->whereNull('sent_at'), + fn ($q) => $q->where('sent_at', $gruppe->sent_at)) + ->orderBy('id') + ->pluck('id'); + + $behalten = (int) ceil($ids->count() / 2); + $weg = $ids->slice($behalten)->all(); + + if ($weg !== []) { + DB::table('sent_mails')->whereIn('id', $weg)->delete(); + } + } + } + + public function down(): void + { + // Geloeschte Zeilen kommen nicht zurueck, und eine Zeile zu erfinden + // waere schlimmer als die Luecke: das Register darf nichts behaupten. + } +}; diff --git a/tests/Feature/Admin/MailRegisterTest.php b/tests/Feature/Admin/MailRegisterTest.php index bfbf1dd..20480df 100644 --- a/tests/Feature/Admin/MailRegisterTest.php +++ b/tests/Feature/Admin/MailRegisterTest.php @@ -52,6 +52,38 @@ it('writes exactly one row per recipient, however the listener is registered', f expect(SentMail::query()->where('to', $customer->email)->count())->toBe(1); }); +/** + * Die Wanderung, die den Altbestand geradezieht. + * + * Der doppelte Zuhörer hat jeden Versand exakt verdoppelt, also bleibt von + * jeder Gruppe gleicher Zeilen die ältere Hälfte stehen — aus vier werden zwei + * (das waren zwei echte Versande), aus zwei wird eine, und was einzeln steht, + * bleibt unberührt. + */ +it('halves the duplicated rows the doubled listener left behind', function () { + $zeile = fn (string $to, string $subject, string $zeit) => SentMail::create([ + 'to' => $to, 'subject' => $subject, 'mailable' => \App\Mail\OperatorMessageMail::class, + 'from_operator' => false, 'sent_at' => $zeit, + ]); + + // Zwei echte Versande, verdoppelt. + foreach (range(1, 4) as $i) { + $zeile('vier@example.com', 'Vier', '2026-07-30 12:00:00'); + } + // Ein echter Versand, verdoppelt. + foreach (range(1, 2) as $i) { + $zeile('zwei@example.com', 'Zwei', '2026-07-30 12:00:01'); + } + // Nach der Behebung geschrieben — steht schon richtig da. + $zeile('eins@example.com', 'Eins', '2026-07-30 12:00:02'); + + (require database_path('migrations/2026_08_04_100000_das_register_zaehlt_jeden_versand_einmal.php'))->up(); + + expect(SentMail::query()->where('to', 'vier@example.com')->count())->toBe(2) + ->and(SentMail::query()->where('to', 'zwei@example.com')->count())->toBe(1) + ->and(SentMail::query()->where('to', 'eins@example.com')->count())->toBe(1); +}); + it('records the send, not the intention', function () { // On MessageSent, not MessageSending: a row here has to mean the transport // accepted it. Recording intentions would fill the register with mails that