188 lines
6.7 KiB
Python
188 lines
6.7 KiB
Python
"""结算服务:推荐激活(72h 窗口+次日登录)/ 创作收益增量结算 / 订单超时关单"""
|
||
import uuid
|
||
from datetime import date, datetime, timedelta
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
from sqlalchemy import select
|
||
|
||
from app.models.billing import RechargeOrder, WatchRecord
|
||
from app.models.incentive import AssetWallet, Referral, VideoEarning
|
||
from app.models.user import User, UserMembership, UserWallet
|
||
from app.services import settle_service
|
||
|
||
# config.yaml:register_reward_points=10 / activate_watch_count=3 / activate_hours=72
|
||
REWARD = 10
|
||
WATCH_N = 3
|
||
|
||
|
||
async def _user(db, *, member=False, last_login_at=None):
|
||
u = User(password_hash="x", register_type=1, last_login_at=last_login_at,
|
||
invite_code=uuid.uuid4().hex[:8].upper())
|
||
db.add(u)
|
||
await db.flush()
|
||
if member:
|
||
db.add(UserMembership(user_id=u.id, is_member=1, is_lifetime=1))
|
||
await db.commit()
|
||
return u
|
||
|
||
|
||
async def _referral(db, inviter_id, invitee_id, *, created_at=None, status=0):
|
||
r = Referral(
|
||
inviter_id=inviter_id, invitee_id=invitee_id, invite_code="CODE123",
|
||
status=status, created_at=created_at or datetime.now(),
|
||
)
|
||
db.add(r)
|
||
await db.commit()
|
||
return r
|
||
|
||
|
||
async def _watches(db, user_id, n):
|
||
for i in range(n):
|
||
db.add(WatchRecord(
|
||
subject_type=1, subject_id=str(user_id), video_id=100 + i,
|
||
watch_date=date.today(), cost_type=0,
|
||
))
|
||
await db.commit()
|
||
|
||
|
||
# ---------- 推荐激活结算 ----------
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_referral_activated_and_rewarded(db):
|
||
inviter = await _user(db, member=True)
|
||
invitee = await _user(db, last_login_at=datetime.now())
|
||
r = await _referral(db, inviter.id, invitee.id, created_at=datetime.now() - timedelta(days=1))
|
||
await _watches(db, invitee.id, WATCH_N)
|
||
|
||
result = await settle_service.settle_referral_activations(db)
|
||
assert result["activated"] == 1
|
||
await db.refresh(r)
|
||
assert r.status == 1 and r.activated_at is not None
|
||
# 推荐人是会员 → 发放注册奖励积分(biz_type=8)
|
||
assert (await db.get(UserWallet, inviter.id)).points == REWARD
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_referral_expired_after_72h(db):
|
||
inviter = await _user(db, member=True)
|
||
invitee = await _user(db, last_login_at=datetime.now())
|
||
r = await _referral(db, inviter.id, invitee.id, created_at=datetime.now() - timedelta(hours=73))
|
||
await _watches(db, invitee.id, WATCH_N)
|
||
|
||
result = await settle_service.settle_referral_activations(db)
|
||
assert result["expired"] == 1 and result["activated"] == 0
|
||
await db.refresh(r)
|
||
assert r.status == 2
|
||
assert await db.get(UserWallet, inviter.id) is None # 未发奖
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_referral_watches_not_enough(db):
|
||
inviter = await _user(db, member=True)
|
||
invitee = await _user(db, last_login_at=datetime.now())
|
||
r = await _referral(db, inviter.id, invitee.id, created_at=datetime.now() - timedelta(days=1))
|
||
await _watches(db, invitee.id, WATCH_N - 1)
|
||
|
||
result = await settle_service.settle_referral_activations(db)
|
||
assert result["activated"] == 0
|
||
await db.refresh(r)
|
||
assert r.status == 0 # 窗口内继续等待
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_referral_requires_next_day_login(db):
|
||
inviter = await _user(db, member=True)
|
||
# 注册与登录同一天 → 不满足「次日登录」
|
||
same_day = datetime.now()
|
||
invitee = await _user(db, last_login_at=same_day)
|
||
r = await _referral(db, inviter.id, invitee.id, created_at=same_day)
|
||
await _watches(db, invitee.id, WATCH_N)
|
||
|
||
result = await settle_service.settle_referral_activations(db)
|
||
assert result["activated"] == 0
|
||
await db.refresh(r)
|
||
assert r.status == 0
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_referral_non_member_inviter_no_reward(db):
|
||
inviter = await _user(db, member=False)
|
||
invitee = await _user(db, last_login_at=datetime.now())
|
||
r = await _referral(db, inviter.id, invitee.id, created_at=datetime.now() - timedelta(days=1))
|
||
await _watches(db, invitee.id, WATCH_N)
|
||
|
||
result = await settle_service.settle_referral_activations(db)
|
||
assert result["activated"] == 1 # 激活照常
|
||
await db.refresh(r)
|
||
assert r.status == 1
|
||
assert await db.get(UserWallet, inviter.id) is None # 非会员不发奖
|
||
|
||
|
||
# ---------- 视频创作收益结算 ----------
|
||
|
||
async def _earning(db, author_id, *, views=0, views_settled=0, likes=0, likes_settled=0):
|
||
e = VideoEarning(
|
||
video_id=999, author_id=author_id, complete_views=views,
|
||
views_settled=views_settled, likes_accrued=likes, likes_settled=likes_settled,
|
||
)
|
||
db.add(e)
|
||
await db.commit()
|
||
return e
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_earnings_settle_full_scale(db):
|
||
author = await _user(db, member=True)
|
||
# 满 2 个观看刻度(2000/1000)+ 2 个点赞刻度(200/100),余数保留
|
||
e = await _earning(db, author.id, views=2500, likes=250)
|
||
|
||
result = await settle_service.settle_video_earnings(db)
|
||
assert result["granted_assets"] == 4.0
|
||
await db.refresh(e)
|
||
assert e.views_settled == 2000 and e.likes_settled == 200
|
||
wallet = await db.get(AssetWallet, author.id)
|
||
assert wallet.assets == Decimal(4)
|
||
|
||
# 再跑一轮:余数不足刻度,不再发放(幂等)
|
||
result2 = await settle_service.settle_video_earnings(db)
|
||
assert result2["settled_videos"] == 0
|
||
await db.refresh(wallet)
|
||
assert wallet.assets == Decimal(4)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_earnings_non_member_not_settled(db):
|
||
author = await _user(db, member=False)
|
||
e = await _earning(db, author.id, views=5000, likes=500)
|
||
|
||
result = await settle_service.settle_video_earnings(db)
|
||
assert result["settled_videos"] == 0
|
||
await db.refresh(e)
|
||
assert e.views_settled == 0 and e.likes_settled == 0 # 刻度不推进
|
||
assert await db.get(AssetWallet, author.id) is None
|
||
|
||
|
||
# ---------- 订单超时关单 ----------
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_close_expired_orders(db):
|
||
def _order(no, status, expire_at):
|
||
return RechargeOrder(
|
||
order_no=no, user_id=1, amount_fen=100, points=10,
|
||
pay_method=1, status=status, expire_at=expire_at,
|
||
)
|
||
|
||
db.add(_order("R_EXP", 0, datetime.now() - timedelta(minutes=1))) # 待支付已过期 → 关单
|
||
db.add(_order("R_PEN", 0, datetime.now() + timedelta(minutes=30))) # 待支付未过期 → 保留
|
||
db.add(_order("R_PAY", 1, datetime.now() - timedelta(minutes=1))) # 已支付 → 不动
|
||
await db.commit()
|
||
|
||
result = await settle_service.close_expired_orders(db)
|
||
assert result == {"closed": 1}
|
||
status_map = {
|
||
o.order_no: o.status
|
||
for o in (await db.execute(select(RechargeOrder))).scalars()
|
||
}
|
||
assert status_map == {"R_EXP": 2, "R_PEN": 0, "R_PAY": 1}
|