# 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.attributeUtils import registerModelLabels 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" class BillingAccount(PowerOnModel): """Billing account for mandate or user-mandate combination.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key" ) mandateId: str = Field(..., description="Foreign key to Mandate") userId: Optional[str] = Field(None, description="Foreign key to User (None = mandate pool account, set = user audit account)") balance: float = Field(default=0.0, description="Current balance in CHF") warningThreshold: float = Field(default=0.0, description="Warning threshold in CHF") lastWarningAt: Optional[datetime] = Field(None, description="Last warning sent timestamp") enabled: bool = Field(default=True, description="Account is active") registerModelLabels( "BillingAccount", {"en": "Billing Account", "de": "Abrechnungskonto"}, { "id": {"en": "ID", "de": "ID"}, "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"}, "userId": {"en": "User ID", "de": "Benutzer-ID"}, "balance": {"en": "Balance (CHF)", "de": "Guthaben (CHF)"}, "warningThreshold": {"en": "Warning Threshold (CHF)", "de": "Warnschwelle (CHF)"}, "lastWarningAt": {"en": "Last Warning", "de": "Letzte Warnung"}, "enabled": {"en": "Enabled", "de": "Aktiv"}, }, ) class BillingTransaction(PowerOnModel): """Single billing transaction (credit, debit, adjustment).""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key" ) accountId: str = Field(..., description="Foreign key to BillingAccount") transactionType: TransactionTypeEnum = Field(..., description="Transaction type") amount: float = Field(..., description="Amount in CHF (always positive)") description: str = Field(..., description="Transaction description") # Reference to source referenceType: Optional[ReferenceTypeEnum] = Field(None, description="Reference type") referenceId: Optional[str] = Field(None, description="Reference ID") # Context for workflow transactions workflowId: Optional[str] = Field(None, description="Workflow ID (for WORKFLOW transactions)") featureInstanceId: Optional[str] = Field(None, description="Feature instance ID") featureCode: Optional[str] = Field(None, description="Feature code (e.g., automation)") aicoreProvider: Optional[str] = Field(None, description="AICore provider (anthropic, openai, etc.)") aicoreModel: Optional[str] = Field(None, description="AICore model name (e.g., claude-4-sonnet, gpt-4o)") createdByUserId: Optional[str] = Field(None, description="User who created/caused this transaction") # AI call metadata (for per-call analytics) processingTime: Optional[float] = Field(None, description="Processing time in seconds") bytesSent: Optional[int] = Field(None, description="Bytes sent to AI model") bytesReceived: Optional[int] = Field(None, description="Bytes received from AI model") errorCount: Optional[int] = Field(None, description="Number of errors in this call") registerModelLabels( "BillingTransaction", {"en": "Billing Transaction", "de": "Transaktion"}, { "id": {"en": "ID", "de": "ID"}, "accountId": {"en": "Account ID", "de": "Konto-ID"}, "transactionType": {"en": "Type", "de": "Typ"}, "amount": {"en": "Amount (CHF)", "de": "Betrag (CHF)"}, "description": {"en": "Description", "de": "Beschreibung"}, "referenceType": {"en": "Reference Type", "de": "Referenztyp"}, "referenceId": {"en": "Reference ID", "de": "Referenz-ID"}, "workflowId": {"en": "Workflow ID", "de": "Workflow-ID"}, "featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID"}, "featureCode": {"en": "Feature Code", "de": "Feature-Code"}, "aicoreProvider": {"en": "AI Provider", "de": "AI-Anbieter"}, "aicoreModel": {"en": "AI Model", "de": "AI-Modell"}, "createdByUserId": {"en": "Created By User", "de": "Erstellt von Benutzer"}, }, ) class BillingSettings(BaseModel): """Billing settings per mandate. Only PREPAY_MANDATE model.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key" ) mandateId: str = Field(..., description="Foreign key to Mandate (UNIQUE)") warningThresholdPercent: float = Field(default=10.0, description="Warning threshold as percentage") # Stripe stripeCustomerId: Optional[str] = Field(None, description="Stripe Customer ID (cus_xxx) — one per mandate") # Auto-Recharge for AI budget autoRechargeEnabled: bool = Field(default=False, description="Auto-buy AI budget when low") rechargeAmountCHF: float = Field(default=10.0, description="Amount per auto-recharge (CHF, prepaid via Stripe)") rechargeMaxPerMonth: int = Field(default=3, description="Max auto-recharges per month") rechargesThisMonth: int = Field(default=0, description="Counter: auto-recharges used this month") monthResetAt: Optional[datetime] = Field(None, description="When rechargesThisMonth was last reset") # Notifications notifyEmails: List[str] = Field( default_factory=list, description="Email addresses for billing alerts (pool exhausted, warnings, etc.)", ) notifyOnWarning: bool = Field(default=True, description="Send email when warning threshold is reached") # 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" ) storagePeriodStartAt: Optional[datetime] = Field( None, description="Subscription billing period start used for storage reset" ) storageBilledUpToMB: float = Field( default=0.0, description="Overage MB already debited this period (above plan-included volume)", ) registerModelLabels( "BillingSettings", {"en": "Billing Settings", "de": "Abrechnungseinstellungen"}, { "id": {"en": "ID", "de": "ID"}, "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"}, "warningThresholdPercent": {"en": "Warning Threshold (%)", "de": "Warnschwelle (%)"}, "stripeCustomerId": {"en": "Stripe Customer ID", "de": "Stripe-Kunden-ID"}, "autoRechargeEnabled": {"en": "Auto-Recharge", "de": "Auto-Nachladung"}, "rechargeAmountCHF": {"en": "Recharge Amount (CHF)", "de": "Nachladebetrag (CHF)"}, "rechargeMaxPerMonth": {"en": "Max Recharges/Month", "de": "Max. Nachladungen/Monat"}, "notifyEmails": { "en": "Billing notification emails (owner / admin)", "de": "E-Mails fuer Billing-Alerts (Inhaber/Admin)", }, "notifyOnWarning": {"en": "Notify on Warning", "de": "Bei Warnung benachrichtigen"}, "storageHighWatermarkMB": {"en": "Storage peak (MB)", "de": "Speicher-Peak (MB)"}, "storagePeriodStartAt": {"en": "Storage period start", "de": "Speicher-Periodenbeginn"}, "storageBilledUpToMB": { "en": "Storage billed overage (MB)", "de": "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" ) class UsageStatistics(BaseModel): """Aggregated usage statistics for quick retrieval.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key" ) accountId: str = Field(..., description="Foreign key to BillingAccount") periodType: PeriodTypeEnum = Field(..., description="Period type") periodStart: date = Field(..., description="Period start date") # Aggregated values totalCostCHF: float = Field(default=0.0, description="Total cost in CHF") transactionCount: int = Field(default=0, description="Number of transactions") # Breakdown by provider costByProvider: Dict[str, float] = Field( default_factory=dict, description="Cost breakdown by provider (e.g., {'anthropic': 12.50, 'openai': 8.30})" ) # Breakdown by feature costByFeature: Dict[str, float] = Field( default_factory=dict, description="Cost breakdown by feature (e.g., {'automation': 5.80, 'workspace': 3.20})" ) registerModelLabels( "UsageStatistics", {"en": "Usage Statistics", "de": "Nutzungsstatistik"}, { "id": {"en": "ID", "de": "ID"}, "accountId": {"en": "Account ID", "de": "Konto-ID"}, "periodType": {"en": "Period Type", "de": "Periodentyp"}, "periodStart": {"en": "Period Start", "de": "Periodenbeginn"}, "totalCostCHF": {"en": "Total Cost (CHF)", "de": "Gesamtkosten (CHF)"}, "transactionCount": {"en": "Transaction Count", "de": "Anzahl Transaktionen"}, "costByProvider": {"en": "Cost by Provider", "de": "Kosten nach Anbieter"}, "costByFeature": {"en": "Cost by Feature", "de": "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