gateway/modules/datamodels/datamodelBilling.py
ValueOn AG c8b7517209 refactor: modules/services/ abgeloest durch serviceCenter + serviceHub
serviceCenter = DI-Container (Resolver, Registry, Context) fuer Service-Instanziierung
serviceHub = Consumer-facing Aggregation (DB-Interfaces, Runtime-State, lazy Service-Resolution via serviceCenter)

- modules/serviceHub/ erstellt: ServiceHub, PublicService, getInterface()
- 22 Consumer-Dateien migriert (routes, features, tests): imports von modules.services auf serviceHub bzw. serviceCenter umgestellt
- resolver.py: legacy fallback auf altes services/ entfernt
- modules/services/ komplett geloescht (83 Dateien inkl. dead code mainAiChat.py)
- pre-extraction: progress callback durch chunk-pipeline propagiert, operationType DATA_EXTRACT->DATA_ANALYSE fuer guenstigeres Modell
2026-03-14 11:51:45 +01:00

287 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.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")
# 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."""
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