feat(billing): 任务换额度定稿闭环——claim 凭据校验(40311)+ 定稿 4 任务目录
- claim_task_bonus 在幂等快捷路径前做完成凭据校验:report_feedback 查当日 rp_feedback、first_report 查 succeeded rp_report,无凭据 40311 且不打流水;sign_in/share_report 不校验(产品决策:签到行为即领取,分享内测期接受端侧自报,hard_cap 锁死刷量收益) - task_bonus_rules 默认值与 .env.example 同步产品定稿(产品方案 §3.2,2026-09-11):sign_in 对话+10/day、report_feedback 发型+2/day、share_report 换装+3/day、first_report 重生成+1/lifetime;下架 invite_friend/profile_complete - billing/repo 新增只读凭据探测 has_feedback_between/has_completed_report(import-linter 禁 billing→report,直查 shared.models 的 rp_* 表,永远只读) - 测试:test_task_bonus_eligibility 5 用例(40311 两分支/凭据后 200/40910/40410/零流水) 偏差说明:test_task_bonus 与 test_task_bonus_rules 断言数值随定稿更新(发放/幂等/封顶语义未动);quota.py docstring「占位值」说法顺手更正。
This commit is contained in:
@@ -86,15 +86,15 @@ async def test_claim_grants_immediately_and_repeats_are_rejected(client: Any) ->
|
||||
assert body["data"] == {
|
||||
"task_key": "sign_in",
|
||||
"quota_key": "agent_chat",
|
||||
"amount": 1, # 占位值;正式数值由产品定稿后改 TASK_BONUS_RULES_JSON
|
||||
"bonus_granted": 1,
|
||||
"remaining": 11, # base_limit 10 + bonus 1 - used 0
|
||||
"amount": 10, # §3.2 定稿值(T2b);改 TASK_BONUS_RULES_JSON 即生效
|
||||
"bonus_granted": 10,
|
||||
"remaining": 20, # base_limit 10 + bonus 10 - used 0
|
||||
}
|
||||
|
||||
# 发放即到账:额度视图立刻可见。
|
||||
view = await _quota_view(client, headers, "agent_chat")
|
||||
assert view["bonus_granted"] == 1
|
||||
assert view["remaining"] == 11
|
||||
assert view["bonus_granted"] == 10
|
||||
assert view["remaining"] == 20
|
||||
|
||||
# 同一任务同一周期重复 claim → 40910(FR-BILL-03 发放幂等)。
|
||||
again = await client.post("/api/v1/quota/tasks/sign_in/claim", headers=headers)
|
||||
@@ -108,43 +108,67 @@ async def test_claim_grants_immediately_and_repeats_are_rejected(client: Any) ->
|
||||
assert rows[0].status == "committed"
|
||||
assert rows[0].reason == "sign_in"
|
||||
assert rows[0].ref_type == "task_bonus"
|
||||
assert rows[0].balance_after == 10 + 1
|
||||
assert rows[0].balance_after == 10 + 10
|
||||
assert rows[0].idempotency_key.startswith(f"grant:{user_id}:sign_in:")
|
||||
|
||||
# 幂等拒绝不改变账面。
|
||||
view = await _quota_view(client, headers, "agent_chat")
|
||||
assert view["bonus_granted"] == 1
|
||||
assert view["bonus_granted"] == 10
|
||||
|
||||
|
||||
async def test_all_five_frbill03_tasks_land_on_their_quotas(client: Any) -> None:
|
||||
async def test_final_catalog_tasks_land_on_their_quotas(client: Any) -> None:
|
||||
headers, user_id = await _login(client)
|
||||
|
||||
# T2b 起 report_feedback 需当日 rp_feedback 凭据、first_report 需已完成
|
||||
# rp_report 凭据(否则 40311):先落齐两行。本用例聚焦「各任务落到各自
|
||||
# 配额」的发放语义,不覆盖凭据闸门(见 test_task_bonus_eligibility.py)。
|
||||
from packages.domain.ids import next_id
|
||||
from packages.shared.db import session_scope
|
||||
from packages.shared.models import RpFeedback, RpReport
|
||||
|
||||
async with session_scope() as session:
|
||||
report_id = next_id()
|
||||
session.add(RpReport(report_id=report_id, user_id=user_id, status="succeeded"))
|
||||
session.add(
|
||||
RpFeedback(
|
||||
feedback_id=next_id(),
|
||||
report_id=report_id,
|
||||
user_id=user_id,
|
||||
dimension_key="overall",
|
||||
verdict="accurate",
|
||||
)
|
||||
)
|
||||
|
||||
# §3.2 定稿目录(T2b):invite_friend/profile_complete 已下架,不在 env。
|
||||
all_tasks = (
|
||||
"sign_in",
|
||||
"profile_complete",
|
||||
"report_feedback",
|
||||
"share_report",
|
||||
"invite_friend",
|
||||
"first_report",
|
||||
)
|
||||
for task_key in all_tasks:
|
||||
resp = await client.post(f"/api/v1/quota/tasks/{task_key}/claim", headers=headers)
|
||||
assert resp.status_code == 200, task_key
|
||||
assert resp.json()["code"] == 0, task_key
|
||||
|
||||
# agent_chat 吃两个任务(sign_in + profile_complete);tryon 吃两个;report_regen 一个。
|
||||
# 定稿数值:sign_in→agent_chat+10、report_feedback→hairstyle+2、
|
||||
# share_report→tryon+3、first_report→report_regen+1,各自落各自配额。
|
||||
chat = await _quota_view(client, headers, "agent_chat")
|
||||
assert chat["bonus_granted"] == 2
|
||||
assert chat["remaining"] == 12
|
||||
assert chat["bonus_granted"] == 10
|
||||
assert chat["remaining"] == 20
|
||||
hairstyle = await _quota_view(client, headers, "hairstyle")
|
||||
assert hairstyle["bonus_granted"] == 2
|
||||
assert hairstyle["remaining"] == 5 # base 3 + bonus 2(task_bonus_max=2 触顶即止)
|
||||
tryon = await _quota_view(client, headers, "tryon")
|
||||
assert tryon["bonus_granted"] == 2
|
||||
assert tryon["remaining"] == 5 # base 3 + bonus 2(task_bonus_max=3 未触顶)
|
||||
assert tryon["bonus_granted"] == 3
|
||||
assert tryon["remaining"] == 6 # base 3 + bonus 3(task_bonus_max=3 未触顶)
|
||||
regen = await _quota_view(client, headers, "report_regen")
|
||||
assert regen["bonus_granted"] == 1
|
||||
assert regen["remaining"] == 2
|
||||
|
||||
rows = await _system_grant_rows(user_id)
|
||||
assert len(rows) == 5
|
||||
assert len({row.idempotency_key for row in rows}) == 5
|
||||
assert len(rows) == 4
|
||||
assert len({row.idempotency_key for row in rows}) == 4
|
||||
|
||||
|
||||
async def test_unknown_task_returns_40410(client: Any) -> None:
|
||||
|
||||
Reference in New Issue
Block a user