fix(tradein/quota): per-user quota override + no burn on empty result #2517
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2517
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-quota-per-user-and-no-burn"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
usedbonus hack with a properaccount_quota_overrides(username, monthly_limit)table (migration184_account_quota_overrides.sql).account_quota.user_limit(db, username)reads the override and is now used acrossget_status/check_and_raise/incrementinstead of the hardcoded globalMONTHLY_LIMIT(incl. the atomicWHERE used < :limpredicate).get_statusclampsremaining = max(0, limit - max(0, used))so remaining can never exceed limit even if a negativeusedleaks in again. Migration seedsuser2(Брусника) ->monthly_limit=50and resets the old hack (used < 0 -> 0) so/quotanow returns{limit:50, used:0, remaining:50}instead of the absurd{limit:15, used:-35, remaining:50}.POST /estimateno longer unconditionally callsaccount_quota.increment()afterestimate_quality. When the address fails to geocode and the handler falls back to_empty_estimate(median=0, n_analogs=0,insufficient_data=True, HTTP 200), the quota slot is preserved -- increment is now gated onnot result.insufficient_data. The#747TOCTOU-safe atomic increment/429 gate is unchanged for real (non-empty) results.Test plan
cd tradein-mvp/backend && uv run --frozen python -m pytest tests/test_account_quota.py -q-> 44 passed (added override + insufficient-data coverage)uv run --frozen python -m pytest tests/test_api_avito_stage4a.py tests/test_estimate_idor.py tests/test_estimator_anchor_dedup_gap_2265.py tests/test_sell_time_sensitivity.py -q-> 38 passed (other/estimatecallers unaffected)uv run --with ruff ruff check app/services/account_quota.py app/api/v1/trade_in.py tests/test_account_quota.py-> cleanGET /api/v1/trade-in/quotafor user2 returns{limit:50, used:0, remaining:50}after migration 184 auto-appliesTwo bugs hit the first paying pilot (user2/Брусника): 1. Bonus attempts were granted via negative `used` hack (account_estimate_usage.used = -35), which made /quota return {limit:15, used:-35, remaining:50} -> frontend rendered the absurd "Осталось 50 из 15". Replaced with a proper personal monthly limit: account_quota_overrides table (migration 184) + account_quota.user_limit() used across get_status/check_and_raise/increment instead of the hardcoded global MONTHLY_LIMIT. get_status now clamps remaining = max(0, limit - max(0, used)) so remaining can never exceed limit even if a negative used leaks through again. Migration seeds user2 -> monthly_limit=50 and resets the old hack (used<0 -> 0), so user2 now sees "50 из 50". 2. POST /estimate unconditionally called account_quota.increment() after estimate_quality, even when the address failed to geocode and the handler returned the _empty_estimate fallback (median=0, n_analogs=0, insufficient_data=True, HTTP 200) -- burning a paid slot for a null result. Now increment is skipped when result.insufficient_data is True; the #747 TOCTOU-safe atomic increment/429 gate is unchanged for real results.