"""``GET /quota``、``GET /quota/ledger``、``POST /quota/tasks/{task_key}/claim``. FR-BILL-07: 额度相关文案中**不得出现任何价格、购买、会员、升级字样**。 Only two recovery paths may ever be surfaced: 等待恢复 and 做任务换额度. """ from __future__ import annotations from fastapi import APIRouter, Path, Query from apps.api.deps import BillingServiceDep, CurrentUserId, SettingsDep from apps.api.schemas import LedgerEntry, QuotaResponse from packages.agent.service import QUOTA_KEY_AGENT_CHAT from packages.billing.schemas import TaskBonusClaimResponse from packages.shared.response import ok router = APIRouter(prefix="/quota", tags=["quota"]) @router.get("", summary="Current period quota for a feature") async def get_quota( user_id: CurrentUserId, billing: BillingServiceDep, quota_key: str = Query(default=QUOTA_KEY_AGENT_CHAT, max_length=64), ) -> dict[str, object]: snapshot = await billing.get_quota(user_id=user_id, quota_key=quota_key) return ok( QuotaResponse( quota_key=snapshot.quota_key, period_key=snapshot.period_key, used=snapshot.used, limit_value=snapshot.limit_value, bonus_granted=snapshot.bonus_granted, remaining=snapshot.remaining, reset_at=snapshot.reset_at, ).model_dump() ) @router.get("/ledger", summary="Quota ledger — 流水是真相,余额是缓存") async def get_ledger( user_id: CurrentUserId, billing: BillingServiceDep, limit: int = Query(default=50, ge=1, le=200), ) -> dict[str, object]: rows = await billing.list_ledger(user_id=user_id, limit=limit) return ok( [ LedgerEntry( tx_id=str(row.tx_id), delta=row.delta, tx_type=row.tx_type, status=row.status, reason=row.reason, quota_key=row.quota_key, period_key=row.period_key, balance_after=row.balance_after, created_at=row.created_at, ).model_dump() for row in rows ] ) @router.post("/tasks/{task_key}/claim", summary="做任务换额度:领取任务奖励(FR-BILL-03)") async def claim_task_bonus( user_id: CurrentUserId, billing: BillingServiceDep, settings: SettingsDep, task_key: str = Path(max_length=64), ) -> dict[str, object]: """同一任务同一周期只能领一次(重复领返回 40910),受规则封顶(40310). 奖励数值由 ``TASK_BONUS_RULES_JSON`` 配置驱动(FR-BILL-04),2026-09-11 起默认值 即产品定稿(产品方案 §3.2);report_feedback/first_report 有服务端凭据校验(40311)。 """ claim = await billing.claim_task_bonus( user_id=user_id, task_key=task_key, rules=dict(settings.task_bonus_rules), ) return ok( TaskBonusClaimResponse( task_key=claim.task_key, quota_key=claim.quota_key, amount=claim.amount, bonus_granted=claim.bonus_granted, remaining=claim.remaining, ).model_dump() )