gendesign/tradein-mvp/backend/app/api/v1
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
..
__init__.py feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
admin.py feat(scrapers): wire N1.ru city sweep — pipeline + scheduler + admin endpoint (#575) (#745) 2026-05-30 15:43:38 +00:00
brand.py feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
geocode.py feat(tradein): MapPicker snap marker — Phase 5 of #582 (#594) 2026-05-27 09:30:17 +00:00
me.py feat(rbac): role-based access control via X-Authenticated-User middleware (#585) 2026-05-26 06:18:40 +00:00
search.py feat(tradein): Phase 3.2 — /api/v1/search endpoint + Redis cache (#479) 2026-05-23 14:44:22 +00:00
trade_in.py fix(tradein): atomic conditional quota increment — close TOCTOU (#747) 2026-05-30 18:49:41 +03:00