"""Referral Engine aggregates — Phase 7.5. Independent aggregates (no SQLAlchemy relationships), same as other Loyalty aggregates. Bonus points are always the ledger's responsibility; these rows only record configuration, code ownership, and referrer→referee tracking. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import DateTime, Index, Integer, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base from app.models.base import ( ActorAuditMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import ( GUID, ReferralAttributionStatus, ReferralCodeStatus, ReferralProgramStatus, ) class ReferralProgram( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Referral program configuration — bonus rules for referrer/referee.""" __tablename__ = "referral_programs" program_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) status: Mapped[ReferralProgramStatus] = mapped_column( default=ReferralProgramStatus.DRAFT, nullable=False ) referrer_bonus_points: Mapped[int] = mapped_column( Integer, default=0, nullable=False ) referee_bonus_points: Mapped[int] = mapped_column( Integer, default=0, nullable=False ) max_referrals_per_member: Mapped[int | None] = mapped_column( Integer, nullable=True ) __table_args__ = ( UniqueConstraint( "tenant_id", "program_id", "code", name="uq_referral_programs_tenant_program_code", ), Index("ix_referral_programs_tenant_status", "tenant_id", "status"), Index("ix_referral_programs_tenant_deleted", "tenant_id", "is_deleted"), ) class ReferralCode( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, ): """Referral code owned by a member (the referrer). Status-only lifecycle.""" __tablename__ = "referral_codes" referral_program_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) program_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) member_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) code: Mapped[str] = mapped_column(String(50), nullable=False) status: Mapped[ReferralCodeStatus] = mapped_column( default=ReferralCodeStatus.ACTIVE, nullable=False ) uses_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) created_by: Mapped[str | None] = mapped_column(String(100), nullable=True) __table_args__ = ( UniqueConstraint("tenant_id", "code", name="uq_referral_codes_tenant_code"), Index( "ix_referral_codes_program_member", "tenant_id", "referral_program_id", "member_id", ), ) class ReferralAttribution( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, ): """Referrer -> referee link and its lifecycle through reward.""" __tablename__ = "referral_attributions" referral_program_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) referral_code_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) program_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) referrer_member_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) referee_member_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[ReferralAttributionStatus] = mapped_column( default=ReferralAttributionStatus.PENDING, nullable=False ) referrer_ledger_entry_id: Mapped[uuid.UUID | None] = mapped_column( GUID(), nullable=True ) referee_ledger_entry_id: Mapped[uuid.UUID | None] = mapped_column( GUID(), nullable=True ) idempotency_key: Mapped[str | None] = mapped_column(String(100), nullable=True) converted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) rewarded_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) cancelled_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) cancel_reason: Mapped[str | None] = mapped_column(String(500), nullable=True) actor_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "referral_program_id", "referee_member_id", name="uq_referral_attributions_tenant_program_referee", ), UniqueConstraint( "tenant_id", "idempotency_key", name="uq_referral_attributions_tenant_idempotency", ), Index( "ix_referral_attributions_referrer", "tenant_id", "referral_program_id", "referrer_member_id", ), Index("ix_referral_attributions_status", "tenant_id", "status"), )