from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timezone from types import SimpleNamespace from uuid import UUID, uuid4 import pytest from v1.points.invite_rewards import grant_invite_rewards_for_creem_payment from v1.points.service import PointsService @dataclass class _FakeAccount: balance: int = 0 frozen_balance: int = 0 lifetime_earned: int = 0 lifetime_spent: int = 0 version: int = 0 class _FakePointsRepository: def __init__(self) -> None: self.accounts: dict[UUID, _FakeAccount] = {} self.redeem_code: SimpleNamespace | None = None self.referral: SimpleNamespace | None = None self.user_emails: dict[UUID, str] = {} self.ledgers: list[object] = [] self.audit_ledgers: list[object] = [] self.system_audits: list[dict[str, object]] = [] self.committed = False async def get_redeem_code_for_update(self, *, code: str) -> SimpleNamespace | None: if self.redeem_code is None or self.redeem_code.code != code: return None return self.redeem_code async def get_or_create_user_points_for_update( self, *, user_id: UUID ) -> _FakeAccount: return self.accounts.setdefault(user_id, _FakeAccount()) async def append_ledger(self, *, command: object, balance_after: int) -> None: self.ledgers.append(command) async def append_audit_ledger(self, *, command: object) -> None: self.audit_ledgers.append(command) async def append_system_audit_log( self, *, actor_user_id: UUID | None, target_user_id: UUID | None, action: str, entity_type: str, entity_id: UUID | None, metadata: dict[str, object], ) -> None: self.system_audits.append( { "actor_user_id": actor_user_id, "target_user_id": target_user_id, "action": action, "entity_type": entity_type, "entity_id": entity_id, "metadata": metadata, } ) async def commit(self) -> None: self.committed = True async def get_referral_by_invitee_for_update( self, *, invitee_user_id: UUID ) -> SimpleNamespace | None: if self.referral is None or self.referral.invitee_user_id != invitee_user_id: return None return self.referral async def get_user_email(self, *, user_id: UUID) -> str | None: return self.user_emails.get(user_id) @pytest.mark.asyncio async def test_redeem_code_credits_account_and_audits() -> None: user_id = uuid4() code_id = uuid4() repo = _FakePointsRepository() repo.redeem_code = SimpleNamespace( id=code_id, code="ABCD2345", status="active", credits=100, package_product_code="starter_pack", package_name_snapshot="starter_pack", redeemed_by_user_id=None, redeemed_at=None, redeem_event_id=None, ) result = await PointsService(repository=repo).redeem_code( user_id=user_id, user_email="buyer@example.com", raw_code="abcd2345", ) assert result.credits == 100 assert result.balance_after == 100 assert repo.accounts[user_id].balance == 100 assert repo.redeem_code.status == "redeemed" assert repo.redeem_code.redeemed_by_user_id == user_id assert len(repo.ledgers) == 1 assert len(repo.audit_ledgers) == 1 assert repo.system_audits[0]["action"] == "redeem_code.activate" assert repo.committed is True @pytest.mark.asyncio async def test_creem_payment_after_binding_grants_invite_rewards_to_both_users() -> ( None ): inviter_id = uuid4() invitee_id = uuid4() referral_id = uuid4() creem_transaction_id = uuid4() paid_at = datetime.now(timezone.utc) repo = _FakePointsRepository() repo.user_emails[inviter_id] = "inviter@example.com" repo.referral = SimpleNamespace( id=referral_id, inviter_user_id=inviter_id, invitee_user_id=invitee_id, first_creem_transaction_id=None, first_creem_paid_at=None, inviter_reward_event_id=None, invitee_reward_event_id=None, inviter_reward_granted_at=None, invitee_reward_granted_at=None, ) await grant_invite_rewards_for_creem_payment( repository=repo, invitee_user_id=invitee_id, invitee_email="invitee@example.com", creem_transaction_id=creem_transaction_id, paid_at=paid_at, ) assert repo.accounts[inviter_id].balance == 40 assert repo.accounts[invitee_id].balance == 40 assert repo.referral.first_creem_transaction_id == creem_transaction_id assert repo.referral.inviter_reward_granted_at == paid_at assert repo.referral.invitee_reward_granted_at == paid_at assert len(repo.ledgers) == 2 assert len(repo.audit_ledgers) == 2 assert repo.system_audits[0]["action"] == "invite.reward_grant" @pytest.mark.asyncio async def test_creem_payment_after_existing_completed_payment_still_grants_binding_reward() -> ( None ): inviter_id = uuid4() invitee_id = uuid4() repo = _FakePointsRepository() repo.referral = SimpleNamespace( id=uuid4(), inviter_user_id=inviter_id, invitee_user_id=invitee_id, first_creem_transaction_id=None, first_creem_paid_at=None, inviter_reward_event_id=None, invitee_reward_event_id=None, inviter_reward_granted_at=None, invitee_reward_granted_at=None, ) creem_transaction_id = uuid4() paid_at = datetime.now(timezone.utc) await grant_invite_rewards_for_creem_payment( repository=repo, invitee_user_id=invitee_id, invitee_email="invitee@example.com", creem_transaction_id=creem_transaction_id, paid_at=paid_at, ) assert repo.accounts[inviter_id].balance == 40 assert repo.accounts[invitee_id].balance == 40 assert repo.referral.first_creem_transaction_id == creem_transaction_id assert repo.referral.inviter_reward_granted_at == paid_at assert len(repo.ledgers) == 2 assert len(repo.audit_ledgers) == 2