from __future__ import annotations import uuid from datetime import datetime from decimal import Decimal from typing import Any from sqlalchemy import ( Boolean, CheckConstraint, DateTime, ForeignKeyConstraint, Index, Integer, Numeric, String, UniqueConstraint, func, text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.types import JSON from app.db.base_class import Base def _new_id() -> str: return str(uuid.uuid4()) class TenantCommercialPlan(Base): """租户已协商的商业套餐版本,不与平台内部成本或客户节省事实混用。""" __tablename__ = "tenant_commercial_plans" __table_args__ = ( UniqueConstraint("tenant_id", "id", name="uq_tenant_commercial_plans_tenant_id"), UniqueConstraint( "tenant_id", "plan_code", "version", name="uq_tenant_commercial_plans_tenant_code_version", ), CheckConstraint( "pricing_model IN ('subscription', 'usage', 'hybrid', 'pilot', 'custom')", name="ck_tenant_commercial_plans_pricing_model", ), CheckConstraint( "billing_interval IN ('monthly', 'quarterly', 'annual', 'contract')", name="ck_tenant_commercial_plans_billing_interval", ), CheckConstraint( "status IN ('draft', 'active', 'retired')", name="ck_tenant_commercial_plans_status", ), CheckConstraint( "base_fee >= 0 AND included_seats >= 0 AND version >= 1", name="ck_tenant_commercial_plans_values", ), CheckConstraint( "length(trim(plan_code)) > 0 AND length(trim(name)) > 0", name="ck_tenant_commercial_plans_keys", ), CheckConstraint( "length(trim(currency)) = 3", name="ck_tenant_commercial_plans_currency", ), CheckConstraint( "effective_to IS NULL OR effective_to > effective_from", name="ck_tenant_commercial_plans_effective_window", ), Index( "uq_tenant_commercial_plans_active_code", "tenant_id", "plan_code", unique=True, postgresql_where=text("status = 'active'"), ), Index( "ix_tenant_commercial_plans_tenant_status", "tenant_id", "status", "effective_from", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) plan_code: Mapped[str] = mapped_column(String(80), nullable=False) name: Mapped[str] = mapped_column(String(160), nullable=False) pricing_model: Mapped[str] = mapped_column(String(24), nullable=False) billing_interval: Mapped[str] = mapped_column(String(20), nullable=False) currency: Mapped[str] = mapped_column(String(3), nullable=False) base_fee: Mapped[Decimal] = mapped_column(Numeric(20, 4), nullable=False) included_seats: Mapped[int] = mapped_column( Integer, nullable=False, default=0, server_default="0" ) overage_enabled: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, server_default="false" ) status: Mapped[str] = mapped_column( String(16), nullable=False, default="draft", server_default="draft" ) effective_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) effective_to: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1") contract_terms_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict) created_by: Mapped[str] = mapped_column(String(120), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) subscriptions = relationship("TenantSubscription", back_populates="plan", passive_deletes=True) class TenantSubscription(Base): """租户订阅及当期计费快照,避免套餐后续变化污染历史账期。""" __tablename__ = "tenant_subscriptions" __table_args__ = ( UniqueConstraint("tenant_id", "id", name="uq_tenant_subscriptions_tenant_id"), UniqueConstraint( "tenant_id", "subscription_key", name="uq_tenant_subscriptions_tenant_key" ), UniqueConstraint( "tenant_id", "external_provider", "external_subscription_id", name="uq_tenant_subscriptions_external_ref", ), ForeignKeyConstraint( ["tenant_id", "plan_id"], ["tenant_commercial_plans.tenant_id", "tenant_commercial_plans.id"], name="fk_tenant_subscriptions_tenant_plan", ondelete="RESTRICT", ), CheckConstraint( "status IN ('trialing', 'active', 'past_due', 'suspended', 'canceled', 'expired')", name="ck_tenant_subscriptions_status", ), CheckConstraint( "billing_interval IN ('monthly', 'quarterly', 'annual', 'contract')", name="ck_tenant_subscriptions_billing_interval", ), CheckConstraint( "seats > 0 AND base_fee_snapshot >= 0 AND version >= 1", name="ck_tenant_subscriptions_values", ), CheckConstraint( "length(trim(subscription_key)) > 0 AND length(trim(currency)) = 3", name="ck_tenant_subscriptions_keys", ), CheckConstraint( "current_period_end > current_period_start", name="ck_tenant_subscriptions_period", ), CheckConstraint( "ends_at IS NULL OR ends_at > starts_at", name="ck_tenant_subscriptions_contract_window", ), CheckConstraint( "(external_provider IS NULL AND external_subscription_id IS NULL) OR " "(external_provider IS NOT NULL AND external_subscription_id IS NOT NULL)", name="ck_tenant_subscriptions_external_pair", ), CheckConstraint( "status != 'canceled' OR canceled_at IS NOT NULL", name="ck_tenant_subscriptions_cancellation", ), Index( "uq_tenant_subscriptions_current", "tenant_id", unique=True, postgresql_where=text("status IN ('trialing', 'active', 'past_due', 'suspended')"), ), Index( "ix_tenant_subscriptions_tenant_status_period", "tenant_id", "status", "current_period_end", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) subscription_key: Mapped[str] = mapped_column(String(120), nullable=False) plan_id: Mapped[str] = mapped_column(String(36), nullable=False) status: Mapped[str] = mapped_column(String(20), nullable=False) starts_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) current_period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) current_period_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) seats: Mapped[int] = mapped_column(Integer, nullable=False) base_fee_snapshot: Mapped[Decimal] = mapped_column(Numeric(20, 4), nullable=False) currency: Mapped[str] = mapped_column(String(3), nullable=False) billing_interval: Mapped[str] = mapped_column(String(20), nullable=False) auto_renew: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, server_default="false" ) external_provider: Mapped[str | None] = mapped_column(String(60)) external_subscription_id: Mapped[str | None] = mapped_column(String(160)) canceled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1") metadata_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict) created_by: Mapped[str] = mapped_column(String(120), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) plan = relationship("TenantCommercialPlan", back_populates="subscriptions") entitlements = relationship( "CommercialEntitlement", back_populates="subscription", passive_deletes=True ) class CommercialEntitlement(Base): """订阅的功能权益与可计量配额定义。""" __tablename__ = "commercial_entitlements" __table_args__ = ( UniqueConstraint("tenant_id", "id", name="uq_commercial_entitlements_tenant_id"), UniqueConstraint( "tenant_id", "subscription_id", "id", name="uq_commercial_entitlements_tenant_subscription_id", ), UniqueConstraint( "tenant_id", "subscription_id", "entitlement_key", name="uq_commercial_entitlements_subscription_key", ), ForeignKeyConstraint( ["tenant_id", "subscription_id"], ["tenant_subscriptions.tenant_id", "tenant_subscriptions.id"], name="fk_commercial_entitlements_tenant_subscription", ondelete="RESTRICT", ), CheckConstraint( "entitlement_type IN ('feature', 'metered', 'unlimited')", name="ck_commercial_entitlements_type", ), CheckConstraint( "reset_interval IN ('none', 'monthly', 'quarterly', 'annual', 'contract')", name="ck_commercial_entitlements_reset_interval", ), CheckConstraint( "overage_policy IN ('block', 'allow', 'alert')", name="ck_commercial_entitlements_overage_policy", ), CheckConstraint( "status IN ('active', 'suspended', 'expired')", name="ck_commercial_entitlements_status", ), CheckConstraint( "version >= 1 AND (included_quantity IS NULL OR included_quantity >= 0) " "AND (hard_limit_quantity IS NULL OR hard_limit_quantity >= 0)", name="ck_commercial_entitlements_values", ), CheckConstraint( "(entitlement_type = 'unlimited' AND included_quantity IS NULL " "AND hard_limit_quantity IS NULL) OR " "(entitlement_type = 'feature' AND included_quantity IN (0, 1) " "AND (hard_limit_quantity IS NULL OR hard_limit_quantity IN (0, 1))) OR " "(entitlement_type = 'metered' AND included_quantity IS NOT NULL " "AND (hard_limit_quantity IS NULL OR hard_limit_quantity >= included_quantity))", name="ck_commercial_entitlements_quota_shape", ), CheckConstraint( "length(trim(entitlement_key)) > 0 AND length(trim(metric_key)) > 0 " "AND length(trim(unit)) > 0", name="ck_commercial_entitlements_keys", ), CheckConstraint( "effective_to IS NULL OR effective_to > effective_from", name="ck_commercial_entitlements_effective_window", ), Index( "ix_commercial_entitlements_subscription_status", "tenant_id", "subscription_id", "status", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) subscription_id: Mapped[str] = mapped_column(String(36), nullable=False) entitlement_key: Mapped[str] = mapped_column(String(120), nullable=False) metric_key: Mapped[str] = mapped_column(String(120), nullable=False) entitlement_type: Mapped[str] = mapped_column(String(20), nullable=False) unit: Mapped[str] = mapped_column(String(40), nullable=False) included_quantity: Mapped[Decimal | None] = mapped_column(Numeric(20, 6)) hard_limit_quantity: Mapped[Decimal | None] = mapped_column(Numeric(20, 6)) reset_interval: Mapped[str] = mapped_column(String(20), nullable=False) overage_policy: Mapped[str] = mapped_column(String(16), nullable=False) status: Mapped[str] = mapped_column(String(16), nullable=False) effective_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) effective_to: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1") config_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) subscription = relationship("TenantSubscription", back_populates="entitlements") class UsageMeterEvent(Base): """追加只读的客户用量事实;同一来源幂等键只能产生一条事件。""" __tablename__ = "usage_meter_events" __table_args__ = ( UniqueConstraint("tenant_id", "id", name="uq_usage_meter_events_tenant_id"), UniqueConstraint( "tenant_id", "subscription_id", "id", name="uq_usage_meter_events_tenant_subscription_id", ), UniqueConstraint( "tenant_id", "subscription_id", "entitlement_id", "id", name="uq_usage_meter_events_entitlement_id", ), UniqueConstraint( "tenant_id", "source_system", "idempotency_key", name="uq_usage_meter_events_source_request", ), ForeignKeyConstraint( ["tenant_id", "subscription_id"], ["tenant_subscriptions.tenant_id", "tenant_subscriptions.id"], name="fk_usage_meter_events_tenant_subscription", ondelete="RESTRICT", ), ForeignKeyConstraint( ["tenant_id", "subscription_id", "entitlement_id"], [ "commercial_entitlements.tenant_id", "commercial_entitlements.subscription_id", "commercial_entitlements.id", ], name="fk_usage_meter_events_tenant_entitlement", ondelete="RESTRICT", ), ForeignKeyConstraint( ["tenant_id", "subscription_id", "billing_period_id"], [ "commercial_billing_periods.tenant_id", "commercial_billing_periods.subscription_id", "commercial_billing_periods.id", ], name="fk_usage_meter_events_tenant_billing_period", ondelete="RESTRICT", ), ForeignKeyConstraint( ["tenant_id", "subscription_id", "entitlement_id", "reversal_of_event_id"], [ "usage_meter_events.tenant_id", "usage_meter_events.subscription_id", "usage_meter_events.entitlement_id", "usage_meter_events.id", ], name="fk_usage_meter_events_tenant_reversal", ondelete="RESTRICT", ), CheckConstraint( "event_type IN ('usage', 'credit', 'adjustment', 'reversal')", name="ck_usage_meter_events_type", ), CheckConstraint( "(event_type = 'usage' AND quantity > 0) OR " "(event_type = 'credit' AND quantity < 0) OR " "(event_type IN ('adjustment', 'reversal') AND quantity <> 0)", name="ck_usage_meter_events_quantity", ), CheckConstraint( "(event_type = 'reversal' AND reversal_of_event_id IS NOT NULL) OR " "(event_type != 'reversal' AND reversal_of_event_id IS NULL)", name="ck_usage_meter_events_reversal", ), CheckConstraint( "(subject_type IS NULL AND subject_id IS NULL) OR " "(subject_type IS NOT NULL AND subject_id IS NOT NULL)", name="ck_usage_meter_events_subject_pair", ), CheckConstraint( "actor_type IN ('system', 'user', 'integration', 'admin')", name="ck_usage_meter_events_actor_type", ), CheckConstraint( "length(trim(metric_key)) > 0 AND length(trim(unit)) > 0 " "AND length(trim(period_key)) > 0 AND length(trim(source_system)) > 0 " "AND length(trim(quota_period_key)) > 0 " "AND length(trim(idempotency_key)) > 0 " "AND length(trim(request_fingerprint)) > 0", name="ck_usage_meter_events_keys", ), Index( "ix_usage_meter_events_quota_window", "tenant_id", "subscription_id", "metric_key", "quota_period_key", "occurred_at", ), Index( "ix_usage_meter_events_billing_period", "tenant_id", "billing_period_id", "occurred_at", ), Index( "ix_usage_meter_events_correlation", "tenant_id", "correlation_id", "occurred_at", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) subscription_id: Mapped[str] = mapped_column(String(36), nullable=False) entitlement_id: Mapped[str] = mapped_column(String(36), nullable=False) billing_period_id: Mapped[str] = mapped_column(String(36), nullable=False) event_type: Mapped[str] = mapped_column(String(16), nullable=False) metric_key: Mapped[str] = mapped_column(String(120), nullable=False) quantity: Mapped[Decimal] = mapped_column(Numeric(20, 6), nullable=False) unit: Mapped[str] = mapped_column(String(40), nullable=False) period_key: Mapped[str] = mapped_column(String(64), nullable=False) quota_period_key: Mapped[str] = mapped_column(String(64), nullable=False) occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) source_system: Mapped[str] = mapped_column(String(80), nullable=False) idempotency_key: Mapped[str] = mapped_column(String(160), nullable=False) request_fingerprint: Mapped[str] = mapped_column(String(80), nullable=False) reversal_of_event_id: Mapped[str | None] = mapped_column(String(36)) subject_type: Mapped[str | None] = mapped_column(String(60)) subject_id: Mapped[str | None] = mapped_column(String(160)) actor_type: Mapped[str] = mapped_column(String(20), nullable=False) actor_id: Mapped[str] = mapped_column(String(120), nullable=False) correlation_id: Mapped[str | None] = mapped_column(String(120)) trace_id: Mapped[str | None] = mapped_column(String(120)) metadata_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict) recorded_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class CommercialCostEvent(Base): """平台内部成本事实;物理上独立于客户节省、价值机会和价值实现。""" __tablename__ = "commercial_cost_events" __table_args__ = ( UniqueConstraint("tenant_id", "id", name="uq_commercial_cost_events_tenant_id"), UniqueConstraint( "tenant_id", "source_system", "idempotency_key", name="uq_commercial_cost_events_source_request", ), ForeignKeyConstraint( ["tenant_id", "subscription_id"], ["tenant_subscriptions.tenant_id", "tenant_subscriptions.id"], name="fk_commercial_cost_events_tenant_subscription", ondelete="RESTRICT", ), ForeignKeyConstraint( ["tenant_id", "subscription_id", "usage_event_id"], [ "usage_meter_events.tenant_id", "usage_meter_events.subscription_id", "usage_meter_events.id", ], name="fk_commercial_cost_events_tenant_usage", ondelete="RESTRICT", ), ForeignKeyConstraint( ["tenant_id", "subscription_id", "billing_period_id"], [ "commercial_billing_periods.tenant_id", "commercial_billing_periods.subscription_id", "commercial_billing_periods.id", ], name="fk_commercial_cost_events_tenant_billing_period", ondelete="RESTRICT", ), ForeignKeyConstraint( ["tenant_id", "reversal_of_cost_event_id"], ["commercial_cost_events.tenant_id", "commercial_cost_events.id"], name="fk_commercial_cost_events_tenant_reversal", ondelete="RESTRICT", ), CheckConstraint( "event_type IN ('incurred', 'credit', 'adjustment', 'reversal')", name="ck_commercial_cost_events_type", ), CheckConstraint( "cost_category IN ('ai_inference', 'ocr', 'storage', 'connector', " "'support', 'implementation', 'infrastructure', 'payment', 'other')", name="ck_commercial_cost_events_category", ), CheckConstraint( "quantity > 0 AND unit_cost >= 0 AND fx_rate > 0", name="ck_commercial_cost_events_values", ), CheckConstraint( "(event_type = 'incurred' AND cost_amount >= 0 AND reporting_amount >= 0) OR " "(event_type = 'credit' AND cost_amount <= 0 AND reporting_amount <= 0) OR " "(event_type IN ('adjustment', 'reversal') AND cost_amount <> 0 " "AND reporting_amount <> 0)", name="ck_commercial_cost_events_amount_direction", ), CheckConstraint( "(event_type = 'reversal' AND reversal_of_cost_event_id IS NOT NULL) OR " "(event_type != 'reversal' AND reversal_of_cost_event_id IS NULL)", name="ck_commercial_cost_events_reversal", ), CheckConstraint( "usage_event_id IS NULL OR subscription_id IS NOT NULL", name="ck_commercial_cost_events_usage_pair", ), CheckConstraint( "(subscription_id IS NULL AND billing_period_id IS NULL) OR " "(subscription_id IS NOT NULL AND billing_period_id IS NOT NULL)", name="ck_commercial_cost_events_billing_period_pair", ), CheckConstraint( "length(trim(unit)) > 0 AND length(trim(source_system)) > 0 " "AND length(trim(idempotency_key)) > 0 " "AND length(trim(request_fingerprint)) > 0 " "AND length(trim(original_currency)) = 3 " "AND length(trim(reporting_currency)) = 3", name="ck_commercial_cost_events_keys", ), Index( "ix_commercial_cost_events_tenant_period", "tenant_id", "occurred_at", "cost_category", ), Index( "ix_commercial_cost_events_subscription_period", "tenant_id", "subscription_id", "billing_period_id", "occurred_at", ), Index( "ix_commercial_cost_events_allocation", "tenant_id", "allocation_key", "occurred_at", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) subscription_id: Mapped[str | None] = mapped_column(String(36)) billing_period_id: Mapped[str | None] = mapped_column(String(36)) usage_event_id: Mapped[str | None] = mapped_column(String(36)) event_type: Mapped[str] = mapped_column(String(16), nullable=False) cost_category: Mapped[str] = mapped_column(String(32), nullable=False) quantity: Mapped[Decimal] = mapped_column(Numeric(20, 6), nullable=False) unit: Mapped[str] = mapped_column(String(40), nullable=False) unit_cost: Mapped[Decimal] = mapped_column(Numeric(20, 8), nullable=False) cost_amount: Mapped[Decimal] = mapped_column(Numeric(20, 4), nullable=False) original_currency: Mapped[str] = mapped_column(String(3), nullable=False) reporting_amount: Mapped[Decimal] = mapped_column(Numeric(20, 4), nullable=False) reporting_currency: Mapped[str] = mapped_column(String(3), nullable=False) fx_rate: Mapped[Decimal] = mapped_column(Numeric(20, 8), nullable=False) provider: Mapped[str | None] = mapped_column(String(120)) sku: Mapped[str | None] = mapped_column(String(120)) model_name: Mapped[str | None] = mapped_column(String(120)) allocation_key: Mapped[str] = mapped_column(String(160), nullable=False) occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) source_system: Mapped[str] = mapped_column(String(80), nullable=False) idempotency_key: Mapped[str] = mapped_column(String(160), nullable=False) request_fingerprint: Mapped[str] = mapped_column(String(80), nullable=False) reversal_of_cost_event_id: Mapped[str | None] = mapped_column(String(36)) correlation_id: Mapped[str | None] = mapped_column(String(120)) trace_id: Mapped[str | None] = mapped_column(String(120)) metadata_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict) recorded_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) __all__ = [ "CommercialCostEvent", "CommercialEntitlement", "TenantCommercialPlan", "TenantSubscription", "UsageMeterEvent", ]