"""后台退款(积分+返利资产扣回)与 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"]