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
parent
1bc1e972d6
commit
4023a0939c
|
|
@ -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.
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue