c1ae195141
Закрывает 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>