gateway/modules/datamodels/datamodelBilling.py
2026-04-17 11:50:24 +02:00

278 lines
12 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Billing models: BillingAccount, BillingTransaction, BillingSettings, UsageStatistics."""
from typing import List, Dict, Any, Optional
from enum import Enum
from datetime import date, datetime, timezone
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
from modules.shared.i18nRegistry import i18nModel
import uuid
# End-customer price for storage above plan-included volume (CHF per GB per month).
STORAGE_PRICE_PER_GB_CHF = 0.50
class TransactionTypeEnum(str, Enum):
"""Transaction types for billing."""
CREDIT = "CREDIT" # Credit/top-up (positive)
DEBIT = "DEBIT" # Debit/usage (positive amount, reduces balance)
ADJUSTMENT = "ADJUSTMENT" # Manual adjustment by admin
class ReferenceTypeEnum(str, Enum):
"""Reference types for transactions."""
WORKFLOW = "WORKFLOW" # AI workflow usage
PAYMENT = "PAYMENT" # Payment/top-up
ADMIN = "ADMIN" # Admin adjustment
SYSTEM = "SYSTEM" # System credit (e.g., initial credit)
STORAGE = "STORAGE" # Metered storage overage (prepay pool)
SUBSCRIPTION = "SUBSCRIPTION" # AI budget credit from subscription plan
class PeriodTypeEnum(str, Enum):
"""Period types for usage statistics."""
DAY = "DAY"
MONTH = "MONTH"
YEAR = "YEAR"
@i18nModel("Abrechnungskonto")
class BillingAccount(PowerOnModel):
"""Billing account for mandate or user-mandate combination."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
json_schema_extra={"label": "ID"},
)
mandateId: str = Field(
...,
description="Foreign key to Mandate",
json_schema_extra={"label": "Mandanten-ID", "fk_target": {"db": "poweron_app", "table": "Mandate"}},
)
userId: Optional[str] = Field(
None,
description="Foreign key to User (None = mandate pool account, set = user audit account)",
json_schema_extra={"label": "Benutzer-ID", "fk_target": {"db": "poweron_app", "table": "User"}},
)
balance: float = Field(default=0.0, description="Current balance in CHF", json_schema_extra={"label": "Guthaben (CHF)"})
warningThreshold: float = Field(
default=0.0,
description="Warning threshold in CHF",
json_schema_extra={"label": "Warnschwelle (CHF)"},
)
lastWarningAt: Optional[datetime] = Field(
None,
description="Last warning sent timestamp",
json_schema_extra={"label": "Letzte Warnung"},
)
enabled: bool = Field(default=True, description="Account is active", json_schema_extra={"label": "Aktiv"})
@i18nModel("Transaktion")
class BillingTransaction(PowerOnModel):
"""Single billing transaction (credit, debit, adjustment)."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
json_schema_extra={"label": "ID"},
)
accountId: str = Field(
...,
description="Foreign key to BillingAccount",
json_schema_extra={"label": "Konto-ID", "fk_target": {"db": "poweron_billing", "table": "BillingAccount"}},
)
transactionType: TransactionTypeEnum = Field(..., description="Transaction type", json_schema_extra={"label": "Typ"})
amount: float = Field(..., description="Amount in CHF (always positive)", json_schema_extra={"label": "Betrag (CHF)"})
description: str = Field(..., description="Transaction description", json_schema_extra={"label": "Beschreibung"})
# Reference to source
referenceType: Optional[ReferenceTypeEnum] = Field(None, description="Reference type", json_schema_extra={"label": "Referenztyp"})
referenceId: Optional[str] = Field(None, description="Reference ID", json_schema_extra={"label": "Referenz-ID"})
# Context for workflow transactions
workflowId: Optional[str] = Field(
None,
description="Workflow ID (for WORKFLOW transactions; may be Chat or Graphical Editor)",
json_schema_extra={"label": "Workflow-ID"},
)
featureInstanceId: Optional[str] = Field(
None,
description="Feature instance ID",
json_schema_extra={"label": "Feature-Instanz-ID", "fk_target": {"db": "poweron_app", "table": "FeatureInstance"}},
)
featureCode: Optional[str] = Field(
None,
description="Feature code (e.g., automation)",
json_schema_extra={"label": "Feature-Code", "fk_target": {"db": "poweron_app", "table": "Feature", "column": "code"}},
)
aicoreProvider: Optional[str] = Field(None, description="AICore provider (anthropic, openai, etc.)", json_schema_extra={"label": "AI-Anbieter"})
aicoreModel: Optional[str] = Field(None, description="AICore model name (e.g., claude-4-sonnet, gpt-4o)", json_schema_extra={"label": "AI-Modell"})
createdByUserId: Optional[str] = Field(
None,
description="User who created/caused this transaction",
json_schema_extra={"label": "Erstellt von Benutzer", "fk_target": {"db": "poweron_app", "table": "User"}},
)
# AI call metadata (for per-call analytics)
processingTime: Optional[float] = Field(None, description="Processing time in seconds", json_schema_extra={"label": "Verarbeitungszeit (s)"})
bytesSent: Optional[int] = Field(None, description="Bytes sent to AI model", json_schema_extra={"label": "Gesendete Bytes"})
bytesReceived: Optional[int] = Field(None, description="Bytes received from AI model", json_schema_extra={"label": "Empfangene Bytes"})
errorCount: Optional[int] = Field(None, description="Number of errors in this call", json_schema_extra={"label": "Fehleranzahl"})
@i18nModel("Abrechnungseinstellungen")
class BillingSettings(BaseModel):
"""Billing settings per mandate. Only PREPAY_MANDATE model."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
json_schema_extra={"label": "ID"},
)
mandateId: str = Field(
...,
description="Foreign key to Mandate (UNIQUE)",
json_schema_extra={"label": "Mandanten-ID", "fk_target": {"db": "poweron_app", "table": "Mandate"}},
)
warningThresholdPercent: float = Field(
default=10.0,
description="Benachrichtigung wenn das AI-Guthaben unter diesen Prozentsatz des Gesamtbudgets fällt",
json_schema_extra={"label": "Warnschwelle (%)"},
)
# Stripe
stripeCustomerId: Optional[str] = Field(
None,
description="Stripe Customer ID (cus_xxx) — one per mandate",
json_schema_extra={"label": "Stripe-Kunden-ID"},
)
# Auto-Recharge for AI budget
autoRechargeEnabled: bool = Field(default=False, description="Auto-buy AI budget when low", json_schema_extra={"label": "Auto-Nachladung"})
rechargeAmountCHF: float = Field(
default=10.0,
description="Amount per auto-recharge (CHF, prepaid via Stripe)",
json_schema_extra={"label": "Nachladebetrag (CHF)"},
)
rechargeMaxPerMonth: int = Field(default=3, description="Max auto-recharges per month", json_schema_extra={"label": "Max. Nachladungen/Monat"})
rechargesThisMonth: int = Field(default=0, description="Counter: auto-recharges used this month", json_schema_extra={"label": "Nachladungen diesen Monat"})
monthResetAt: Optional[datetime] = Field(None, description="When rechargesThisMonth was last reset", json_schema_extra={"label": "Monats-Reset"})
# Notifications
notifyEmails: List[str] = Field(
default_factory=list,
description="Email addresses for billing alerts (pool exhausted, warnings, etc.)",
json_schema_extra={"label": "E-Mails fuer Billing-Alerts (Inhaber/Admin)"},
)
notifyOnWarning: bool = Field(default=True, description="Send email when warning threshold is reached", json_schema_extra={"label": "Bei Warnung benachrichtigen"})
# Storage overage (high-watermark within subscription period; resets on new period)
storageHighWatermarkMB: float = Field(
default=0.0,
description="Peak indexed data volume MB this billing period",
json_schema_extra={"label": "Speicher-Peak (MB)"},
)
storagePeriodStartAt: Optional[datetime] = Field(
None,
description="Subscription billing period start used for storage reset",
json_schema_extra={"label": "Speicher-Periodenbeginn"},
)
storageBilledUpToMB: float = Field(
default=0.0,
description="Overage MB already debited this period (above plan-included volume)",
json_schema_extra={"label": "Speicher abgerechneter Überhang (MB)"},
)
class StripeWebhookEvent(BaseModel):
"""Stores processed Stripe webhook event IDs for idempotency."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
)
event_id: str = Field(..., description="Stripe event ID (evt_xxx)")
processed_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
description="When the event was processed",
)
@i18nModel("Nutzungsstatistik")
class UsageStatistics(BaseModel):
"""Aggregated usage statistics for quick retrieval."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
json_schema_extra={"label": "ID"},
)
accountId: str = Field(
...,
description="Foreign key to BillingAccount",
json_schema_extra={"label": "Konto-ID", "fk_target": {"db": "poweron_billing", "table": "BillingAccount"}},
)
periodType: PeriodTypeEnum = Field(..., description="Period type", json_schema_extra={"label": "Periodentyp"})
periodStart: date = Field(..., description="Period start date", json_schema_extra={"label": "Periodenbeginn"})
# Aggregated values
totalCostCHF: float = Field(default=0.0, description="Total cost in CHF", json_schema_extra={"label": "Gesamtkosten (CHF)"})
transactionCount: int = Field(default=0, description="Number of transactions", json_schema_extra={"label": "Anzahl Transaktionen"})
# Breakdown by provider
costByProvider: Dict[str, float] = Field(
default_factory=dict,
description="Cost breakdown by provider (e.g., {'anthropic': 12.50, 'openai': 8.30})",
json_schema_extra={"label": "Kosten nach Anbieter"},
)
# Breakdown by feature
costByFeature: Dict[str, float] = Field(
default_factory=dict,
description="Cost breakdown by feature (e.g., {'automation': 5.80, 'workspace': 3.20})",
json_schema_extra={"label": "Kosten nach Feature"},
)
# ============================================================================
# Response Models for API
# ============================================================================
class BillingBalanceResponse(BaseModel):
"""Response model for balance endpoint."""
mandateId: str
mandateName: str
balance: float
currency: str = "CHF"
warningThreshold: float
isWarning: bool
class BillingStatisticsChartData(BaseModel):
"""Chart data point for statistics."""
label: str
totalCost: float
byProvider: Dict[str, float]
class BillingStatisticsResponse(BaseModel):
"""Response model for statistics endpoint."""
mandateId: str
period: PeriodTypeEnum
year: int
month: Optional[int] = None
currency: str = "CHF"
data: List[BillingStatisticsChartData]
totals: Dict[str, Any]
class BillingCheckResult(BaseModel):
"""Result of a billing balance check (budget + subscription gate)."""
allowed: bool
reason: Optional[str] = None
currentBalance: Optional[float] = None
requiredAmount: Optional[float] = None
upgradeRequired: Optional[bool] = None
subscriptionUiPath: Optional[str] = None
userAction: Optional[str] = None