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:
@@ -22,10 +22,36 @@ class TokenResponse(BaseModel):
|
||||
email: str = ""
|
||||
|
||||
|
||||
class LicenseParams(BaseModel):
|
||||
"""Stellwerte einer Lizenz ohne Org-Bezug.
|
||||
|
||||
Wird zweifach genutzt, direkt beim Lizenz-Anlegen (LicenseCreate) und
|
||||
verschachtelt beim Org-Anlegen (OrgCreate.license). Die Laufzeitfelder
|
||||
(credits_used, credits_carried, credits_period_start, budget_warning_sent)
|
||||
verwaltet der Monitor, das Portal setzt nur Stellwerte.
|
||||
"""
|
||||
license_type: str = Field(pattern="^(trial|annual|permanent)$")
|
||||
max_users: int = Field(default=5, ge=1, le=1000)
|
||||
duration_days: Optional[int] = Field(default=None, ge=1, le=3650)
|
||||
token_budget_usd: Optional[float] = None
|
||||
credits_total: Optional[int] = None
|
||||
cost_per_credit: Optional[float] = None
|
||||
budget_warning_percent: Optional[int] = Field(default=80, ge=1, le=100)
|
||||
unlimited_budget: bool = False
|
||||
# 'monthly' = Kontingent wird zum Monatswechsel neu gefuellt (der Monitor
|
||||
# setzt den Verbrauch traege zurueck), 'total' = gilt fuer die Laufzeit.
|
||||
credits_period: str = Field(default="monthly", pattern="^(monthly|total)$")
|
||||
# Uebertrag ungenutzter Credits in den Folgemonat, der Monitor deckelt
|
||||
# auf ein Monatskontingent. Empfohlen an, faengt Krisenspitzen ab.
|
||||
credits_rollover: bool = True
|
||||
|
||||
|
||||
class OrgCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=200)
|
||||
slug: str = Field(min_length=1, max_length=100, pattern="^[a-z0-9-]+$")
|
||||
output_language: str = Field(default="de", pattern="^(de|en)$")
|
||||
# Optional direkt eine Lizenz mit Credits-Kontingent anlegen
|
||||
license: Optional[LicenseParams] = None
|
||||
|
||||
|
||||
class OrgUpdate(BaseModel):
|
||||
@@ -48,16 +74,8 @@ class OrgResponse(BaseModel):
|
||||
output_language: str = "de"
|
||||
|
||||
|
||||
class LicenseCreate(BaseModel):
|
||||
class LicenseCreate(LicenseParams):
|
||||
organization_id: int
|
||||
license_type: str = Field(pattern="^(trial|annual|permanent)$")
|
||||
max_users: int = Field(default=5, ge=1, le=1000)
|
||||
duration_days: Optional[int] = Field(default=None, ge=1, le=3650)
|
||||
token_budget_usd: Optional[float] = None
|
||||
credits_total: Optional[int] = None
|
||||
cost_per_credit: Optional[float] = None
|
||||
budget_warning_percent: Optional[int] = Field(default=80, ge=1, le=100)
|
||||
unlimited_budget: bool = False
|
||||
|
||||
|
||||
class LicenseResponse(BaseModel):
|
||||
@@ -75,6 +93,12 @@ class LicenseResponse(BaseModel):
|
||||
cost_per_credit: Optional[float] = None
|
||||
budget_warning_percent: Optional[int] = None
|
||||
unlimited_budget: bool = False
|
||||
# Neue Kontingent-Felder, alle optional mit Default, damit Responses auch
|
||||
# von einer noch nicht migrierten Datenbank validieren.
|
||||
credits_period: Optional[str] = "monthly"
|
||||
credits_rollover: Optional[bool] = False
|
||||
credits_carried: Optional[float] = 0
|
||||
credits_period_start: Optional[str] = None
|
||||
created_at: str
|
||||
globe_access: bool = False
|
||||
network_access: bool = False
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren