diff --git a/docs/ABRECHNUNG.md b/docs/ABRECHNUNG.md index 5658de5..3ef161d 100644 --- a/docs/ABRECHNUNG.md +++ b/docs/ABRECHNUNG.md @@ -15,9 +15,12 @@ einheitlich in Monitor und Verwaltungsportal). ## Was eine Aktion kostet -Im Modus `flat` (Voreinstellung) kostet jede Aktion einen festen Satz aus -`CREDIT_TARIFF` in `src/config.py`, unabhängig davon, was sie uns tatsächlich -verursacht hat. 1 Credit entspricht 0,20 USD. +Im Modus `flat` (Voreinstellung) kostet jede Aktion einen festen Satz, +unabhängig davon, was sie uns tatsächlich verursacht hat. 1 Credit entspricht +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 | |---|---|---| @@ -138,8 +141,6 @@ Zur Orientierung bei einem Kontingent von 10.000 Credits und 45 Credits je Lauf. ## Offene Punkte - 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). - Nachkaufpakete. Ohne sie ist der Deckel eine Sackgasse, mit ihnen eine Umsatzquelle. Der Verkauf gehört ins Verwaltungsportal, im Monitor müsste nur diff --git a/src/database.py b/src/database.py index 16878d9..1cc0592 100644 --- a/src/database.py +++ b/src/database.py @@ -3,7 +3,7 @@ import aiosqlite import json import logging import os -from config import DB_PATH, DATA_DIR +from config import DB_PATH, DATA_DIR, CREDIT_TARIFF logger = logging.getLogger("osint.database") @@ -896,6 +896,30 @@ async def init_db(): await db.commit() 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 cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='token_usage_monthly'") if not await cursor.fetchone(): diff --git a/src/services/license_service.py b/src/services/license_service.py index 208df9c..2e8070e 100644 --- a/src/services/license_service.py +++ b/src/services/license_service.py @@ -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( db: aiosqlite.Connection, tenant_id: int, @@ -398,7 +416,8 @@ async def _charge_credits( key = _tariff_key(source, incident_type) 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: # Unbekannte Quelle. Lieber auf die echte Rechnung zurueckfallen als # stillschweigend gratis abzugeben.