195 lines
6.1 KiB
Python
195 lines
6.1 KiB
Python
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
|