fix(supplier): правки по код-ревью — матч наших строк по external_id, shouldBeOff без активных, sync_run_id
- normalizeLive матчит наши строки по supplier_external_id (tag=регион, не _lidpotok) — иначе live всегда пуст - shouldBeOff исключает ключи, активные по формуле (нет ложного should_be_off) - recordRunSummary → insertGetId, проверка получает sync_run_id - обновлён phpstan-baseline.neon: count mock() 2→3 (новый тест на баг 2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -193,7 +193,7 @@ class SyncSupplierProjectsJob implements ShouldQueue
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$this->recordRunSummary(
|
||||
$runId = $this->recordRunSummary(
|
||||
startedAt: $startedAt,
|
||||
groupsTotal: count($groups),
|
||||
syncedOk: $syncedOk,
|
||||
@@ -206,7 +206,7 @@ class SyncSupplierProjectsJob implements ShouldQueue
|
||||
// Итоговая проверка: после «готово» перечитать живой кабинет и сверить.
|
||||
// Идёт всегда — и при штатном финише, и при обрыве. При auth-сбое (кабинет
|
||||
// недоступен) сам джоб зафиксирует unable_to_verify.
|
||||
VerifySupplierOrderJob::dispatch();
|
||||
VerifySupplierOrderJob::dispatch(1, $runId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ class SyncSupplierProjectsJob implements ShouldQueue
|
||||
int $deferred,
|
||||
int $failed,
|
||||
bool $aborted,
|
||||
): void {
|
||||
): int {
|
||||
if ($aborted) {
|
||||
$status = 'aborted';
|
||||
} elseif ($failed > 0 && $syncedOk === 0) {
|
||||
@@ -233,7 +233,7 @@ class SyncSupplierProjectsJob implements ShouldQueue
|
||||
$status = 'ok';
|
||||
}
|
||||
|
||||
DB::connection(self::DB_CONNECTION)->table('supplier_sync_runs')->insert([
|
||||
$id = DB::connection(self::DB_CONNECTION)->table('supplier_sync_runs')->insertGetId([
|
||||
'started_at' => $startedAt,
|
||||
'finished_at' => now(),
|
||||
'groups_total' => $groupsTotal,
|
||||
@@ -244,6 +244,8 @@ class SyncSupplierProjectsJob implements ShouldQueue
|
||||
'status' => $status,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,7 +49,23 @@ final class VerifySupplierOrderJob implements ShouldQueue
|
||||
$plan = SupplierOrderPlan::build($eligible, $targetDate);
|
||||
$intended = $plan['intended'];
|
||||
|
||||
// 2. shouldBeOff — наши выключенные supplier_projects.
|
||||
// Ключи активных по формуле — для (а) исключения из shouldBeOff, (б) ничего более.
|
||||
$intendedKeys = [];
|
||||
foreach ($intended as $r) {
|
||||
$intendedKeys[$r['signal_type'].'|'.$r['identifier'].'|'.$r['platform']] = true;
|
||||
}
|
||||
|
||||
// Наши external_id в кабинете — признак «наша строка» (tag НЕ годится: робот
|
||||
// шлёт tag=регион/«РФ», не маркер). Сверка живого идёт только по нашим строкам.
|
||||
$ourExternalIds = SupplierProject::on(self::DB_CONNECTION)
|
||||
->whereNotNull('supplier_external_id')
|
||||
->pluck('supplier_external_id')
|
||||
->map(fn ($id) => (string) $id)
|
||||
->all();
|
||||
|
||||
// 2. shouldBeOff — наши выключенные supplier_projects, КРОМЕ ключей,
|
||||
// которые сейчас активны по формуле (иначе ложный should_be_off на
|
||||
// реактивированной группе с зависшим inactive_since).
|
||||
$shouldBeOff = SupplierProject::on(self::DB_CONNECTION)
|
||||
->whereNotNull('inactive_since')
|
||||
->get(['signal_type', 'unique_key', 'platform'])
|
||||
@@ -57,11 +73,14 @@ final class VerifySupplierOrderJob implements ShouldQueue
|
||||
'signal_type' => (string) $sp->signal_type,
|
||||
'identifier' => (string) $sp->unique_key,
|
||||
'platform' => (string) $sp->platform,
|
||||
])->all();
|
||||
])
|
||||
->reject(fn (array $r): bool => isset($intendedKeys[$r['signal_type'].'|'.$r['identifier'].'|'.$r['platform']]))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
// 3. Живой кабинет (только наши строки: tag _lidpotok).
|
||||
// 3. Живой кабинет (только наши строки — по supplier_external_id).
|
||||
try {
|
||||
$live = $this->normalizeLive($client->listProjects());
|
||||
$live = $this->normalizeLive($client->listProjects(), $ourExternalIds);
|
||||
} catch (Throwable $e) {
|
||||
$this->record('unable_to_verify', count($intended), 0, 0, ['error' => $e->getMessage()]);
|
||||
Mail::to((string) config('services.supplier.alert_email'))
|
||||
@@ -93,16 +112,19 @@ final class VerifySupplierOrderJob implements ShouldQueue
|
||||
}
|
||||
|
||||
/**
|
||||
* Портальные строки → формат SupplierOrderVerifier (только наши _lidpotok, с распознанной площадкой).
|
||||
* Портальные строки → формат SupplierOrderVerifier. «Наши» = те, чей портальный id
|
||||
* совпадает с нашим supplier_external_id (tag НЕ используется — робот шлёт регион).
|
||||
*
|
||||
* @param array<int, array<string,mixed>> $rows
|
||||
* @param list<string> $ourExternalIds
|
||||
* @return list<array{signal_type:string,identifier:string,platform:string,limit:int,on:bool}>
|
||||
*/
|
||||
private function normalizeLive(array $rows): array
|
||||
private function normalizeLive(array $rows, array $ourExternalIds): array
|
||||
{
|
||||
$ours = array_flip($ourExternalIds);
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
if (($row['tag'] ?? null) !== '_lidpotok') {
|
||||
if (! isset($ours[(string) ($row['id'] ?? '')])) {
|
||||
continue;
|
||||
}
|
||||
$name = (string) ($row['name'] ?? '');
|
||||
|
||||
@@ -3063,7 +3063,7 @@ parameters:
|
||||
-
|
||||
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:mock\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 2
|
||||
count: 3
|
||||
path: tests/Feature/Supplier/VerifySupplierOrderJobTest.php
|
||||
|
||||
-
|
||||
|
||||
@@ -50,14 +50,16 @@ function seedActiveCallProject(string $identifier, int $limit): Project
|
||||
/**
|
||||
* Наша supplier_project, которую мы уже выключили (inactive_since IS NOT NULL) —
|
||||
* SupplierOrderVerifier::diff должен поймать kind=should_be_off, если поставщик
|
||||
* до сих пор показывает её включённой (status=1).
|
||||
* до сих пор показывает её включённой (status=1). supplier_external_id — признак
|
||||
* «наша строка» в normalizeLive() (баг 1: tag НЕ годится — робот шлёт регион/«РФ»).
|
||||
*/
|
||||
function seedPausedSupplierProject(string $identifier, string $platform, string $signalType): SupplierProject
|
||||
function seedPausedSupplierProject(string $identifier, string $platform, string $signalType, string $externalId): SupplierProject
|
||||
{
|
||||
return SupplierProject::factory()->create([
|
||||
'platform' => $platform,
|
||||
'signal_type' => $signalType,
|
||||
'unique_key' => $identifier,
|
||||
'supplier_external_id' => $externalId,
|
||||
'inactive_since' => now(),
|
||||
]);
|
||||
}
|
||||
@@ -66,11 +68,24 @@ it('нет расхождений → письма нет, статус ok', fun
|
||||
Mail::fake();
|
||||
seedActiveCallProject('79135397707', 3);
|
||||
|
||||
SupplierProject::factory()->create([
|
||||
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
||||
'supplier_external_id' => '1001', 'current_limit' => 1, 'inactive_since' => null,
|
||||
]);
|
||||
SupplierProject::factory()->create([
|
||||
'platform' => 'B2', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
||||
'supplier_external_id' => '1002', 'current_limit' => 1, 'inactive_since' => null,
|
||||
]);
|
||||
SupplierProject::factory()->create([
|
||||
'platform' => 'B3', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
||||
'supplier_external_id' => '1003', 'current_limit' => 1, 'inactive_since' => null,
|
||||
]);
|
||||
|
||||
$this->mock(SupplierPortalClient::class, function ($m): void {
|
||||
$m->shouldReceive('listProjects')->andReturn([
|
||||
['name' => 'B1_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => '_lidpotok'],
|
||||
['name' => 'B2_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => '_lidpotok'],
|
||||
['name' => 'B3_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => '_lidpotok'],
|
||||
['id' => '1001', 'name' => 'B1_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||||
['id' => '1002', 'name' => 'B2_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||||
['id' => '1003', 'name' => 'B3_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -84,11 +99,11 @@ it('нет расхождений → письма нет, статус ok', fun
|
||||
|
||||
it('пауза не дошла (should_be_off) → письмо + статус mismatch на попытке 2', function (): void {
|
||||
Mail::fake();
|
||||
seedPausedSupplierProject('automoney.ru', 'B3', 'site');
|
||||
seedPausedSupplierProject('automoney.ru', 'B3', 'site', '2001');
|
||||
|
||||
$this->mock(SupplierPortalClient::class, function ($m): void {
|
||||
$m->shouldReceive('listProjects')->andReturn([
|
||||
['name' => 'B3_automoney.ru', 'content' => 'automoney.ru', 'type' => 'hosts', 'lim' => 1, 'status' => '1', 'tag' => '_lidpotok'],
|
||||
['id' => '2001', 'name' => 'B3_automoney.ru', 'content' => 'automoney.ru', 'type' => 'hosts', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -99,3 +114,38 @@ it('пауза не дошла (should_be_off) → письмо + статус m
|
||||
expect($row)->not->toBeNull();
|
||||
expect($row->status)->toBe('mismatch')->and($row->mismatch_count)->toBe(1);
|
||||
});
|
||||
|
||||
it('ключ активен по формуле, но inactive_since завис → НЕ ложная тревога (баг 2)', function (): void {
|
||||
Mail::fake();
|
||||
seedActiveCallProject('79999999999', 3);
|
||||
|
||||
// B1 «завис» с inactive_since, хотя группа сейчас активна по формуле —
|
||||
// должен быть исключён из shouldBeOff, а не дать ложный should_be_off.
|
||||
SupplierProject::factory()->create([
|
||||
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79999999999',
|
||||
'supplier_external_id' => '3001', 'current_limit' => 1, 'inactive_since' => now(),
|
||||
]);
|
||||
SupplierProject::factory()->create([
|
||||
'platform' => 'B2', 'signal_type' => 'call', 'unique_key' => '79999999999',
|
||||
'supplier_external_id' => '3002', 'current_limit' => 1, 'inactive_since' => null,
|
||||
]);
|
||||
SupplierProject::factory()->create([
|
||||
'platform' => 'B3', 'signal_type' => 'call', 'unique_key' => '79999999999',
|
||||
'supplier_external_id' => '3003', 'current_limit' => 1, 'inactive_since' => null,
|
||||
]);
|
||||
|
||||
$this->mock(SupplierPortalClient::class, function ($m): void {
|
||||
$m->shouldReceive('listProjects')->andReturn([
|
||||
['id' => '3001', 'name' => 'B1_79999999999', 'content' => '79999999999', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||||
['id' => '3002', 'name' => 'B2_79999999999', 'content' => '79999999999', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||||
['id' => '3003', 'name' => 'B3_79999999999', 'content' => '79999999999', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||||
]);
|
||||
});
|
||||
|
||||
(new VerifySupplierOrderJob(attempt: 2))->handle();
|
||||
|
||||
Mail::assertNothingQueued();
|
||||
$row = DB::connection('pgsql_supplier')->table('supplier_order_checks')->latest('id')->first();
|
||||
expect($row)->not->toBeNull();
|
||||
expect($row->status)->toBe('ok')->and($row->mismatch_count)->toBe(0);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-07-09T17:27:50.153Z
|
||||
Last updated: 2026-07-09T17:36:28.707Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
@@ -112,8 +112,8 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
| PID | Имя | CPU-время | Возраст |
|
||||
|---|---|---|---|
|
||||
| 11216 | MsMpEng | 2.46ч | NaNч |
|
||||
| 10400 | Code | 1.83ч | NaNч |
|
||||
| 11216 | MsMpEng | 2.48ч | NaNч |
|
||||
| 10400 | Code | 1.85ч | 0.0ч |
|
||||
|
||||
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user