feat(kontingent): Credits-Stellwerte je Lizenz + Lizenz beim Org-Anlegen

Bedienseite für das Credits-Monatskontingent des Monitors. Das Portal
setzt jetzt alle Stellwerte, der Monitor bleibt Herr der Laufzeitfelder.

- Lizenz-Modal und Budget-Formular um Abrechnungszeitraum (monthly/total),
  Übertrag-Checkbox und Warnschwelle erweitert, Vorbelegungen auf das
  beschlossene Modell (10.000 Credits, monatlich, Übertrag an, 0,20 USD).
- Neue Organisation kann direkt eine Lizenz mit Kontingent anlegen
  (OrgCreate.license, verschachtelt). Org- und Lizenz-Insert laufen in
  EINER Transaktion, schlägt der Lizenz-Teil fehl, entsteht keine
  halbe Organisation. Gemeinsamer Helper insert_license_row statt
  Duplikation der Widerruf- und Audit-Logik.
- PUT /api/token-usage/budget validiert credits_period (monthly|total)
  und budget_warning_percent (1 bis 100), credits_rollover wird wie
  unlimited_budget normalisiert. Laufzeitfelder bleiben bewusst
  außerhalb der Whitelist.
- Overview und Org-Detail rechnen den Rest jetzt gegen verfügbar =
  Kontingent + Übertrag, dieselbe Bezugsgröße wie der Hard-Stop im
  Monitor. Neuer read-only Block Laufzeitwerte im Token-Subtab.
- Startup-Check loggt einen deutlichen Fehler, wenn die licenses-Tabelle
  die Credits-Spalten noch nicht hat (falsche Deploy-Reihenfolge).
- Migration migrations/2026-07-25_credits_period.py (idempotent,
  wortgleich mit der Monitor-Migration) für die Portal-Staging-DB,
  auf Live gilt: Monitor VOR Portal promoten.
- 13 neue Modell-Tests, pytest grün (die zwei bekannten
  shared-Import-Fehlschläge bestehen unverändert seit vorher).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 13:11:02 +00:00
Ursprung c95903fb3a
Commit e0c39de21f
10 geänderte Dateien mit 566 neuen und 92 gelöschten Zeilen

Datei anzeigen

@@ -64,6 +64,16 @@ async def create_organization(
(data.name, data.slug, now, now),
)
org_id = cursor.lastrowid
# Optional direkt eine Lizenz mit Credits-Kontingent anlegen. Beide Inserts
# liegen VOR dem Commit, schlaegt der Lizenz-Teil fehl (z.B. fehlende
# Spalten), entsteht keine halb angelegte Organisation.
lic_id = None
revoked_lics: list = []
if data.license is not None:
from routers.licenses import insert_license_row
lic_id, revoked_lics = await insert_license_row(db, org_id, data.license)
await db.commit()
# output_language als organization_settings-Eintrag persistieren
@@ -77,6 +87,11 @@ async def create_organization(
action="create", resource_type="organization", resource_id=org_id,
after=dict(new_row_obj),
)
if lic_id is not None:
from routers.licenses import audit_license_creation
await audit_license_creation(
db, admin, get_client_ip(request), lic_id, revoked_lics
)
return await _enrich_org(db, new_row_obj)