Commit Graph

4 Commits

Author SHA1 Message Date
Дмитрий ba97f952cc phase1(webhook): failed() callback + FailedWebhookJob модель — упавшие jobs после 3 ретраев
После исчерпания всех 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>
2026-05-08 15:52:47 +03:00
Дмитрий 2d9e84ef1d phase1(antifraud): DuplicateDetector сервис (Биз-19) — антифрод-дедуп по phone в окне 24ч
Закрыт Биз-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>
2026-05-08 15:41:37 +03:00
Дмитрий 1ba25e6b4e phase1(webhook): закрыты TODO в ProcessWebhookJob — BalanceTransaction/ActivityLog/RejectedDealsLog/SupplierLeadCost
Закрыты 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>
2026-05-08 15:35:28 +03:00
Дмитрий 4803fa0200 phase1(webhook): Deal/WebhookDedupKey + ProcessWebhookJob (advisory lock) — CTO-17 addendum
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>
2026-05-08 15:24:55 +03:00