# 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="Warning threshold as percentage", 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