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:
parent
eb658020e5
commit
1c38c820f4
9 changed files with 392 additions and 21 deletions
|
|
@ -111,7 +111,7 @@ class ReportPressReleaseViews extends Command
|
|||
}
|
||||
|
||||
if ($kohorten !== []) {
|
||||
$this->line(' <comment>Aufrufe nach Alter (nur Meldungen seit dem Relaunch)</comment>');
|
||||
$this->line(" <comment>Aufrufe nach Alter am Stichtag {$data['stichtag']} (nur Meldungen seit dem Relaunch)</comment>");
|
||||
$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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,12 +53,14 @@ class ViewDistributionReport
|
|||
* @return array{
|
||||
* from: string,
|
||||
* to: string,
|
||||
* stichtag: string,
|
||||
* portals: list<array{portal: string, label: string, segments: list<array{segment: string, stats: array{anzahl: int, min?: int, p10?: int, p25?: int, median?: int, p75?: int, p90?: int, max?: int, mittelwert?: float, verteilung?: array<string, int>}}>, cohorts: list<array{alter: string, anzahl: int, median: int|null, mittelwert: float|null}>}>,
|
||||
* }
|
||||
*/
|
||||
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<array{alter: string, anzahl: int, median: int|null, mittelwert: float|null}>
|
||||
*/
|
||||
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[] = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue