После исчерпания всех 3 ретраев Laravel вызывает failed(\Throwable $e) —
упавший job сохраняется в failed_webhook_jobs для ручного разбора и
повторного запуска через админку.
Реализация:
- app/app/Models/FailedWebhookJob.php — Eloquent для failed_webhook_jobs
- ProcessWebhookJob::failed() через DB::table->insert (не Eloquent::create)
чтобы обойти RLS: failed-callback запускается вне транзакции воркера,
SET LOCAL app.current_tenant_id не выставлен, политика бы отвергла INSERT.
Запись должна попасть в БД даже в катастрофическом сценарии.
- payload через json_encode(JSON_UNESCAPED_UNICODE) — UTF-8 кириллица
сохраняется
Sentry::captureException оставлен как TODO для production (на dev-стеке
нет DSN).
3 новых Pest-теста:
- failed() пишет упавший job с webhookLogId (через DB::table('webhook_log')
для FK satisfaction)
- failed() работает БЕЗ webhookLogId (NULL ok — soft FK)
- failed() записывает payload с UTF-8 кириллицей корректно
Pest 48/48 зелёные за 4.7 сек. Pint + Larastan чисто.
Webhook-flow покрыт полностью на dev-стеке (за исключением Sentry и
SendNewLeadNotificationJob — Биз-20 Telegram, ждёт настоящего бота).
CLAUDE.md v1.15 → v1.16. Реестр Открытые_вопросы v1.24 → v1.25.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Закрыт Биз-19 (§10.8.1) на код-уровне. При создании НОВОЙ сделки сервис
DuplicateDetector ищет master по (tenant_id, phone) в окне 24 ч. Если
найден — новой сделке проставляется duplicate_of_id = master.id, баланс
НЕ списывается, SupplierLeadCost НЕ создаётся. ActivityLog пишется с
context.duplicate_of = master.id.
Реализация:
- app/app/Services/DuplicateDetector.php — отдельный сервис:
findMaster(tenantId, phone, ?Carbon $now): ?Deal. Ищет deals с
duplicate_of_id IS NULL и received_at >= now - 24h. Возвращает
первую по received_at ASC или null. WINDOW_HOURS = 24 — константа.
- App\Jobs\ProcessWebhookJob::handle() — после upsertDeal() для новой
сделки вызывает findMaster(). Если master !== создаваемая сделка —
markAsDuplicate(): UPDATE duplicate_of_id + ActivityLog с context.
- DI через app(DuplicateDetector::class) внутри handle() (не в
сигнатуре — для совместимости с прямыми вызовами из Pest без
Bus::dispatchSync).
4 новых Pest-теста:
- master в окне 24ч → дубль, баланс НЕ списывается
- master старше 24ч → НЕ дубль, баланс списан дважды
- дубли изолированы по tenant_id
- ActivityLog для дубля содержит context.duplicate_of
Pest 41/41 зелёные за 4.1 сек. Pint + Larastan чисто.
CLAUDE.md v1.13 → v1.14. Реестр Открытые_вопросы v1.22 → v1.23.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Закрыты 4 TODO в Webhook PoC. Job теперь полностью реализует §5.5
narrative ТЗ за исключением DuplicateDetector (Биз-19) и
SendNewLeadNotificationJob (Биз-20) — отдельные ветви.
5 новых Eloquent-моделей:
- app/app/Models/BalanceTransaction.php — списание lead_charge -1,
type-константы (TYPE_LEAD_CHARGE и т.д.)
- app/app/Models/ActivityLog.php — event=deal.created с
context.source=webhook, event-константы
- app/app/Models/RejectedDealsLog.php — zero_balance ветка вместо
Log::info (payload сохраняется для возможного восстановления)
- app/app/Models/SupplierLeadCost.php — composite PK (id, received_at),
snapshot cost_rub из suppliers, supplier_id resolves через
project_suppliers m2m (первый активный по sort_order)
- app/app/Models/Supplier.php — минимальная для FK target
Job-структура реструктурирована: handle() оркестрирует, делегирует в
logRejection() / chargeNewLead() / resolveSupplierId() / upsertDeal().
Все INSERT'ы в одной DB::transaction — атомарность Ю-2 (deal +
balance_transaction + supplier_lead_cost появляются вместе).
Graceful skip SupplierLeadCost если у проекта нет активного supplier
через project_suppliers + Log::warning. TODO для production: SystemSetting
fallback.
6 новых Pest-тестов в ProcessWebhookJobTest:
- BalanceTransaction lead_charge -1 для новой сделки
- Дубль vid НЕ создаёт BalanceTransaction
- ActivityLog event=deal.created с context.source=webhook
- RejectedDealsLog reason=zero_balance при balance_leads=0
- SupplierLeadCost snapshot cost_rub (helper seedSupplierForProject)
- SupplierLeadCost graceful skip без активного supplier
Pest 37/37 зелёные за 3.9 сек. Pint + Larastan чисто (ide-helper:models
регенерирован для 5 новых моделей).
CLAUDE.md v1.12 → v1.13. Реестр Открытые_вопросы v1.21 → v1.22.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Webhook PoC раскрыл архитектурный пробел в schema v8.6: §5.5-спецификация
делает INSERT в webhook_dedup_keys ДО INSERT в deals (атомарный захват
ключа), но FK был immediate. Решение в две стадии:
1. schema.sql v8.6 → v8.7 — DEFERRABLE INITIALLY DEFERRED на FK
(deal_id, deal_received_at) → deals. ON DELETE CASCADE остаётся
immediate. В bare-транзакции production worker'а решает проблему.
2. Pivot Job на pg_advisory_xact_lock — Pest-тесты с DatabaseTransactions
trait всё равно падали: PG проверяет deferred FK на RELEASE SAVEPOINT,
не на outer COMMIT. Воспроизведено standalone PHP-скриптом, это
PG-семантика subtransactions. Advisory lock работает identically
в любой вложенности транзакций. DEFERRABLE FK сохранён в schema
как defense-in-depth для batch-импортов без savepoint.
Backend стек:
- app/app/Models/Deal.php — composite PK через override
setKeysForSaveQuery (PG требует id+received_at для partition pruning)
- app/app/Models/WebhookDedupKey.php — мини-модель для тестов и debug
- app/database/factories/DealFactory.php — fake данные с received_at
в текущей партиции
- app/app/Jobs/ProcessWebhookJob.php — advisory-lock-based upsert
по §5.5 v8.7. PoC scope: dedup + balance check + project findOrCreate.
TODO для следующих ветвей: BalanceTransaction, SupplierLeadCost,
ActivityLog, RejectedDealsLog, DuplicateDetector (Биз-19).
- app/tests/Feature/DealModelTest.php — 6 тестов composite PK + связи
- app/tests/Feature/ProcessWebhookJobTest.php — 6 тестов: новая сделка,
дубль vid, balance=0, изоляция тенантов, findOrCreate проекта,
ON DELETE CASCADE.
Pest 31/31 за 2.7 сек. Pint + Larastan чисто (phpstan-baseline регенерирован,
scanFiles _ide_helper_models.php добавлен в phpstan.neon).
Документы:
- db/CHANGELOG_schema.md §W (две стадии решения)
- narrative §2.4/§5.5/§6.5/§11 синхронизированы под advisory lock
- Реестр Открытые_вопросы v1.20 → v1.21
- CLAUDE.md v1.11 → v1.12
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>