feat(abrechnung): pflegbare Preistabelle billing_tariff als Quelle der Sätze
Die Credits-Sätze je Aktion stehen jetzt in der geteilten Tabelle billing_tariff statt nur in der Konfiguration. Die Migration legt die Tabelle an und befüllt sie einmalig mit den wirksamen CREDIT_TARIFF- Werten, danach liest _charge_credits() bei jeder Buchung die Tabelle, fehlende Schlüssel oder eine fehlende Tabelle fallen auf die Konfiguration zurück. Damit können Monitor und Verwaltungsportal dieselben Sätze anzeigen und das Portal kann sie künftig pflegen, eine Preisänderung braucht keinen Server-Eingriff mehr. Getestet gegen eine Kopie der Staging-DB. Seed mit 7 Sätzen (45/40/ 12/12/1/1/1), Buchung nach Tabelle, Tabellenänderung greift sofort, Rückfall auf die Konfiguration ohne Tabelle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -15,9 +15,12 @@ einheitlich in Monitor und Verwaltungsportal).
|
|||||||
|
|
||||||
## Was eine Aktion kostet
|
## Was eine Aktion kostet
|
||||||
|
|
||||||
Im Modus `flat` (Voreinstellung) kostet jede Aktion einen festen Satz aus
|
Im Modus `flat` (Voreinstellung) kostet jede Aktion einen festen Satz,
|
||||||
`CREDIT_TARIFF` in `src/config.py`, unabhängig davon, was sie uns tatsächlich
|
unabhängig davon, was sie uns tatsächlich verursacht hat. 1 Credit entspricht
|
||||||
verursacht hat. 1 Credit entspricht 0,20 USD.
|
0,20 USD. Die wirksamen Sätze stehen seit 25.07.2026 in der geteilten Tabelle
|
||||||
|
`billing_tariff` und sind über den Verbrauchsrechner des Verwaltungsportals
|
||||||
|
pflegbar. `CREDIT_TARIFF` in `src/config.py` befüllt die Tabelle beim ersten
|
||||||
|
Start und bleibt Rückfallebene für fehlende Schlüssel.
|
||||||
|
|
||||||
| Aktion | Credits | Herkunft des Werts |
|
| Aktion | Credits | Herkunft des Werts |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -138,8 +141,6 @@ Zur Orientierung bei einem Kontingent von 10.000 Credits und 45 Credits je Lauf.
|
|||||||
## Offene Punkte
|
## Offene Punkte
|
||||||
|
|
||||||
- E-Mail-Versand der Budget-Warnung.
|
- E-Mail-Versand der Budget-Warnung.
|
||||||
- Eingabefelder für die Stellwerte im Verwaltungsportal (heute erzeugt das
|
|
||||||
Anlegen einer Organisation dort nicht einmal eine Lizenz).
|
|
||||||
- Takt-Untergrenze und Kostenvorschau aus dem Lokal-Fork portieren (siehe oben).
|
- Takt-Untergrenze und Kostenvorschau aus dem Lokal-Fork portieren (siehe oben).
|
||||||
- Nachkaufpakete. Ohne sie ist der Deckel eine Sackgasse, mit ihnen eine
|
- Nachkaufpakete. Ohne sie ist der Deckel eine Sackgasse, mit ihnen eine
|
||||||
Umsatzquelle. Der Verkauf gehört ins Verwaltungsportal, im Monitor müsste nur
|
Umsatzquelle. Der Verkauf gehört ins Verwaltungsportal, im Monitor müsste nur
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import aiosqlite
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from config import DB_PATH, DATA_DIR
|
from config import DB_PATH, DATA_DIR, CREDIT_TARIFF
|
||||||
|
|
||||||
logger = logging.getLogger("osint.database")
|
logger = logging.getLogger("osint.database")
|
||||||
|
|
||||||
@@ -896,6 +896,30 @@ async def init_db():
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
logger.info("Migration: unlimited_budget zu Lizenzen hinzugefuegt")
|
logger.info("Migration: unlimited_budget zu Lizenzen hinzugefuegt")
|
||||||
|
|
||||||
|
# Migration: Preistabelle fuer die Credits-Saetze. Pflegbare Quelle der
|
||||||
|
# Wahrheit fuer Monitor UND Verwaltungsportal (das Portal schreibt sie
|
||||||
|
# ueber den Verbrauchsrechner). Beim ersten Start mit den wirksamen
|
||||||
|
# Konfigurationswerten befuellt, danach gewinnt die Tabelle,
|
||||||
|
# CREDIT_TARIFF bleibt Rueckfallebene fuer fehlende Schluessel.
|
||||||
|
cursor = await db.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='billing_tariff'"
|
||||||
|
)
|
||||||
|
if not await cursor.fetchone():
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE billing_tariff (
|
||||||
|
tariff_key TEXT PRIMARY KEY,
|
||||||
|
credits REAL NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
for tariff_key, credits in CREDIT_TARIFF.items():
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO billing_tariff (tariff_key, credits) VALUES (?, ?)",
|
||||||
|
(tariff_key, float(credits)),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
logger.info("Migration: billing_tariff angelegt und mit Konfigurationswerten befuellt")
|
||||||
|
|
||||||
# Migration: Token-Usage-Monatstabelle
|
# Migration: Token-Usage-Monatstabelle
|
||||||
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='token_usage_monthly'")
|
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='token_usage_monthly'")
|
||||||
if not await cursor.fetchone():
|
if not await cursor.fetchone():
|
||||||
|
|||||||
@@ -371,6 +371,24 @@ async def _record_usage_statistics(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_tariff(db: aiosqlite.Connection) -> dict:
|
||||||
|
"""Wirksame Credits-Saetze je Aktion.
|
||||||
|
|
||||||
|
Die Tabelle billing_tariff ist die pflegbare Quelle (das Verwaltungsportal
|
||||||
|
schreibt sie), CREDIT_TARIFF aus der Konfiguration bleibt Rueckfallebene
|
||||||
|
fuer fehlende Schluessel oder eine noch fehlende Tabelle.
|
||||||
|
"""
|
||||||
|
tariff = dict(CREDIT_TARIFF)
|
||||||
|
try:
|
||||||
|
cursor = await db.execute("SELECT tariff_key, credits FROM billing_tariff")
|
||||||
|
for row in await cursor.fetchall():
|
||||||
|
if row["credits"] is not None and row["credits"] > 0:
|
||||||
|
tariff[row["tariff_key"]] = float(row["credits"])
|
||||||
|
except Exception:
|
||||||
|
pass # Tabelle existiert noch nicht, die Konfiguration gilt
|
||||||
|
return tariff
|
||||||
|
|
||||||
|
|
||||||
async def _charge_credits(
|
async def _charge_credits(
|
||||||
db: aiosqlite.Connection,
|
db: aiosqlite.Connection,
|
||||||
tenant_id: int,
|
tenant_id: int,
|
||||||
@@ -398,7 +416,8 @@ async def _charge_credits(
|
|||||||
key = _tariff_key(source, incident_type)
|
key = _tariff_key(source, incident_type)
|
||||||
|
|
||||||
if BILLING_MODE == "flat":
|
if BILLING_MODE == "flat":
|
||||||
credits_consumed = CREDIT_TARIFF.get(key)
|
tariff = await _load_tariff(db)
|
||||||
|
credits_consumed = tariff.get(key)
|
||||||
if credits_consumed is None:
|
if credits_consumed is None:
|
||||||
# Unbekannte Quelle. Lieber auf die echte Rechnung zurueckfallen als
|
# Unbekannte Quelle. Lieber auf die echte Rechnung zurueckfallen als
|
||||||
# stillschweigend gratis abzugeben.
|
# stillschweigend gratis abzugeben.
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren