gendesign/tradein-mvp/backend/app
bot-backend 850642548e fix(tradein): atomic conditional quota increment — close TOCTOU (#747)
check_and_raise (SELECT) and increment (unconditional UPSERT) were 5-40s apart
(the estimate scrapes run between them), so two concurrent /estimate at used=14
both passed the check and both incremented → used=16, pilot exceeds 15/mo.

Make increment atomic-conditional: ON CONFLICT DO UPDATE SET used=used+1
WHERE used < :lim RETURNING used → returns bool. The WHERE applies only to the
DO UPDATE branch, so a fresh insert (used=1) always succeeds; a conflicting row
already at the limit is not updated → RETURNING empty → False. trade_in raises
429 when increment returns False. check_and_raise kept as a fast pre-check.

Tests: 4 new (atomic refuse-at-limit incl. used stays == limit not +2; fresh
insert succeeds; returns bool; unlimited no-op) via an in-memory fake modeling
the conditional UPSERT (CI has no Postgres). pytest -k quota → 36 passed.

Refs #747
2026-05-30 18:49:41 +03:00
..
api fix(tradein): atomic conditional quota increment — close TOCTOU (#747) 2026-05-30 18:49:41 +03:00
core feat(tradein): brand-by-account + result IA redesign (distinct asking/sale prices) (#683) 2026-05-30 06:45:14 +00:00
observability feat(tradein): enrich GlitchTip SDK init — integrations + PII scrub (#396) (#643) 2026-05-29 09:23:17 +00:00
schemas docs(tradein): clarify n_analogs vs analogs[] top-10 cap (#698) (#737) 2026-05-30 15:00:48 +00:00
services fix(tradein): atomic conditional quota increment — close TOCTOU (#747) 2026-05-30 18:49:41 +03:00
tasks feat(tradein): daily asking→sold ratio refresh (#648 S4) (#663) 2026-05-29 13:49:36 +00:00
__init__.py feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
main.py feat(tradein): enrich GlitchTip SDK init — integrations + PII scrub (#396) (#643) 2026-05-29 09:23:17 +00:00