Files
portal/app/tests/Feature/Jobs
Дмитрий c1ae195141 fix(jobs): RouteSupplierLeadJob — lockForUpdate Project + recheck delivered_today (Plan 2.5 #2)
Закрывает CV.11 audit BLOCKER #2 (Plan 2/5 closure).

Проблема: matchEligibleProjects делал SELECT delivered_today < limit БЕЗ lockForUpdate.
Между snapshot'ом и createDealCopyForProject (который инкрементит счётчик) — окно
для concurrent webhook'а:
  worker A видит delivered_today=9, limit=10 → OK; createDealCopyForProject → 10.
  worker B параллельно видит то же 9 → OK; createDealCopyForProject → 11. OVERCOMMIT.
Лимит daily_limit_target нарушен, баланс tenant'а списан дважды.

Fix: внутри createDealCopyForProject (после lockForUpdate Tenant) — lockForUpdate(Project)
+ recheck delivered_today >= COALESCE(effective_daily_limit_today, daily_limit_target).
Если уже at-limit под блокировкой → return false без charge / counter / deal-row +
Log::info('supplier_lead.project_at_limit_skipped') для observability.

TDD: новый тест 'rejects deal copy if delivered_today >= limit at lock time' симулирует
race через Mockery — мокнутый LeadRouter возвращает project уже at-limit
(delivered_today=1, daily_limit_target=1), как будто matchEligibleProjects делал SELECT
когда delivered_today=0. Assert: deal НЕ создаётся, counter не растёт, balance не списан.

Pest: 549/547 passed (+2 от baseline 547), 1745 assertions, 19s parallel.
Larastan + Pint: passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:26:12 +03:00
..