From f9019eecdabf1c551825a4eb23051f6edfc1830f Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Mon, 21 Sep 2026 15:32:32 +0000 Subject: [PATCH] reports: Kohortenalter ab Stichtag statt ab --to; legacy:settle-unpaid kennt bereits ersetzte Quellen; dev: Basiswert 0.1 vom Live-System ausgewertet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Commands/ReportPressReleaseViews.php | 2 +- .../Commands/SettleUnpaidLegacyInvoices.php | 10 + .../PressRelease/ViewDistributionReport.php | 23 +- dev/2026-09-01/basiswert-0.1.json | 244 ++++++++++++++++++ dev/2026-09-01/basiswerte-block0.md | 49 +++- dev/2026-09-01/roadmap-2026.md | 2 +- dev/2026-09-01/umsetzung-protokoll.md | 47 +++- .../Billing/LegacySettleUnpaidCommandTest.php | 15 +- .../Feature/ConversionBaselineReportsTest.php | 21 ++ 9 files changed, 392 insertions(+), 21 deletions(-) create mode 100644 dev/2026-09-01/basiswert-0.1.json diff --git a/app/Console/Commands/ReportPressReleaseViews.php b/app/Console/Commands/ReportPressReleaseViews.php index 502c6371..1bd857b4 100644 --- a/app/Console/Commands/ReportPressReleaseViews.php +++ b/app/Console/Commands/ReportPressReleaseViews.php @@ -111,7 +111,7 @@ class ReportPressReleaseViews extends Command } if ($kohorten !== []) { - $this->line(' Aufrufe nach Alter (nur Meldungen seit dem Relaunch)'); + $this->line(" Aufrufe nach Alter am Stichtag {$data['stichtag']} (nur Meldungen seit dem Relaunch)"); $this->table(['Alter der Meldung', 'PMs', 'Median', 'Mittelwert'], $kohorten); // Der Gesamtmedian oben hängt an der Altersmischung des // Fensters und ist zwischen zwei Messzeitpunkten nicht diff --git a/app/Console/Commands/SettleUnpaidLegacyInvoices.php b/app/Console/Commands/SettleUnpaidLegacyInvoices.php index 6e6fc2c8..25875513 100644 --- a/app/Console/Commands/SettleUnpaidLegacyInvoices.php +++ b/app/Console/Commands/SettleUnpaidLegacyInvoices.php @@ -73,6 +73,7 @@ class SettleUnpaidLegacyInvoices extends Command 'singles_written_off' => [], 'conflicts' => [], 'untouched_paid' => [], + 'already_reissued' => [], ]; if ($this->dryRun) { @@ -161,6 +162,15 @@ class SettleUnpaidLegacyInvoices extends Command continue; } + // Ein früherer Lauf hat die Forderung schon als MAN-Rechnung neu + // ausgestellt — der Posten bleibt Bestandsschutz, die Quelle trägt + // `superseded`. Kein Konflikt, nichts zu tun. + if ($status === LegacyInvoice::STATUS_SUPERSEDED) { + $this->report['already_reissued'][] = $this->describe($post, $source).' · ersetzt durch '.($source->raw_snapshot['superseded_by_invoice_number'] ?? '?'); + + continue; + } + // hold im gemischten Bündel oder Quelle nicht auffindbar → manuell klären. $this->report['conflicts'][] = $this->describe($post, $source).' · Status '.($status ?? 'unbekannt').' im gemischten Bündel'; } diff --git a/app/Services/PressRelease/ViewDistributionReport.php b/app/Services/PressRelease/ViewDistributionReport.php index cb085d28..366c31fd 100644 --- a/app/Services/PressRelease/ViewDistributionReport.php +++ b/app/Services/PressRelease/ViewDistributionReport.php @@ -53,12 +53,14 @@ class ViewDistributionReport * @return array{ * from: string, * to: string, + * stichtag: string, * portals: list}}>, cohorts: list}>, * } */ public function build(Carbon $from, ?Carbon $to = null): array { - $to ??= Carbon::now(); + $reference = Carbon::now(); + $to ??= $reference->copy(); $portals = []; @@ -79,13 +81,14 @@ class ViewDistributionReport 'portal' => $portal->value, 'label' => $portal->label(), 'segments' => $segments, - 'cohorts' => $this->cohorts($portal, $from, $to), + 'cohorts' => $this->cohorts($portal, $from, $to, $reference), ]; } return [ 'from' => $from->toDateString(), 'to' => $to->toDateString(), + 'stichtag' => $reference->toDateString(), 'portals' => $portals, ]; } @@ -109,9 +112,15 @@ class ViewDistributionReport * Nur Meldungen seit dem Relaunch — die Zähler des Altbestands stammen aus * dem Altsystem und haben keine hier nachvollziehbare Vorgeschichte. * + * Das Alter zählt ab dem Stichtag der Erhebung (heute), nicht ab `$to`: + * `hits` steht immer auf dem Stand von heute, also muss auch das Alter + * von heute aus gerechnet werden. Mit einem `$to` in der Vergangenheit + * fallen die jungen Kohorten schlicht leer aus — statt Meldungen, die in + * Wahrheit Wochen älter sind, als „0–6 Tage" auszuweisen. + * * @return list */ - private function cohorts(Portal $portal, Carbon $from, Carbon $to): array + private function cohorts(Portal $portal, Carbon $from, Carbon $to, Carbon $reference): array { $bounds = self::Cohorts; $result = []; @@ -120,15 +129,15 @@ class ViewDistributionReport $upperDays = $bounds[$i + 1] ?? null; // Älter heißt: früher veröffentlicht. Die Altersgrenzen drehen sich - // deshalb zu Datumsgrenzen um. - $newest = $to->copy()->subDays($lowerDays); - $oldest = $upperDays === null ? $from : $to->copy()->subDays($upperDays - 1); + // deshalb zu Datumsgrenzen um — und bleiben im Fenster from..to. + $newest = $reference->copy()->subDays($lowerDays)->min($to); + $oldest = ($upperDays === null ? $from : $reference->copy()->subDays($upperDays - 1))->max($from); if ($oldest->greaterThan($newest)) { continue; } - $hits = $this->hits($portal, $oldest->max($from), $newest->min($to), true); + $hits = $this->hits($portal, $oldest, $newest, true); $count = count($hits); $result[] = [ diff --git a/dev/2026-09-01/basiswert-0.1.json b/dev/2026-09-01/basiswert-0.1.json new file mode 100644 index 00000000..a9c42018 --- /dev/null +++ b/dev/2026-09-01/basiswert-0.1.json @@ -0,0 +1,244 @@ +{ + "from": "2026-06-01", + "to": "2026-09-01", + "portals": [ + { + "portal": "presseecho", + "label": "Presseecho", + "segments": [ + { + "segment": "gesamt", + "stats": { + "anzahl": 1413, + "min": 24, + "p10": 65, + "p25": 95, + "median": 126, + "p75": 183, + "p90": 229, + "max": 496, + "mittelwert": 141.4, + "verteilung": { + "0–24": 1, + "25–49": 90, + "50–99": 309, + "100–199": 735, + "200–499": 278, + "500–999": 0, + "1000+": 0 + } + } + }, + { + "segment": "seit Relaunch", + "stats": { + "anzahl": 1137, + "min": 58, + "p10": 86, + "p25": 103, + "median": 128, + "p75": 178, + "p90": 233, + "max": 420, + "mittelwert": 146.7, + "verteilung": { + "0–24": 0, + "25–49": 0, + "50–99": 244, + "100–199": 680, + "200–499": 213, + "500–999": 0, + "1000+": 0 + } + } + }, + { + "segment": "Altbestand", + "stats": { + "anzahl": 276, + "min": 24, + "p10": 34, + "p25": 43, + "median": 72, + "p75": 196, + "p90": 216, + "max": 496, + "mittelwert": 119.7, + "verteilung": { + "0–24": 1, + "25–49": 90, + "50–99": 65, + "100–199": 55, + "200–499": 65, + "500–999": 0, + "1000+": 0 + } + } + } + ], + "cohorts": [ + { + "alter": "0–6 Tage", + "anzahl": 188, + "median": 109, + "mittelwert": 112.5 + }, + { + "alter": "7–13 Tage", + "anzahl": 146, + "median": 108, + "mittelwert": 112.2 + }, + { + "alter": "14–20 Tage", + "anzahl": 171, + "median": 104, + "mittelwert": 114.6 + }, + { + "alter": "21–27 Tage", + "anzahl": 136, + "median": 128, + "mittelwert": 139.7 + }, + { + "alter": "28–55 Tage", + "anzahl": 351, + "median": 202, + "mittelwert": 206.8 + }, + { + "alter": "56–90 Tage", + "anzahl": 0, + "median": null, + "mittelwert": null + }, + { + "alter": "91+ Tage", + "anzahl": 0, + "median": null, + "mittelwert": null + } + ] + }, + { + "portal": "businessportal24", + "label": "Businessportal24", + "segments": [ + { + "segment": "gesamt", + "stats": { + "anzahl": 1466, + "min": 26, + "p10": 149, + "p25": 243, + "median": 303, + "p75": 354, + "p90": 409, + "max": 1536, + "mittelwert": 303.5, + "verteilung": { + "0–24": 0, + "25–49": 24, + "50–99": 52, + "100–199": 168, + "200–499": 1152, + "500–999": 61, + "1000+": 9 + } + } + }, + { + "segment": "seit Relaunch", + "stats": { + "anzahl": 1173, + "min": 99, + "p10": 213, + "p25": 264, + "median": 310, + "p75": 353, + "p90": 395, + "max": 1536, + "mittelwert": 311.6, + "verteilung": { + "0–24": 0, + "25–49": 0, + "50–99": 1, + "100–199": 96, + "200–499": 1047, + "500–999": 28, + "1000+": 1 + } + } + }, + { + "segment": "Altbestand", + "stats": { + "anzahl": 293, + "min": 26, + "p10": 54, + "p25": 97, + "median": 198, + "p75": 377, + "p90": 591, + "max": 1365, + "mittelwert": 271.1, + "verteilung": { + "0–24": 0, + "25–49": 24, + "50–99": 51, + "100–199": 72, + "200–499": 105, + "500–999": 33, + "1000+": 8 + } + } + } + ], + "cohorts": [ + { + "alter": "0–6 Tage", + "anzahl": 187, + "median": 252, + "mittelwert": 253.9 + }, + { + "alter": "7–13 Tage", + "anzahl": 154, + "median": 294, + "mittelwert": 298.2 + }, + { + "alter": "14–20 Tage", + "anzahl": 175, + "median": 336, + "mittelwert": 342.1 + }, + { + "alter": "21–27 Tage", + "anzahl": 142, + "median": 319, + "mittelwert": 330.8 + }, + { + "alter": "28–55 Tage", + "anzahl": 368, + "median": 338, + "mittelwert": 338.1 + }, + { + "alter": "56–90 Tage", + "anzahl": 0, + "median": null, + "mittelwert": null + }, + { + "alter": "91+ Tage", + "anzahl": 0, + "median": null, + "mittelwert": null + } + ] + } + ] +} diff --git a/dev/2026-09-01/basiswerte-block0.md b/dev/2026-09-01/basiswerte-block0.md index 07b395e3..2606ae92 100644 --- a/dev/2026-09-01/basiswerte-block0.md +++ b/dev/2026-09-01/basiswerte-block0.md @@ -70,9 +70,52 @@ erheben. > `php artisan press-releases:view-distribution --json > basiswert-0.1.json` > > **21.09.2026:** Lauf mit `--from=2026-06-01 --to=2026-09-01 --json` auf -> dem Live-Server erfolgt, Datei liegt dort unter -> `dev/2026-09-01/basiswert-0.1.json`. Sobald sie im Repo ist, wird die -> Zeile „28–55 Tage" je Portal hier eingetragen. +> dem Live-System, Datei `basiswert-0.1.json` liegt im Ordner. Auswertung +> im nächsten Abschnitt. + +### Erhebung 21.09.2026 — dasselbe Fenster, drei Wochen später + +Gleiches Fenster (01.06.–01.09.), gemessen am 21.09. statt am 01.09. +(`basiswert-0.1.json`): + +| Portal | PMs | Median 01.09. | **Median 21.09.** | Mittelwert 21.09. | P10 / P90 | +|---|---|---|---|---|---| +| Presseecho | 1413 | 98 | **126** | 141,4 | 65 / 229 | +| Businessportal24 | 1466 | 200 | **303** | 303,5 | 149 / 409 | + +Der Gesamtmedian ist in drei Wochen um 29 % (Presseecho) bzw. 52 % +(Businessportal24) gestiegen, ohne dass sich am Fenster etwas geändert hat +— genau der Effekt, vor dem der Abschnitt oben warnt. (Die PM-Zahl ist um +23 bzw. 25 gesunken: seither offline genommene Meldungen.) + +Kohorten laut Datei (Alter gerechnet ab `--to` = 01.09., Zähler aber +Stand 21.09. — das **tatsächliche Alter liegt 20 Tage höher**): + +| Zeile in der Datei | tatsächliches Alter | Presseecho PMs / Median | Businessportal24 PMs / Median | +|---|---|---|---| +| 0–6 Tage | 20–26 | 188 / 109 | 187 / 252 | +| 7–13 Tage | 27–33 | 146 / 108 | 154 / 294 | +| 14–20 Tage | 34–40 | 171 / 104 | 175 / 336 | +| 21–27 Tage | 41–47 | 136 / 128 | 142 / 319 | +| 28–55 Tage | 48–75 (nur ab Go-Live 20.07.) | 351 / 202 | 368 / 338 | + +Lesbar ist daraus schon einiges: Businessportal24 erreicht ab etwa fünf +Wochen ein Plateau um 320–340 Aufrufe; Presseecho liegt bei rund 105–130 +und springt nur in der Go-Live-Kohorte (Meldungen vom 20.07.–04.08.) auf +202 — vermutlich der Erstcrawl nach dem Umzug, kein Dauerzustand. + +**Als Basiswert taugt diese Kohortenzeile trotzdem nicht:** Der Report +rechnete das Alter ab `--to`, die Zähler stehen aber immer auf heute. Mit +einem Enddatum in der Vergangenheit verschieben sich alle Kohorten um die +Verzögerung des Laufs. Seit dem 21.09.2026 zählt der Report das Alter ab +dem Stichtag der Erhebung (Feld `stichtag` in der JSON-Ausgabe); ein +`--to` in der Vergangenheit lässt die jungen Kohorten leer. + +> **Offen (letzter Schritt für 0.1):** einmal mit Stichtag = heute laufen +> lassen, dann steht die Zeile „28–55 Tage" sauber: +> `php artisan press-releases:view-distribution --from=2026-06-01 --json > dev/2026-09-01/basiswert-0.1-kohorten.json` +> (ohne `--to`; auch mit dem Live-Stand vor dem Fix korrekt, weil dann +> Enddatum und Stichtag zusammenfallen). --- diff --git a/dev/2026-09-01/roadmap-2026.md b/dev/2026-09-01/roadmap-2026.md index d1c2270e..5681c0d1 100644 --- a/dev/2026-09-01/roadmap-2026.md +++ b/dev/2026-09-01/roadmap-2026.md @@ -36,7 +36,7 @@ Ohne definierte Konversionspunkte ist keiner der folgenden Blöcke bewertbar. Di | Nr. | Arbeitspaket | Aufwand | Abhängigkeit | |---|---|---|---| -| 0.1 | Basiswert Aufrufzahlen: Median und Verteilung je Portal über die letzten drei Monate | S | keine | +| 0.1 | Basiswert Aufrufzahlen: Median und Verteilung je Portal über die letzten drei Monate — **erhoben 01.09. und 21.09.2026** (`basiswerte-block0.md`); Kohortenzeile „28–55 Tage" braucht noch einen Lauf mit Stichtag = heute | S | keine | | 0.2 | Basiswert Ablehnungsquote je Quelle (API-Client, direkt), als wiederkehrende Auswertung angelegt | S | keine | | 0.3 | Tabelle `conversion_events` anlegen: Ereignistyp, Firmen-ID, Meldungs-ID, Quelle, Zeitstempel | S | keine | | 0.4 | Herkunftsparameter in allen Mail-Links, gespeichert **auf dem Magic-Link-Token**, nicht in der Session | S | 0.3 | diff --git a/dev/2026-09-01/umsetzung-protokoll.md b/dev/2026-09-01/umsetzung-protokoll.md index 29276ec4..9def30e6 100644 --- a/dev/2026-09-01/umsetzung-protokoll.md +++ b/dev/2026-09-01/umsetzung-protokoll.md @@ -2337,11 +2337,42 @@ meldet „Seiten- und Sitemap-Caches geleert." (`CanonicalAssignmentTest` prüft das über den vorher gecachten Sitemap-Stand). Für den Live-Lauf vom 21.09. ist das inzwischen durch die TTL erledigt. -**Noch zu prüfen auf dem Server** (nicht Teil der Ausgabe oben): -`FEED_FETCHES_RECORDED_SINCE=2026-09-21` und -`CRAWLER_HITS_RECORDED_SINCE=2026-09-21` in der `.env` + `config:cache`; -`categories:seed-descriptions` (2.3); `livewire:publish --assets`; -`queue:restart` und FPM-Neustart; Audit-Plan „Live-Schritte nach dem -Deploy" (`company-memberships:audit`, `--classify --before=`, -`schedule:list` auf `billing:expire-grandfathering`); Rechnungs-Paket -`legacy:settle-unpaid --dry-run` → scharf. +**Nach-Deploy-Schritte, Stand 21.09.2026 17:25** (vom Betreiber +ausgeführt, teils schon direkt nach dem Deploy): + +| Schritt | Ergebnis | +|---|---| +| `config:cache` | erneut gelaufen | +| `categories:seed-descriptions` | 28 × „bereits vorhanden" — war schon nach dem Deploy gelaufen | +| `company-memberships:audit` | 91.439 responsible-Zuordnungen, 58.275 über eine E-Mail erklärbar, 33.164 nicht; seit Go-Live 17 nicht erklärbare (12 × connektar-Konto 13993 an API-Firmen vom 20.07., 4 × Dr. Frenzel, 1 × t2t-soft) — alle `manual`, bleiben geschützt. CSV in `storage/app`. **`--classify --before=` steht noch aus.** | +| `legacy:settle-unpaid --dry-run` | 7 `untouched_paid`, 1 „Konflikt", sonst 0 — siehe unten | +| `basiswert-0.1.json` | ins Repo übernommen, ausgewertet in `basiswerte-block0.md` | + +**Lesart `settle-unpaid`:** Die Dev-Kopie hatte 22 Bestandsschutz-Posten +(7 paid, 9 hold, 5 reminder, 1 pending). Live sieht der Dry-Run nur noch +8: die 7 bezahlten und UPO #13, dessen Quelle `43541-7` schon +`superseded` trägt — den Status schreibt ausschließlich der MAN-Kreis beim +Neuausstellen. Der scharfe Lauf war also bereits erfolgt (hold-Bündel +gekündigt, offene Vorgänge neu ausgestellt); der Befehl kannte nur seine +eigene frühere Arbeit nicht und meldete sie als Konflikt. **Behoben:** +Quelle `superseded` zählt jetzt als `already_reissued` (mit der +ersetzenden MAN-Nummer), kein Konflikt (`LegacySettleUnpaidCommandTest`). +Für User 19076 einmal in Admin ▸ Zahlungen prüfen, dass die ersetzende +MAN-Rechnung offen ist. + +**Befund 0.1, behoben:** `view-distribution` rechnete das Kohortenalter ab +`--to`, die Zähler stehen aber immer auf heute — mit `--to=2026-09-01` am +21.09. waren alle Kohorten um 20 Tage zu jung ausgewiesen. Der Report +zählt das Alter jetzt ab dem Stichtag der Erhebung (Feld `stichtag`, +Tabellenüberschrift nennt ihn); ein Enddatum in der Vergangenheit lässt +die jungen Kohorten leer. Die gelieferte Datei ist als Fenster-Verteilung +verwertet (Median 126 / 303 statt 98 / 200 drei Wochen zuvor — der +Beleg, warum der Gesamtmedian kein Basiswert ist). Für die saubere +Kohortenzeile fehlt ein Lauf ohne `--to`: +`press-releases:view-distribution --from=2026-06-01 --json`. + +**Noch offen auf dem Server:** `.env` `FEED_FETCHES_RECORDED_SINCE` und +`CRAWLER_HITS_RECORDED_SINCE` auf `2026-09-21` (dann `config:cache`); +`company-memberships:audit --classify --before=""`; +`livewire:publish --assets`; `queue:restart` + FPM-Neustart; +`schedule:list` auf `billing:expire-grandfathering` (04:15). diff --git a/tests/Feature/Billing/LegacySettleUnpaidCommandTest.php b/tests/Feature/Billing/LegacySettleUnpaidCommandTest.php index 69adec1e..e81bef70 100644 --- a/tests/Feature/Billing/LegacySettleUnpaidCommandTest.php +++ b/tests/Feature/Billing/LegacySettleUnpaidCommandTest.php @@ -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); }); diff --git a/tests/Feature/ConversionBaselineReportsTest.php b/tests/Feature/ConversionBaselineReportsTest.php index a8c249dc..c5ae69d1 100644 --- a/tests/Feature/ConversionBaselineReportsTest.php +++ b/tests/Feature/ConversionBaselineReportsTest.php @@ -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)]);