reports: Kohortenalter ab Stichtag statt ab --to; legacy:settle-unpaid kennt bereits ersetzte Quellen; dev: Basiswert 0.1 vom Live-System ausgewertet

view-distribution rechnete das Alter der Kohorten ab dem Enddatum des
Fensters, die Zähler stehen aber immer auf heute — mit einem --to in der
Vergangenheit waren alle Kohorten um die Verzögerung des Laufs zu jung.
Das Alter zählt jetzt ab dem Stichtag der Erhebung (Feld stichtag), junge
Kohorten fallen bei altem Enddatum leer aus.

settle-unpaid meldete beim zweiten Lauf einen Posten, dessen Quelle der
erste Lauf schon per MAN-Rechnung ersetzt hatte (superseded), als
Konflikt. Solche Quellen zählen jetzt als already_reissued.

Basiswerte Block 0: Live-Erhebung vom 21.09.2026 (gleiches Fenster,
Median 126/303 statt 98/200) eingetragen, Kohortenzeile mit Alters-
Versatz erklärt, letzter Schritt für 0.1 benannt (Lauf ohne --to).
Protokoll: Nach-Deploy-Schritte, Lesart von Audit und Rechnungslauf.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-21 15:32:32 +00:00
parent dab14dc810
commit f9019eecda
9 changed files with 392 additions and 21 deletions

View file

@ -217,7 +217,20 @@ test('dry-run writes nothing and a second real run is a no-op', function () {
expect($holdPost->fresh()->status)->toBe(UserPaymentOptionStatus::Cancelled);
expect(Invoice::count())->toBe(1);
$this->artisan(SettleUnpaidLegacyInvoices::class, ['--no-report' => true])->assertSuccessful();
// Zweiter Lauf: die schon ersetzte Alt-Rechnung ist kein Konflikt, sondern erledigt.
$this->artisan(SettleUnpaidLegacyInvoices::class, ['--no-report' => true])
->doesntExpectOutputToContain('Konflikt')
->expectsTable(['Ergebnis', 'Anzahl'], [
['bundles_cancelled', 0],
['man_invoices_voided', 0],
['reopened', 0],
['man_invoices_issued', 0],
['singles_written_off', 0],
['conflicts', 0],
['untouched_paid', 0],
['already_reissued', 1],
])
->assertSuccessful();
expect(Invoice::count())->toBe(1);
expect(UserPaymentOption::query()->where('status', UserPaymentOptionStatus::Cancelled->value)->count())->toBe(1);
});

View file

@ -130,6 +130,27 @@ test('the view distribution groups releases into age cohorts', function () {
->and($cohorts['28–55 Tage']['median'])->toBe(300);
});
test('the age cohorts count from today even when the window ends in the past', function () {
/** @var TestCase $this */
baselineRelease(Portal::Presseecho, 10, ['published_at' => now()->subDays(2)]);
baselineRelease(Portal::Presseecho, 300, ['published_at' => now()->subDays(40)]);
baselineRelease(Portal::Presseecho, 700, ['published_at' => now()->subDays(100)]);
// `hits` steht auf dem Stand von heute — eine vor 40 Tagen veröffentlichte
// Meldung ist 40 Tage alt, auch wenn das Fenster vor 20 Tagen endet. Die
// Meldung von vorgestern liegt außerhalb des Fensters; die junge Kohorte
// fällt leer aus, statt 20 Tage alte Meldungen als „0–6 Tage" zu führen.
$data = app(ViewDistributionReport::class)->build(now()->subDays(120), now()->subDays(20));
$cohorts = collect(collect($data['portals'])->firstWhere('portal', 'presseecho')['cohorts'])
->keyBy('alter');
expect($data['stichtag'])->toBe(now()->toDateString())
->and($cohorts->has('0–6 Tage'))->toBeFalse()
->and($cohorts['28–55 Tage']['anzahl'])->toBe(1)
->and($cohorts['28–55 Tage']['median'])->toBe(300)
->and($cohorts['91+ Tage']['median'])->toBe(700);
});
test('the age cohorts leave out legacy stock', function () {
/** @var TestCase $this */
baselineRelease(Portal::Presseecho, 50, ['published_at' => now()->subDays(3)]);