Promote develop → main (2026-07-25 23:31 UTC) #53
@@ -61,11 +61,9 @@ Der Wechsel passiert träge bei der nächsten Lizenzprüfung, nicht über einen
|
|||||||
Zeitplan. Ein verpasster Monatswechsel wird dadurch beim nächsten Zugriff
|
Zeitplan. Ein verpasster Monatswechsel wird dadurch beim nächsten Zugriff
|
||||||
nachgeholt, und ohne Nutzung wird ohnehin nichts verbraucht.
|
nachgeholt, und ohne Nutzung wird ohnehin nichts verbraucht.
|
||||||
|
|
||||||
`credits_rollover` überträgt ungenutzte Credits in den Folgemonat, gedeckelt
|
Ungenutzte Credits verfallen zum Periodenende. Ein Übertrag in den Folgemonat
|
||||||
auf ein Monatskontingent. Der Übertrag steht in `credits_carried` und zählt beim
|
war kurz vorgesehen und wurde als Produktentscheidung 07/2026 wieder entfernt,
|
||||||
Hard-Stop mit. Das ist empfehlenswert, weil OSINT-Nutzung ereignisgetrieben ist.
|
es gilt der harte Monatsdeckel wie verkauft.
|
||||||
Bricht eine Krise aus, braucht der Kunde in einer Woche mehr als sonst im
|
|
||||||
Quartal, und genau dann darf das System nicht dichtmachen.
|
|
||||||
|
|
||||||
Eine Bestandslizenz ohne Periodenmarke bekommt beim ersten Zugriff die aktuelle
|
Eine Bestandslizenz ohne Periodenmarke bekommt beim ersten Zugriff die aktuelle
|
||||||
Periode eingetragen, **ohne** den Verbrauch zurückzusetzen. Sonst würden ihr
|
Periode eingetragen, **ohne** den Verbrauch zurückzusetzen. Sonst würden ihr
|
||||||
@@ -93,7 +91,6 @@ nächste Schritt.
|
|||||||
| `credits_total` | Kontingent je Periode | 10000 |
|
| `credits_total` | Kontingent je Periode | 10000 |
|
||||||
| `cost_per_credit` | nur für `BILLING_MODE=actual` und die interne Rechnung | 0.20 |
|
| `cost_per_credit` | nur für `BILLING_MODE=actual` und die interne Rechnung | 0.20 |
|
||||||
| `credits_period` | `monthly` oder `total` | monthly |
|
| `credits_period` | `monthly` oder `total` | monthly |
|
||||||
| `credits_rollover` | Übertrag erlauben | 1 |
|
|
||||||
| `budget_warning_percent` | Warnschwelle in Prozent | 80 |
|
| `budget_warning_percent` | Warnschwelle in Prozent | 80 |
|
||||||
| `unlimited_budget` | Kontingent aushebeln | 0 |
|
| `unlimited_budget` | Kontingent aushebeln | 0 |
|
||||||
|
|
||||||
|
|||||||
@@ -877,10 +877,9 @@ async def init_db():
|
|||||||
# Beginn der laufenden Periode als YYYY-MM. Leer = beim naechsten
|
# Beginn der laufenden Periode als YYYY-MM. Leer = beim naechsten
|
||||||
# Zugriff auf den aktuellen Monat gesetzt, ohne Verbrauch zu loeschen.
|
# Zugriff auf den aktuellen Monat gesetzt, ohne Verbrauch zu loeschen.
|
||||||
await db.execute("ALTER TABLE licenses ADD COLUMN credits_period_start TEXT")
|
await db.execute("ALTER TABLE licenses ADD COLUMN credits_period_start TEXT")
|
||||||
# Ungenutzte Credits in die Folgeperiode uebertragen. Aus = harter
|
# Ungenutzte Credits verfallen zum Periodenende. Ein Uebertrag in den
|
||||||
# Monatsdeckel wie verkauft, An = faengt Krisenspitzen ab.
|
# Folgemonat war kurz vorgesehen und wurde als Produktentscheidung
|
||||||
await db.execute("ALTER TABLE licenses ADD COLUMN credits_rollover INTEGER DEFAULT 0")
|
# 07/2026 wieder entfernt (harter Monatsdeckel wie verkauft).
|
||||||
await db.execute("ALTER TABLE licenses ADD COLUMN credits_carried REAL DEFAULT 0")
|
|
||||||
# Verhindert, dass die Warnschwelle bei jeder Buchung erneut meldet.
|
# Verhindert, dass die Warnschwelle bei jeder Buchung erneut meldet.
|
||||||
await db.execute("ALTER TABLE licenses ADD COLUMN budget_warning_sent INTEGER DEFAULT 0")
|
await db.execute("ALTER TABLE licenses ADD COLUMN budget_warning_sent INTEGER DEFAULT 0")
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ async def roll_credit_period(db: aiosqlite.Connection, lic: dict) -> dict:
|
|||||||
Das ist robuster, weil ein verpasster Monatswechsel beim naechsten Zugriff
|
Das ist robuster, weil ein verpasster Monatswechsel beim naechsten Zugriff
|
||||||
ohnehin nachgeholt wird und ohne Nutzung auch nichts verbraucht wird.
|
ohnehin nachgeholt wird und ohne Nutzung auch nichts verbraucht wird.
|
||||||
|
|
||||||
Bei aktivem Uebertrag wandert ungenutztes Guthaben in die Folgeperiode,
|
Ungenutzte Credits verfallen zum Periodenende (kein Uebertrag,
|
||||||
gedeckelt auf ein Monatskontingent, damit es nicht unbegrenzt anwaechst.
|
Produktentscheidung 07/2026).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Das ggf. aktualisierte Lizenz-dict (in-place ergaenzt).
|
Das ggf. aktualisierte Lizenz-dict (in-place ergaenzt).
|
||||||
@@ -70,33 +70,21 @@ async def roll_credit_period(db: aiosqlite.Connection, lic: dict) -> dict:
|
|||||||
if started == period:
|
if started == period:
|
||||||
return lic
|
return lic
|
||||||
|
|
||||||
total = lic.get("credits_total") or 0
|
|
||||||
carried_old = lic.get("credits_carried") or 0
|
|
||||||
used = lic.get("credits_used") or 0
|
|
||||||
|
|
||||||
if lic.get("credits_rollover"):
|
|
||||||
carried_new = max(0.0, (total + carried_old) - used)
|
|
||||||
carried_new = min(carried_new, float(total)) # hoechstens ein Monat
|
|
||||||
else:
|
|
||||||
carried_new = 0.0
|
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""UPDATE licenses
|
"""UPDATE licenses
|
||||||
SET credits_used = 0, credits_carried = ?, credits_period_start = ?,
|
SET credits_used = 0, credits_period_start = ?,
|
||||||
budget_warning_sent = 0
|
budget_warning_sent = 0
|
||||||
WHERE id = ?""",
|
WHERE id = ?""",
|
||||||
(round(carried_new, 2), period, lic["id"]),
|
(period, lic["id"]),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
lic["credits_used"] = 0
|
lic["credits_used"] = 0
|
||||||
lic["credits_carried"] = carried_new
|
|
||||||
lic["credits_period_start"] = period
|
lic["credits_period_start"] = period
|
||||||
lic["budget_warning_sent"] = 0
|
lic["budget_warning_sent"] = 0
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Lizenz {lic['id']}: neue Periode {period}, Verbrauch zurueckgesetzt"
|
f"Lizenz {lic['id']}: neue Periode {period}, Verbrauch zurueckgesetzt"
|
||||||
f"(Uebertrag {round(carried_new, 2)} Einheiten)"
|
|
||||||
)
|
)
|
||||||
return lic
|
return lic
|
||||||
|
|
||||||
@@ -185,10 +173,10 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
|
|||||||
unlimited_budget = bool(lic_dict.get("unlimited_budget"))
|
unlimited_budget = bool(lic_dict.get("unlimited_budget"))
|
||||||
credits_total = lic_dict.get("credits_total")
|
credits_total = lic_dict.get("credits_total")
|
||||||
credits_used = lic_dict.get("credits_used") or 0
|
credits_used = lic_dict.get("credits_used") or 0
|
||||||
credits_carried = lic_dict.get("credits_carried") or 0
|
|
||||||
credits_period = lic_dict.get("credits_period") or "monthly"
|
credits_period = lic_dict.get("credits_period") or "monthly"
|
||||||
# Verfuegbar ist das Kontingent plus ein etwaiger Uebertrag aus dem Vormonat.
|
# Verfuegbar ist genau das Kontingent. Ungenutzte Credits verfallen zum
|
||||||
credits_available = (credits_total or 0) + credits_carried
|
# Periodenende (kein Uebertrag, Produktentscheidung 07/2026).
|
||||||
|
credits_available = credits_total or 0
|
||||||
|
|
||||||
# STAGING_MODE: kein Token-Budget-Hard-Stop, immer unlimited
|
# STAGING_MODE: kein Token-Budget-Hard-Stop, immer unlimited
|
||||||
if _staging_mode():
|
if _staging_mode():
|
||||||
@@ -219,7 +207,6 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Budget-Check (Hard-Stop bei aufgebrauchtem Guthaben, ausser unlimited).
|
# Budget-Check (Hard-Stop bei aufgebrauchtem Guthaben, ausser unlimited).
|
||||||
# Geprueft wird gegen das verfuegbare Guthaben, also inklusive Uebertrag.
|
|
||||||
budget_exceeded = False
|
budget_exceeded = False
|
||||||
if not unlimited_budget and credits_available > 0:
|
if not unlimited_budget and credits_available > 0:
|
||||||
if credits_used >= credits_available:
|
if credits_used >= credits_available:
|
||||||
@@ -245,7 +232,6 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
|
|||||||
"unlimited_budget": False,
|
"unlimited_budget": False,
|
||||||
"credits_total": credits_total,
|
"credits_total": credits_total,
|
||||||
"credits_used": credits_used,
|
"credits_used": credits_used,
|
||||||
"credits_carried": credits_carried,
|
|
||||||
"credits_available": credits_available,
|
"credits_available": credits_available,
|
||||||
"credits_period": credits_period,
|
"credits_period": credits_period,
|
||||||
}
|
}
|
||||||
@@ -262,7 +248,6 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
|
|||||||
"unlimited_budget": unlimited_budget,
|
"unlimited_budget": unlimited_budget,
|
||||||
"credits_total": credits_total,
|
"credits_total": credits_total,
|
||||||
"credits_used": credits_used,
|
"credits_used": credits_used,
|
||||||
"credits_carried": credits_carried,
|
|
||||||
"credits_available": credits_available,
|
"credits_available": credits_available,
|
||||||
"credits_period": credits_period,
|
"credits_period": credits_period,
|
||||||
}
|
}
|
||||||
@@ -437,7 +422,7 @@ async def _charge_credits(
|
|||||||
)
|
)
|
||||||
|
|
||||||
used_new = (lic.get("credits_used") or 0) + credits_consumed
|
used_new = (lic.get("credits_used") or 0) + credits_consumed
|
||||||
available = (lic.get("credits_total") or 0) + (lic.get("credits_carried") or 0)
|
available = lic.get("credits_total") or 0
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"charge_usage_to_tenant[{key}] Tenant {tenant_id}: "
|
f"charge_usage_to_tenant[{key}] Tenant {tenant_id}: "
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren