TorbatYar/backend/services/loyalty/app/models/wallet.py
Mortezakoohjani 071c484530 Ship Loyalty phases 7.2-7.6 (points through wallet) with production deploy.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 21:27:22 +03:30

99 lines
3.9 KiB
Python

"""Wallet Engine aggregates — Phase 7.6.
Mirrors the Point Engine's immutability discipline: WalletAccount is a shell
only (no mutable balance column). Balance = SUM(signed WalletLedgerEntry
amounts). WalletLedgerEntry rows are append-only — never update/delete.
"""
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 sqlalchemy.types import JSON
from app.core.database import Base
from app.models.base import (
ActorAuditMixin,
SoftDeleteMixin,
TenantMixin,
TimestampMixin,
UUIDPrimaryKeyMixin,
)
from app.models.types import GUID, WalletAccountStatus, WalletLedgerEntryType
class WalletAccount(
Base,
UUIDPrimaryKeyMixin,
TenantMixin,
TimestampMixin,
SoftDeleteMixin,
ActorAuditMixin,
):
"""Wallet account shell — balance only via immutable ledger (Phase 7.6)."""
__tablename__ = "wallet_accounts"
program_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
member_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
account_number: Mapped[str] = mapped_column(String(50), nullable=False)
currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False)
status: Mapped[WalletAccountStatus] = mapped_column(
default=WalletAccountStatus.OPEN, nullable=False
)
__table_args__ = (
UniqueConstraint(
"tenant_id",
"account_number",
name="uq_wallet_accounts_tenant_account_number",
),
UniqueConstraint(
"tenant_id",
"program_id",
"member_id",
name="uq_wallet_accounts_tenant_program_member",
),
Index("ix_wallet_accounts_program", "tenant_id", "program_id"),
Index("ix_wallet_accounts_member", "tenant_id", "member_id"),
Index("ix_wallet_accounts_tenant_deleted", "tenant_id", "is_deleted"),
)
class WalletLedgerEntry(UUIDPrimaryKeyMixin, TenantMixin, Base):
"""Append-only wallet ledger entry. Balance = SUM(signed amounts). Never update/delete."""
__tablename__ = "wallet_ledger_entries"
wallet_account_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)
entry_type: Mapped[WalletLedgerEntryType] = mapped_column(nullable=False)
# Signed minor-unit amount: credit/transfer_in positive; debit/transfer_out
# negative; adjust caller-supplied signed value.
amount: Mapped[int] = mapped_column(Integer, nullable=False)
balance_after: Mapped[int] = mapped_column(Integer, nullable=False)
idempotency_key: Mapped[str | None] = mapped_column(String(100), nullable=True)
reference_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
reference_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
actor_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
__table_args__ = (
UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_wallet_ledger_tenant_idempotency",
),
Index("ix_wallet_ledger_account", "tenant_id", "wallet_account_id"),
Index("ix_wallet_ledger_member", "tenant_id", "member_id"),
Index("ix_wallet_ledger_type", "tenant_id", "entry_type"),
Index("ix_wallet_ledger_created", "tenant_id", "created_at"),
)