# 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 from pydantic import BaseModel, Field from modules.shared.attributeUtils import registerModelLabels import uuid class BillingModelEnum(str, Enum): """Billing model types.""" PREPAY_MANDATE = "PREPAY_MANDATE" # Prepaid budget shared by all users in mandate PREPAY_USER = "PREPAY_USER" # Prepaid budget per user within mandate CREDIT_POSTPAY = "CREDIT_POSTPAY" # Credit with monthly invoice (requires billing address) UNLIMITED = "UNLIMITED" # No cost limitation (internal mandates only) class AccountTypeEnum(str, Enum): """Account type for billing accounts.""" MANDATE = "MANDATE" # Account for entire mandate USER = "USER" # Account for specific user within mandate 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) class PeriodTypeEnum(str, Enum): """Period types for usage statistics.""" DAY = "DAY" MONTH = "MONTH" YEAR = "YEAR" class BillingAddress(BaseModel): """Billing address for CREDIT_POSTPAY mandates.""" company: str = Field(..., description="Company name") street: str = Field(..., description="Street and number") zip: str = Field(..., description="Postal code") city: str = Field(..., description="City") country: str = Field(default="CH", description="Country code") vatNumber: Optional[str] = Field(None, description="VAT number (optional)") registerModelLabels( "BillingAddress", {"en": "Billing Address", "de": "Rechnungsadresse"}, { "company": {"en": "Company", "de": "Firma"}, "street": {"en": "Street", "de": "Strasse"}, "zip": {"en": "ZIP", "de": "PLZ"}, "city": {"en": "City", "de": "Ort"}, "country": {"en": "Country", "de": "Land"}, "vatNumber": {"en": "VAT Number", "de": "MwSt-Nummer"}, }, ) class BillingAccount(BaseModel): """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 (only for PREPAY_USER)") accountType: AccountTypeEnum = Field(..., description="Account type: MANDATE or USER") balance: float = Field(default=0.0, description="Current balance in CHF") creditLimit: Optional[float] = Field(None, description="Credit limit in CHF (only for CREDIT_POSTPAY)") 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"}, "accountType": {"en": "Account Type", "de": "Kontotyp"}, "balance": {"en": "Balance (CHF)", "de": "Guthaben (CHF)"}, "creditLimit": {"en": "Credit Limit (CHF)", "de": "Kreditlimit (CHF)"}, "warningThreshold": {"en": "Warning Threshold (CHF)", "de": "Warnschwelle (CHF)"}, "lastWarningAt": {"en": "Last Warning", "de": "Letzte Warnung"}, "enabled": {"en": "Enabled", "de": "Aktiv"}, }, ) class BillingTransaction(BaseModel): """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., chatplayground, 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") 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.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key" ) mandateId: str = Field(..., description="Foreign key to Mandate (UNIQUE)") billingModel: BillingModelEnum = Field(..., description="Billing model") # Configuration defaultUserCredit: float = Field(default=10.0, description="Initial credit in CHF for new users (PREPAY_USER)") warningThresholdPercent: float = Field(default=10.0, description="Warning threshold as percentage") blockOnZeroBalance: bool = Field(default=True, description="Block AI features when balance is zero") # Billing address (required for CREDIT_POSTPAY) billingAddress: Optional[BillingAddress] = Field(None, description="Billing address") # Notifications notifyEmails: List[str] = Field(default_factory=list, description="Email addresses for billing notifications") notifyOnWarning: bool = Field(default=True, description="Send email when warning threshold is reached") registerModelLabels( "BillingSettings", {"en": "Billing Settings", "de": "Abrechnungseinstellungen"}, { "id": {"en": "ID", "de": "ID"}, "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"}, "billingModel": {"en": "Billing Model", "de": "Abrechnungsmodell"}, "defaultUserCredit": {"en": "Default User Credit (CHF)", "de": "Standard-Startguthaben (CHF)"}, "warningThresholdPercent": {"en": "Warning Threshold (%)", "de": "Warnschwelle (%)"}, "blockOnZeroBalance": {"en": "Block on Zero Balance", "de": "Bei 0 blockieren"}, "billingAddress": {"en": "Billing Address", "de": "Rechnungsadresse"}, "notifyEmails": {"en": "Notification Emails", "de": "Benachrichtigungs-Emails"}, "notifyOnWarning": {"en": "Notify on Warning", "de": "Bei Warnung benachrichtigen"}, }, ) 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., {'chatplayground': 15.00, 'automation': 5.80})" ) 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 billingModel: BillingModelEnum balance: float currency: str = "CHF" warningThreshold: float isWarning: bool creditLimit: Optional[float] = None 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.""" allowed: bool reason: Optional[str] = None currentBalance: Optional[float] = None requiredAmount: Optional[float] = None billingModel: Optional[BillingModelEnum] = None