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:
@@ -4,7 +4,7 @@ from pydantic import ValidationError
|
||||
from models import (
|
||||
MagicLinkRequest, MagicLinkResponse,
|
||||
VerifyTokenRequest, TokenResponse,
|
||||
OrgCreate, LicenseCreate, UserCreate,
|
||||
OrgCreate, LicenseCreate, LicenseParams, LicenseResponse, UserCreate,
|
||||
)
|
||||
|
||||
|
||||
@@ -46,6 +46,58 @@ def test_license_create_type_pattern():
|
||||
LicenseCreate(organization_id=1, license_type="lifetime")
|
||||
|
||||
|
||||
def test_license_params_credits_defaults():
|
||||
"""Monatskontingent mit Übertrag ist die Voreinstellung."""
|
||||
p = LicenseParams(license_type="trial")
|
||||
assert p.credits_period == "monthly"
|
||||
assert p.credits_rollover is True
|
||||
assert p.budget_warning_percent == 80
|
||||
|
||||
|
||||
def test_license_params_period_pattern():
|
||||
LicenseParams(license_type="trial", credits_period="monthly")
|
||||
LicenseParams(license_type="trial", credits_period="total")
|
||||
with pytest.raises(ValidationError):
|
||||
LicenseParams(license_type="trial", credits_period="weekly")
|
||||
|
||||
|
||||
def test_license_create_inherits_credits_fields():
|
||||
lic = LicenseCreate(
|
||||
organization_id=1, license_type="annual",
|
||||
credits_total=10000, credits_period="monthly", credits_rollover=False,
|
||||
)
|
||||
assert lic.credits_total == 10000
|
||||
assert lic.credits_rollover is False
|
||||
|
||||
|
||||
def test_org_create_without_license_still_valid():
|
||||
"""Rückwärtskompatibilität, das Lizenz-Objekt ist optional."""
|
||||
org = OrgCreate(name="Test", slug="test")
|
||||
assert org.license is None
|
||||
|
||||
|
||||
def test_org_create_with_nested_license():
|
||||
org = OrgCreate(
|
||||
name="Test", slug="test",
|
||||
license={"license_type": "trial", "credits_total": 10000},
|
||||
)
|
||||
assert org.license.credits_total == 10000
|
||||
assert org.license.credits_period == "monthly"
|
||||
with pytest.raises(ValidationError):
|
||||
OrgCreate(name="Test", slug="test", license={"license_type": "trial", "credits_period": "weekly"})
|
||||
|
||||
|
||||
def test_license_response_without_new_columns():
|
||||
"""Responses müssen auch von einer noch nicht migrierten DB validieren."""
|
||||
r = LicenseResponse(
|
||||
id=1, organization_id=1, license_type="trial", max_users=5,
|
||||
valid_from="2026-07-25", valid_until=None, status="active",
|
||||
notes=None, created_at="2026-07-25",
|
||||
)
|
||||
assert r.credits_period == "monthly"
|
||||
assert r.credits_carried == 0
|
||||
|
||||
|
||||
def test_user_create_role_pattern():
|
||||
UserCreate(email="a@b.de", role="member")
|
||||
UserCreate(email="a@b.de", role="org_admin")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren