ki/quota: Hash-Bindung atomar bis in den Publish-Lock, roter Re-Check lehnt ab, Kontingent-Altbestand (Block 7, U02–U04)

- Jobs speichern Ergebnis nur per bedingtem UPDATE auf den analysierten Hash;
  publish() prüft bei ki/scheduler unter der Zeilensperre die jüngste gültige
  Klassifikation und plant sonst eine neue Prüfung ein (StaleClassificationException)
- Re-Check ohne Routing: Rot bei einer PM in Review lehnt wie die Erstprüfung ab
- consumePublishSlot() zählt eine NULL-Periode wie den laufenden Monat;
  Backfill-Migration setzt die Periode für Altverbrauch
- ApiPortalResolutionTest: gemeinsame Kategorie wegen der U10-Prüfung

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-18 12:22:22 +00:00
parent eb95b3b830
commit 082e6bb76c
11 changed files with 698 additions and 25 deletions

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
/**
* Backfill (DML, getrennt vom DDL): Altverbrauch dem Migrationsmonat
* zuordnen (Audit U04, Nachtrag zu F18).
*
* Die Spalte `press_release_quota_period` kam ohne Bestandsinitialisierung.
* Ein positiver Zähler mit Periode NULL wurde beim Lesen als verbraucht
* gewertet, beim nächsten Verbrauch aber auf 1 zurückgesetzt, und nach dem
* Monatswechsel ohne Reset-Cron blieb er ausgeschöpft. Die Lese- und
* Verbrauchslogik behandelt NULL jetzt einheitlich wie den laufenden Monat;
* dieser Backfill bindet den vorhandenen Verbrauch fest an den Monat, in dem
* die Migration läuft — der Zähler galt zu diesem Zeitpunkt schließlich als
* laufender Verbrauch. Ab dem nächsten Monatswechsel gibt die Periode ihn
* dann frei, mit oder ohne Cron.
*
* Danach trägt jede Zeile mit Verbrauch eine Periode; NULL-Zeilen sind nur
* noch solche ohne Verbrauch. Dass `press-releases:reset-monthly-quota`
* NULL-Perioden weiterhin zurücksetzt, ist damit folgenlos (nur `> 0` wird
* angefasst) und bleibt als Sicherheitsnetz für den Fall, dass der Befehl
* vor diesem Backfill läuft.
*/
return new class extends Migration
{
public function up(): void
{
DB::table('users')
->where('press_release_quota_used_this_month', '>', 0)
->whereNull('press_release_quota_period')
->update(['press_release_quota_period' => now()->format('Y-m')]);
}
public function down(): void
{
// Bewusst leer: reine Datenzuordnung; die Spalte entfernt die
// zugehörige DDL-Migration (2026_09_18_170100).
}
};