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:
xsl
2026-09-12 00:23:43 +08:00
parent b9c3e4dada
commit 76a0502ea3
9 changed files with 365 additions and 44 deletions
+10 -7
View File
@@ -14,13 +14,13 @@ import pytest
from packages.domain.billing.quota import PERIOD_TYPES
from packages.shared.config import Settings
#: FR-BILL-03 的五类任务:签到、完善资料、反馈、分享、邀请。
_FR_BILL_03_TASKS = (
#: 产品方案 §3.2 定稿目录(2026-09-11):签到、反馈、分享、首次速测;
#: invite_friend/profile_complete 无可信完成信号,已下架(不在默认值里)。
_FINAL_CATALOG_TASKS = (
"sign_in",
"profile_complete",
"report_feedback",
"share_report",
"invite_friend",
"first_report",
)
@@ -29,10 +29,13 @@ def _rules_without_env() -> dict[str, dict[str, object]]:
return dict(Settings(_env_file=None).task_bonus_rules)
def test_default_rules_cover_all_five_frbill03_tasks() -> None:
def test_default_rules_cover_the_final_catalog() -> None:
rules = _rules_without_env()
for task_key in _FR_BILL_03_TASKS:
for task_key in _FINAL_CATALOG_TASKS:
assert task_key in rules, task_key
# 下架任务不得出现在默认值里(不下发即下架,claim 返回 40410)。
assert "invite_friend" not in rules
assert "profile_complete" not in rules
def test_every_default_rule_entry_has_a_valid_shape() -> None:
@@ -42,7 +45,7 @@ def test_every_default_rule_entry_has_a_valid_shape() -> None:
assert isinstance(quota_key, str) and quota_key, task_key
amount = conf.get("amount")
assert isinstance(amount, int) and not isinstance(amount, bool), task_key
assert amount > 0, task_key # 占位值 1,正式数值由产品定稿后改 env
assert amount > 0, task_key # 定稿数值(§3.2),改 env 即生效
assert conf.get("claim_period") in PERIOD_TYPES, task_key