Files
eryao/backend/tests/unit/test_invite_redeem_points.py
T

195 lines
6.1 KiB
Python
Raw Normal View History

2026-05-21 16:26:58 +08:00
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