# 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.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 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., {'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