Files
yingqing/core/backend/apps/billing/tests.py
T
zycandClaude Opus 4.8 6e7425048c fix: PMC 测试清单未完成项 + YYX#row22 未读角标
PMC#16 账户登录地址不正确:
- 写死的 https://airshelf.com/login(不存在的域名)改成 ${location.origin}/login,
  与邀请链接同源,成员拿到的就是当前真实访问地址。

PMC#17/#18 成员三档额度只有月度生效、日/总纯装饰 → 列表显示与配置对不上:
- TeamMember 新增 daily_credit_limit / total_credit_limit(0=不限,迁移 0006);
- reserve_credit 逐档真实管控(每日/每月/累计总额),口径=窗口内 CHARGE 流水 + 在途预留;
- 成员列表"每日额度"由写死"不限"改成真实日额度+今日已用,并新增"总额度"列;
- 创建/编辑弹窗三档额度真入库真生效;补 3 个计费单测(日/总拦截、0=不限)。

PMC#24 同批生成的图片文字/内容雷同:
- 根因=套图/自由创作同批 N 张用同一 prompt 且无变化(只有模特上身图按 index 变);
- 给平台套图/自由参考/纯文生图都加按 index 的版式变体,避开传 seed(防中转站 400)。

PMC#25 重跑应在原任务上重跑而非新开任务:
- 全批"重跑"改为原地复位同一批次卡;"重跑这张/再生成"改为补图进原卡(不覆盖好图、不新开任务)。

YYX#row22 图片生成未读角标(本仓库既有改动一并提交):
- AITask 新增 read_at(迁移 0020 + 索引一致性迁移 0021);导航栏"图片生成"未读胶囊、
  商品预览右下角未读分数;查看某商品即标记其生成任务已读、清零角标。

注:PMC#14/#15/#20/#22/#23 早前提交已修,需部署重测;PMC#13/#21 为 provider 侧
生成失败,代码侧重试/退费/僵尸回收/错误外露已就位,需部署后看真实 error_message 定位;
PMC#19 收件箱标记与设计稿逐字一致,待截图复现。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 18:27:15 +08:00

130 lines
6.6 KiB
Python

from decimal import Decimal
from django.test import TestCase
from rest_framework.test import APIClient
from apps.accounts.models import Team, TeamMember, User
from apps.ai.models import AITask, ModelConfig, ModelProvider
from apps.billing.models import CreditAccount, CreditLedger, CreditReservation
from apps.billing.services.ledger import charge_reserved_credit, release_credit, reserve_credit
class CreditLedgerTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="owner", password="pass")
self.team = Team.objects.create(name="Billing Team", owner=self.user)
TeamMember.objects.create(team=self.team, user=self.user, role=TeamMember.Role.OWNER)
self.account = CreditAccount.objects.create(team=self.team, balance=Decimal("100.0000"))
self.provider = ModelProvider.objects.create(name="volcengine", display_name="Volcano")
self.model = ModelConfig.objects.create(
provider=self.provider,
name="doubao-seed-2-0-pro-260215",
display_name="Doubao",
capability=ModelConfig.Capability.TEXT,
)
self.task = AITask.objects.create(
team=self.team,
created_by=self.user,
task_type=AITask.Type.SCRIPT_GENERATION,
model_config=self.model,
idempotency_key="billing-test-task",
estimated_cost=Decimal("10.0000"),
)
def test_reserve_and_charge_credit(self):
reservation = reserve_credit(team=self.team, user=self.user, task=self.task, amount=Decimal("10.0000"))
self.account.refresh_from_db()
self.assertEqual(reservation.status, CreditReservation.Status.ACTIVE)
self.assertEqual(self.account.balance, Decimal("100.0000"))
self.assertEqual(self.account.reserved_balance, Decimal("10.0000"))
charge_reserved_credit(reservation=reservation, actual_amount=Decimal("8.0000"))
self.account.refresh_from_db()
reservation.refresh_from_db()
self.assertEqual(reservation.status, CreditReservation.Status.CHARGED)
self.assertEqual(self.account.balance, Decimal("92.0000"))
self.assertEqual(self.account.reserved_balance, Decimal("0.0000"))
self.assertEqual(CreditLedger.objects.filter(team=self.team).count(), 3)
def test_release_reserved_credit(self):
reservation = reserve_credit(team=self.team, user=self.user, task=self.task, amount=Decimal("10.0000"))
release_credit(reservation=reservation, reason="model failed")
self.account.refresh_from_db()
reservation.refresh_from_db()
self.assertEqual(reservation.status, CreditReservation.Status.RELEASED)
self.assertEqual(self.account.balance, Decimal("100.0000"))
self.assertEqual(self.account.reserved_balance, Decimal("0.0000"))
self.assertEqual(CreditLedger.objects.filter(ledger_type=CreditLedger.Type.RELEASE).count(), 1)
class MemberLimitTests(TestCase):
"""成员三档额度(每日/每月/累计总额,0=不限)在 reserve_credit 时逐档管控(PMC#17/#18)。"""
def setUp(self):
self.user = User.objects.create_user(username="lim-owner", password="pass12345")
self.team = Team.objects.create(name="Limit Team", owner=self.user)
self.member = TeamMember.objects.create(team=self.team, user=self.user, role=TeamMember.Role.OWNER)
CreditAccount.objects.create(team=self.team, balance=Decimal("1000.0000"))
self.provider = ModelProvider.objects.create(name="volcengine", display_name="Volcano")
self.model = ModelConfig.objects.create(
provider=self.provider, name="m", display_name="M", capability=ModelConfig.Capability.IMAGE,
)
def _task(self, key):
return AITask.objects.create(
team=self.team, created_by=self.user, task_type=AITask.Type.PRODUCT_IMAGE,
model_config=self.model, idempotency_key=key, estimated_cost=Decimal("10.0000"),
)
def test_daily_limit_blocks_over_spend(self):
self.member.daily_credit_limit = Decimal("15.0000")
self.member.save(update_fields=["daily_credit_limit"])
# 第一笔 10 预留通过,第二笔 10 超每日 15 → 被拦
reserve_credit(team=self.team, user=self.user, task=self._task("d1"), amount=Decimal("10.0000"))
with self.assertRaises(ValueError):
reserve_credit(team=self.team, user=self.user, task=self._task("d2"), amount=Decimal("10.0000"))
def test_total_limit_blocks_over_spend(self):
self.member.total_credit_limit = Decimal("12.0000")
self.member.save(update_fields=["total_credit_limit"])
reserve_credit(team=self.team, user=self.user, task=self._task("t1"), amount=Decimal("10.0000"))
with self.assertRaises(ValueError):
reserve_credit(team=self.team, user=self.user, task=self._task("t2"), amount=Decimal("10.0000"))
def test_zero_limit_means_unlimited(self):
# 三档都 0 = 不限:多笔大额预留都不被额度档拦(只受账户余额约束)
for i in range(5):
reserve_credit(team=self.team, user=self.user, task=self._task(f"z{i}"), amount=Decimal("10.0000"))
class RechargePermissionTests(TestCase):
"""充值是团队资金操作:仅 owner/admin 可发起,普通成员/访客被 403 拦截。"""
def setUp(self):
self.owner = User.objects.create_user(username="r-owner", password="ownerpass123")
self.member = User.objects.create_user(username="r-member", password="memberpass123")
self.team = Team.objects.create(name="Recharge Team", owner=self.owner)
TeamMember.objects.create(team=self.team, user=self.owner, role=TeamMember.Role.OWNER, status=TeamMember.Status.ACTIVE)
TeamMember.objects.create(team=self.team, user=self.member, role=TeamMember.Role.MEMBER, status=TeamMember.Status.ACTIVE)
CreditAccount.objects.create(team=self.team, balance=Decimal("0.0000"))
def test_owner_can_recharge(self):
client = APIClient()
client.force_authenticate(self.owner)
response = client.post("/api/billing/recharge/", {"amount": "100"}, format="json")
self.assertEqual(response.status_code, 201)
account = CreditAccount.objects.get(team=self.team)
self.assertEqual(account.balance, Decimal("100.0000"))
def test_member_cannot_recharge(self):
client = APIClient()
client.force_authenticate(self.member)
response = client.post("/api/billing/recharge/", {"amount": "100"}, format="json")
self.assertEqual(response.status_code, 403)
account = CreditAccount.objects.get(team=self.team)
self.assertEqual(account.balance, Decimal("0.0000")) # 余额未变,越权被拦