4803fa0200
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>
42 lines
1.5 KiB
PHP
42 lines
1.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Database\Factories;
|
|
|
|
use App\Models\Deal;
|
|
use App\Models\Project;
|
|
use App\Models\Tenant;
|
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
/**
|
|
* @extends Factory<Deal>
|
|
*/
|
|
class DealFactory extends Factory
|
|
{
|
|
public function definition(): array
|
|
{
|
|
// received_at в текущем месяце — попадает в стартовую партицию deals_2026_05.
|
|
// В тестах при выходе за границы существующих партиций нужно явно
|
|
// указывать ->state(['received_at' => ...]).
|
|
// tenant_id и project_id создаются независимыми factories (deals НЕ имеет
|
|
// FK на tenants/projects из-за партиционирования). Тесты, требующие
|
|
// согласованности, передают tenant_id и project_id явно.
|
|
return [
|
|
'tenant_id' => Tenant::factory(),
|
|
'project_id' => Project::factory(),
|
|
'source_crm_id' => fake()->unique()->numberBetween(100_000_000, 999_999_999),
|
|
'phone' => '7'.fake()->numerify('##########'),
|
|
'phones' => null,
|
|
'status' => 'new',
|
|
'contact_name' => fake()->firstName(),
|
|
'comment' => null,
|
|
'manager_id' => null,
|
|
'escalated_count' => 0,
|
|
'is_test' => false,
|
|
'received_at' => Carbon::now(),
|
|
];
|
|
}
|
|
}
|