from __future__ import annotations from datetime import datetime from decimal import Decimal from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, JSON, Numeric, String, text from sqlalchemy.orm import Mapped, mapped_column from app.enums.credit_balance import CreditBalanceStatus, CreditScope from app.models.base import Base, TimestampMixin class UserCreditBalance(Base, TimestampMixin): __tablename__ = "user_credit_balances" __table_args__ = ( CheckConstraint("grant_amount >= 0", name="ck_user_credit_balances_grant_nonnegative"), CheckConstraint("unspent_amount >= 0", name="ck_user_credit_balances_unspent_nonnegative"), CheckConstraint("consumed_amount >= 0", name="ck_user_credit_balances_consumed_nonnegative"), CheckConstraint("expired_amount >= 0", name="ck_user_credit_balances_expired_nonnegative"), CheckConstraint("revoked_amount >= 0", name="ck_user_credit_balances_revoked_nonnegative"), CheckConstraint( "grant_amount = unspent_amount + consumed_amount + expired_amount + revoked_amount", name="ck_user_credit_balances_amount_reconciled", ), CheckConstraint("expires_at > valid_from", name="ck_user_credit_balances_valid_window"), CheckConstraint( "(credit_scope = 'personal' AND team_id IS NULL) OR " "(credit_scope = 'team' AND team_id IS NOT NULL AND subscription_id IS NOT NULL " "AND subscription_period_id IS NOT NULL)", name="ck_user_credit_balances_scope_fields", ), Index( "ix_user_credit_balances_spendable", "user_id", "credit_scope", "credit_level_rank", "expires_at", "valid_from", "id", postgresql_where=text("unspent_amount > 0 AND revoked_at IS NULL"), ), Index( "ix_user_credit_balances_team_spendable", "team_id", "subscription_id", "subscription_period_id", "credit_level_rank", "expires_at", "id", postgresql_where=text("credit_scope = 'team' AND unspent_amount > 0 AND revoked_at IS NULL"), ), Index( "ix_user_credit_balances_expire_due", "expires_at", "id", postgresql_where=text( "unspent_amount > 0 AND expired_processed_at IS NULL AND revoked_at IS NULL" ), ), Index("ix_user_credit_balances_source", "source_type", "source_id"), Index("ix_user_credit_balances_payment", "payment_order_id"), Index("ix_user_credit_balances_subscription", "subscription_id", "subscription_period_id"), Index("uq_user_credit_balances_user_biz_key", "user_id", "biz_key", unique=True), ) id: Mapped[str] = mapped_column(String(32), primary_key=True) user_id: Mapped[str] = mapped_column( String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, comment="资金所有人;团队积分为成交时队长", ) credit_scope: Mapped[str] = mapped_column( String(16), nullable=False, default=CreditScope.PERSONAL.value, server_default=CreditScope.PERSONAL.value, ) team_id: Mapped[str | None] = mapped_column( String(32), ForeignKey("teams.id", ondelete="RESTRICT"), nullable=True ) credit_level: Mapped[str] = mapped_column(String(32), nullable=False) credit_level_rank: Mapped[int] = mapped_column(nullable=False, default=20, server_default="20") source_type: Mapped[str] = mapped_column(String(48), nullable=False, index=True) source_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) product_id: Mapped[str | None] = mapped_column( String(32), ForeignKey("credit_products.id", ondelete="SET NULL"), nullable=True ) payment_order_id: Mapped[str | None] = mapped_column( String(32), ForeignKey("payment_orders.id", ondelete="SET NULL"), nullable=True ) subscription_id: Mapped[str | None] = mapped_column( String(32), ForeignKey("user_credit_subscriptions.id", ondelete="SET NULL"), nullable=True ) subscription_period_id: Mapped[str | None] = mapped_column( String(32), ForeignKey("user_credit_subscription_periods.id", ondelete="SET NULL"), nullable=True ) grant_record_id: Mapped[str | None] = mapped_column( String(32), ForeignKey("credit_records.id", ondelete="SET NULL"), nullable=True, index=True ) grant_amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False) unspent_amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False) consumed_amount: Mapped[Decimal] = mapped_column( Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0" ) expired_amount: Mapped[Decimal] = mapped_column( Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0" ) revoked_amount: Mapped[Decimal] = mapped_column( Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0" ) valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) status: Mapped[str] = mapped_column( String(24), nullable=False, default=CreditBalanceStatus.ACTIVE.value, server_default=CreditBalanceStatus.ACTIVE.value, ) expired_processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) biz_key: Mapped[str] = mapped_column(String(180), nullable=False) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)