99 lines
3.9 KiB
Python
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"),
|
|
)
|