Der Altbestand des Registers wird geradegezogen

Was der doppelte Zuhoerer geschrieben hat, steht noch da: zweiundzwanzig
Zeilen fuer zehn Vorgaenge. Das Register beantwortet damit genau die eine
Frage falsch, fuer die es gebaut wurde — hat der Kunde das bekommen, und wie
oft.

Die Wanderung liest den Fehler rueckwaerts. Der Zuhoerer verdoppelte jeden
Versand exakt, also bleibt von jeder Gruppe gleicher Zeilen (Empfaenger,
Betreff, Mailklasse, Zeitpunkt) die aeltere Haelfte stehen. Aus vier werden
zwei — das waren zwei echte Anmeldungen in derselben Sekunde, beide
verdoppelt —, aus zwei wird eine, Einzelnes bleibt unberuehrt. Ungerade
Gruppen kann dieser Fehler nicht erzeugt haben; taucht doch eine auf, wird
aufgerundet, damit im Zweifel eine Zeile zu viel stehen bleibt.

down() ist leer, mit Begruendung: geloeschte Zeilen kommen nicht zurueck, und
eine erfundene Zeile waere schlimmer als die Luecke.

Auf dieser Installation gefahren: 22 Zeilen vorher, 11 danach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/versandtakt
nexxo 2026-08-03 21:18:26 +02:00
parent 1bc1e972d6
commit 4023a0939c
2 changed files with 100 additions and 0 deletions

View File

@ -0,0 +1,68 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
/**
* Raeumt die Doppelungen aus dem Versandregister.
*
* RecordSentMail war doppelt angemeldet von Hand im AppServiceProvider und
* durch Laravels Erkennung der Zuhoerer in app/Listeners. Jeder Versand
* hinterliess dadurch ZWEI gleiche Zeilen. Die Anmeldung von Hand ist weg;
* was seither geschrieben wird, stimmt. Der Altbestand nicht: dort steht zu
* jedem Vorgang eine Zustellung, die es nie gab, und das Register beantwortet
* genau die eine Frage falsch, fuer die es gebaut wurde hat der Kunde das
* bekommen, und wie oft.
*
* Die Regel ist die des Fehlers, rueckwaerts gelesen: der Zuhoerer verdoppelte
* jeden Versand exakt, also bleibt von jeder Gruppe gleicher Zeilen die
* HAELFTE stehen, die aeltere. Eine Gruppe aus vier Zeilen waren zwei echte
* Versande daraus werden zwei, nicht eine. Ungerade Gruppen kann dieser
* Fehler nicht erzeugt haben; sollte doch eine auftauchen, wird aufgerundet,
* damit im Zweifel eine Zeile zu viel stehen bleibt statt eine zu wenig.
*
* Gleich heisst: derselbe Empfaenger, derselbe Betreff, dieselbe Mailklasse,
* derselbe Zeitpunkt. Zwei echte Mails, die in derselben Sekunde mit
* identischem Betreff an dieselbe Adresse gingen, waren ebenfalls verdoppelt
* sie kommen als vier an und gehen als zwei heraus.
*/
return new class extends Migration
{
public function up(): void
{
$gruppen = DB::table('sent_mails')
->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.
}
};

View File

@ -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