test(supplier): regression-guard неизменности received_at при merge CSV-recovered сделки
This commit is contained in:
@@ -2547,13 +2547,13 @@ parameters:
|
||||
-
|
||||
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$sp\.$#'
|
||||
identifier: property.notFound
|
||||
count: 7
|
||||
count: 9
|
||||
path: tests/Feature/Supplier/CsvWebhookRaceTest.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
|
||||
identifier: property.notFound
|
||||
count: 8
|
||||
count: 10
|
||||
path: tests/Feature/Supplier/CsvWebhookRaceTest.php
|
||||
|
||||
-
|
||||
|
||||
@@ -178,6 +178,79 @@ it('webhook after CSV-recovered merges into existing deal (no duplicate, no doub
|
||||
expect($deliveredLeadIds)->toContain($webhookLead->id);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1b — merge НЕ меняет received_at CSV-recovered сделки.
|
||||
// Regression-guard инцидента 26.05.2026 04:12–05:03 UTC (9 failed_jobs):
|
||||
// received_at — partition key, lead_charges имеет FK (deal_id, deal_received_at)
|
||||
// ON DELETE CASCADE / ON UPDATE NO ACTION. Любое изменение received_at при merge
|
||||
// ломало FK на COMMIT → каскадный риск. Код (RouteSupplierLeadJob:371-393) сохраняет
|
||||
// received_at как есть; этот тест пиннит инвариант (раньше не assert'ился).
|
||||
// ---------------------------------------------------------------------------
|
||||
it('merge preserves CSV-recovered received_at unchanged (FK-partition safety, no extra charge)', function (): void {
|
||||
$phone = '79991000004';
|
||||
$csvTime = now()->subHours(3);
|
||||
|
||||
// ── Step 1: CSV-recovered сделка с известным received_at (3 часа назад) ──
|
||||
$csvLead = SupplierLead::factory()->create([
|
||||
'platform' => 'B1',
|
||||
'phone' => $phone,
|
||||
'vid' => null,
|
||||
'supplier_project_id' => $this->sp->id,
|
||||
'raw_payload' => [
|
||||
'project' => 'B1_race-csv.ru',
|
||||
'phone' => $phone,
|
||||
'time' => $csvTime->getTimestamp(),
|
||||
],
|
||||
'received_at' => $csvTime,
|
||||
'recovered_from_csv_at' => $csvTime,
|
||||
'source' => 'csv_recovery',
|
||||
'processed_at' => null,
|
||||
]);
|
||||
runRaceJob($csvLead->id);
|
||||
|
||||
DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");
|
||||
$csvDeal = Deal::where('phone', $phone)->first();
|
||||
expect($csvDeal)->not->toBeNull('CSV recovery должен был создать Deal');
|
||||
$receivedAtBefore = $csvDeal->received_at;
|
||||
expect($receivedAtBefore->getTimestamp())->toBe($csvTime->getTimestamp(), 'received_at сделки = время CSV-лида');
|
||||
|
||||
// ── Step 2: догоняющий webhook с реальным vid и ДРУГИМ time (15 мин назад) ──
|
||||
$webhookLead = SupplierLead::factory()->create([
|
||||
'platform' => 'B1',
|
||||
'phone' => $phone,
|
||||
'vid' => 1672819990,
|
||||
'supplier_project_id' => $this->sp->id,
|
||||
'raw_payload' => [
|
||||
'vid' => 1672819990,
|
||||
'project' => 'B1_race-csv.ru',
|
||||
'phone' => $phone,
|
||||
'time' => now()->subMinutes(15)->getTimestamp(),
|
||||
],
|
||||
'received_at' => now()->subMinutes(15),
|
||||
'source' => 'webhook',
|
||||
'processed_at' => null,
|
||||
]);
|
||||
runRaceJob($webhookLead->id);
|
||||
|
||||
DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");
|
||||
|
||||
// ── Assertions ──
|
||||
$deals = Deal::where('phone', $phone)->get();
|
||||
expect($deals)->toHaveCount(1, 'merge: одна сделка, не две');
|
||||
|
||||
$merged = $deals->first();
|
||||
// Ключевой инвариант: received_at НЕ изменился webhook-временем (15 мин назад),
|
||||
// остался временем CSV-лида (3 часа назад) — FK-partition safety.
|
||||
expect($merged->received_at->getTimestamp())->toBe(
|
||||
$csvTime->getTimestamp(),
|
||||
'merge НЕ должен менять received_at (partition key + lead_charges FK)',
|
||||
);
|
||||
expect((int) $merged->source_crm_id)->toBe(1672819990, 'source_crm_id обновлён webhook vid');
|
||||
|
||||
// Списание ровно одно — merge не доначисляет.
|
||||
expect(LeadCharge::where('deal_id', $merged->id)->count())->toBe(1, 'merge: второго списания нет');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2 — Spec B regression: два webhook с РАЗНЫМИ vid → два deal (by-design).
|
||||
// Наш Phase 2 fix НЕ должен блокировать это.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- **apiv1-rate — СДЕЛАНО (TDD).** `throttle:api-v1` 120/мин/источник (Bearer-ключ→IP) ПЕРЕД `apikey` на `/api/v1/deals`. Лимитер в `AppServiceProvider`, тест `PublicDealsApiTest` 8/8, Pint/Larastan=0.
|
||||
- **M-1 — СДЕЛАНО локально (TDD), вариант А (fail-closed app-гейт), allowlist A2 config/env.** `EnsureSaasAdmin` требует непустой `REMOTE_USER` ∈ `config('admin.basic_auth_allowlist')` при `admin.basic_auth_gate` (вкл вне local/testing). Новый `config/admin.php`, тест `EnsureSaasAdminGateTest` 4/4, Pint/Larastan=0. Спека+план в `docs/superpowers/`. **🔴 Деплой — твой шаг: дыра на боевом ОТКРЫТА до выката** (`APP_ENV=production` включит гейт сам, дефолт allowlist=`admin`).
|
||||
- **M-2 — прод-.env сверён.** В прод-`.env` НЕТ CAPTCHA-переменных → капча выключена (любой непустой токен проходит). Решение владельца: **ставить реальный Yandex SmartCaptcha** (отдельная фича, нужны ключи) — в работу следующей.
|
||||
- **Раздел B #1 (CsvReconcile/merge) — углублён.** Сверка показала: merge-без-2-го-списания (`CsvWebhookRaceTest`), drift-алерты webhook-loss + business-drift R-05 (`CsvReconcileJobTest`), регион-улучшение при merge (`RouteSupplierLeadJobRegionResolutionTest`) — **уже покрыты** (док B.1 устарел). Единственная неприкрытая денежно-критичная ассерция — **неизменность `received_at` при merge** (regression-guard FK-partition инцидента 26.05) — добавлена в `CsvWebhookRaceTest` (4/4), фальсификацией подтверждено, что тест ловит регресс.
|
||||
|
||||
> ⚠️ **Побочно (вне scope M-1):** `AdminSuppliersControllerTest` падает (4 поставщика вместо 3) и на чистом HEAD — предсуществующее состояние тест-БД `liderra_testing`, не регрессия. Требует отдельной чистки сидов.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user