145 lines
5.3 KiB
Python
145 lines
5.3 KiB
Python
"""后台退款(积分+返利资产扣回)与 iOS 支付开关"""
|
||
import uuid
|
||
from datetime import datetime, timedelta
|
||
from decimal import Decimal
|
||
from types import SimpleNamespace
|
||
|
||
import pytest
|
||
from sqlalchemy import select
|
||
|
||
from app.core.response import BizException
|
||
from app.models.admin import AdminLog
|
||
from app.models.billing import RechargeOrder
|
||
from app.models.common import SystemConfig
|
||
from app.models.incentive import AssetTransaction, AssetWallet
|
||
from app.models.user import User, UserWallet
|
||
from app.services import admin_service, payment_service
|
||
|
||
|
||
async def _user(db, *, points=0):
|
||
u = User(password_hash="x", register_type=1, invite_code=uuid.uuid4().hex[:8].upper())
|
||
db.add(u)
|
||
await db.flush()
|
||
db.add(UserWallet(user_id=u.id, points=points))
|
||
await db.commit()
|
||
return u
|
||
|
||
|
||
async def _paid_order(db, user_id, order_no="R20261015001", *, status=1, points=100):
|
||
o = RechargeOrder(
|
||
order_no=order_no, user_id=user_id, amount_fen=1000, points=points,
|
||
pay_method=1, status=status, paid_at=datetime.now(),
|
||
expire_at=datetime.now() + timedelta(minutes=30),
|
||
)
|
||
db.add(o)
|
||
await db.commit()
|
||
return o
|
||
|
||
|
||
def _admin():
|
||
return SimpleNamespace(admin_id=1, role="super")
|
||
|
||
|
||
# ---------- 退款 ----------
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_refund_claws_back_points_and_rebate(db):
|
||
buyer = await _user(db, points=150)
|
||
inviter = await _user(db)
|
||
db.add(AssetWallet(user_id=inviter.id, assets=Decimal(5)))
|
||
db.add(AssetTransaction(
|
||
user_id=inviter.id, change_amount=Decimal(5), balance_after=Decimal(5),
|
||
biz_type=1, biz_id="R20261015001", remark="充值返利",
|
||
))
|
||
order = await _paid_order(db, buyer.id)
|
||
|
||
r = await admin_service.refund_order(
|
||
db, _admin(), order.order_no, SimpleNamespace(reason="用户申请退款"), "127.0.0.1"
|
||
)
|
||
assert r["order_no"] == order.order_no
|
||
await db.refresh(order)
|
||
assert order.status == 3 # 已退款
|
||
# 积分扣回(biz_type=7)
|
||
assert (await db.get(UserWallet, buyer.id)).points == 50
|
||
# 返利资产扣回(biz_type=6 反向流水)
|
||
assert (await db.get(AssetWallet, inviter.id)).assets == Decimal(0)
|
||
clawback = (await db.execute(select(AssetTransaction).where(
|
||
AssetTransaction.biz_type == 6, AssetTransaction.biz_id == order.order_no
|
||
))).scalar_one()
|
||
assert clawback.change_amount == Decimal(-5)
|
||
# 操作日志落库
|
||
log = (await db.execute(select(AdminLog).where(AdminLog.action == "order_refund"))).scalar_one()
|
||
assert log.target == order.order_no
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_refund_repeat_rejected(db):
|
||
buyer = await _user(db, points=150)
|
||
order = await _paid_order(db, buyer.id)
|
||
await admin_service.refund_order(db, _admin(), order.order_no, SimpleNamespace(reason="退款"), "")
|
||
with pytest.raises(BizException) as exc:
|
||
await admin_service.refund_order(db, _admin(), order.order_no, SimpleNamespace(reason="再退"), "")
|
||
assert exc.value.code == 3002 # ORDER_STATUS_ERROR
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_refund_unpaid_order_rejected(db):
|
||
buyer = await _user(db)
|
||
order = await _paid_order(db, buyer.id, order_no="R_UNPAID", status=0)
|
||
with pytest.raises(BizException) as exc:
|
||
await admin_service.refund_order(db, _admin(), order.order_no, SimpleNamespace(reason="退款"), "")
|
||
assert exc.value.code == 3002
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_refund_not_found(db):
|
||
with pytest.raises(BizException) as exc:
|
||
await admin_service.refund_order(db, _admin(), "NO_SUCH", SimpleNamespace(reason="退款"), "")
|
||
assert exc.value.code == 3001 # ORDER_NOT_FOUND
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_refund_points_can_go_negative(db):
|
||
"""积分已被消费时扣回允许余额为负(对账兜底)"""
|
||
buyer = await _user(db, points=30)
|
||
order = await _paid_order(db, buyer.id) # 该单发了 100 积分
|
||
await admin_service.refund_order(db, _admin(), order.order_no, SimpleNamespace(reason="退款"), "")
|
||
assert (await db.get(UserWallet, buyer.id)).points == -70
|
||
|
||
|
||
# ---------- iOS 支付开关 ----------
|
||
|
||
def _recharge_req():
|
||
return SimpleNamespace(amount_yuan=6, pay_method=3) # USDT 渠道(离线可建单)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ios_blocked_when_switch_off(db, redis):
|
||
db.add(SystemConfig(config_key="ios_payment.enabled", config_value="0"))
|
||
await db.commit()
|
||
with pytest.raises(BizException) as exc:
|
||
await payment_service.create_recharge(db, redis, 1, _recharge_req(), platform="ios")
|
||
assert "iOS" in exc.value.message
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_android_not_affected_by_ios_switch(db, redis):
|
||
db.add(SystemConfig(config_key="ios_payment.enabled", config_value="0"))
|
||
await db.commit()
|
||
r = await payment_service.create_recharge(db, redis, 1, _recharge_req(), platform="android")
|
||
assert r["order_no"] and r["pay_method"] == 3
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ios_allowed_when_switch_on(db, redis):
|
||
db.add(SystemConfig(config_key="ios_payment.enabled", config_value="1"))
|
||
await db.commit()
|
||
r = await payment_service.create_recharge(db, redis, 1, _recharge_req(), platform="ios")
|
||
assert r["order_no"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ios_default_allowed_without_config(db, redis):
|
||
r = await payment_service.create_recharge(db, redis, 1, _recharge_req(), platform="ios")
|
||
assert r["order_no"]
|