fix(supplier): T6 online-mode 3 review-Important — tier-1-only docblock, partial-set re-attempt, per-platform DTO update
This commit is contained in:
@@ -33,7 +33,8 @@ use Illuminate\Support\Facades\Mail;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Daily 20:30 МСК cron-job: синхронизирует supplier_projects с поставщиком crm.bp-gr.ru.
|
||||
* Daily 18:00 МСК cron-job: синхронизирует supplier_projects с поставщиком crm.bp-gr.ru
|
||||
* (расписание перенесено 20:30 → 18:00, см. routes/console.php).
|
||||
*
|
||||
* Алгоритм (Plan 3 Task 5 — per-subject grouping):
|
||||
* 1. Загрузить активные Лидерра-projects (is_active=true, archived_at IS NULL).
|
||||
@@ -269,24 +270,76 @@ class SyncSupplierProjectsJob implements ShouldQueue
|
||||
$existingSps->push($sp);
|
||||
}
|
||||
} else {
|
||||
// Update path: updateProject each external_id (R6: one shared limit)
|
||||
$dto = new SupplierProjectDto(
|
||||
platform: $existingSps->first()->platform,
|
||||
signalType: $signalType,
|
||||
uniqueKey: $identifier,
|
||||
limit: $order,
|
||||
workdays: $workdays,
|
||||
regions: $regions,
|
||||
regionsReverse: false,
|
||||
status: 'active',
|
||||
tag: $tag,
|
||||
);
|
||||
// Fix #3 (review-followup): partial-set recovery — если предыдущий run создал
|
||||
// не все platforms (e.g. B1+B2 OK, B3 escalated), re-attempt missing via multi-flag
|
||||
// save с platforms=$missingPlatforms. Throws пропагируют в outer handle() catch
|
||||
// (SupplierAuth/Transient/Client) — full failover-counter semantics сохраняется.
|
||||
$existingPlatforms = $existingSps->pluck('platform')->all();
|
||||
$missingPlatforms = array_values(array_diff($platforms, $existingPlatforms));
|
||||
|
||||
if ($missingPlatforms !== []) {
|
||||
$missingDto = new SupplierProjectDto(
|
||||
platform: $missingPlatforms[0],
|
||||
signalType: $signalType,
|
||||
uniqueKey: $identifier,
|
||||
limit: $order,
|
||||
workdays: $workdays,
|
||||
regions: $regions,
|
||||
regionsReverse: false,
|
||||
status: 'active',
|
||||
tag: $tag,
|
||||
platforms: $missingPlatforms,
|
||||
);
|
||||
|
||||
$missingIdMap = $this->client->saveProjectMultiFlag($missingDto);
|
||||
|
||||
foreach ($missingPlatforms as $platform) {
|
||||
$externalId = $missingIdMap[$platform] ?? null;
|
||||
if ($externalId === null) {
|
||||
continue;
|
||||
}
|
||||
$sp = SupplierProject::on(self::DB_CONNECTION)->forceCreate([
|
||||
'platform' => $platform,
|
||||
'signal_type' => $signalType,
|
||||
'unique_key' => $identifier,
|
||||
'subject_code' => $subjectCode,
|
||||
'supplier_external_id' => (string) $externalId,
|
||||
'current_limit' => $order,
|
||||
'current_workdays' => $workdays,
|
||||
'current_regions' => $regions,
|
||||
'sync_status' => 'ok',
|
||||
'last_synced_at' => now(),
|
||||
]);
|
||||
SupplierSyncLog::on(self::DB_CONNECTION)->create([
|
||||
'supplier_project_id' => $sp->id,
|
||||
'action' => 'create',
|
||||
'http_status' => 200,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
$existingSps->push($sp);
|
||||
}
|
||||
}
|
||||
|
||||
// Fix #2 (review-followup): per-platform DTO в update-loop, чтобы portal получал
|
||||
// правильные srcrt/srcbl/srcmt для конкретной редактируемой строки (не first()
|
||||
// из mixed-platform existing set). R6 one shared limit/regions сохраняется.
|
||||
foreach ($existingSps as $sp) {
|
||||
if ($sp->supplier_external_id === null) {
|
||||
continue;
|
||||
}
|
||||
$this->channel->updateProject((int) $sp->supplier_external_id, $dto);
|
||||
$perPlatformDto = new SupplierProjectDto(
|
||||
platform: $sp->platform,
|
||||
signalType: $signalType,
|
||||
uniqueKey: $identifier,
|
||||
limit: $order,
|
||||
workdays: $workdays,
|
||||
regions: $regions,
|
||||
regionsReverse: false,
|
||||
status: 'active',
|
||||
tag: $tag,
|
||||
platforms: [$sp->platform],
|
||||
);
|
||||
$this->channel->updateProject((int) $sp->supplier_external_id, $perPlatformDto);
|
||||
$sp->forceFill([
|
||||
'current_limit' => $order,
|
||||
'current_workdays' => $workdays,
|
||||
|
||||
@@ -34,10 +34,17 @@ use Illuminate\Support\Facades\Log;
|
||||
* batch → «каркас»: создаёт supplier_projects с limit=0, без регионов
|
||||
* (старый путь); ночной SyncSupplierProjectsJob дольёт полные параметры.
|
||||
*
|
||||
* Канал миграции — SupplierProjectChannel (резолвится в FailoverProjectChannel:
|
||||
* ярус 1 AJAX → ярус 2 browser-form → ярус 3 manual queue). При эскалации на
|
||||
* ярус 3 / переносе по окну портала — platform пропускается (FK остаётся NULL,
|
||||
* ночной SyncSupplierProjectsJob подберёт после ручного вмешательства).
|
||||
* Канал миграции:
|
||||
* batch mode — SupplierProjectChannel (FailoverProjectChannel: ярус 1 AJAX
|
||||
* → ярус 2 browser-form → ярус 3 manual queue) для createProject.
|
||||
* online mode — multi-flag save идёт напрямую через SupplierPortalClient
|
||||
* (tier-1 AJAX only — multi-flag нет в tier-2 form по архитектуре
|
||||
* портала). При любом transient/auth fail → log warning + skip
|
||||
* subject; Laravel retry (tries=3 backoff [15s,60s,300s]) → ночной
|
||||
* SyncSupplierProjectsJob подберёт с полным failover каналом.
|
||||
* updateProject в online остаётся через $channel (полная схема failover).
|
||||
* При эскалации на ярус 3 / переносе по окну портала — platform/subject пропускается
|
||||
* (FK/pivot остаётся пустым; ночной SyncSupplierProjectsJob восстанавливает).
|
||||
*
|
||||
* Retry: 3 попытки с backoff [15s, 60s, 300s].
|
||||
*
|
||||
@@ -132,6 +139,13 @@ class SyncSupplierProjectJob implements ShouldQueue
|
||||
} catch (WindowDeferredException) {
|
||||
Log::info("SyncSupplierProjectJob: project {$project->id} subject={$subject} deferred by portal window");
|
||||
|
||||
continue;
|
||||
} catch (\Throwable $e) {
|
||||
// Online multi-flag save bypasses FailoverProjectChannel (tier-1 only by design,
|
||||
// см. class docblock). При transient/auth/client/network fail — log+skip; следующий
|
||||
// tries-retry (15s, 60s, 300s) или ночной SyncSupplierProjectsJob подберёт.
|
||||
Log::warning("SyncSupplierProjectJob: online multi-flag save failed for project {$project->id} subject={$subject} (".get_class($e).'): '.$e->getMessage());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -157,24 +171,80 @@ class SyncSupplierProjectJob implements ShouldQueue
|
||||
$existingSps->push($sp);
|
||||
}
|
||||
} else {
|
||||
// Update path: updateProject each (R6: one shared limit)
|
||||
$dto = new SupplierProjectDto(
|
||||
platform: $existingSps->first()->platform,
|
||||
signalType: (string) $project->signal_type,
|
||||
uniqueKey: $identifier,
|
||||
limit: (int) $project->daily_limit_target,
|
||||
workdays: [1, 2, 3, 4, 5, 6, 7],
|
||||
regions: $regions,
|
||||
regionsReverse: false,
|
||||
status: 'active',
|
||||
tag: $tag,
|
||||
);
|
||||
// Fix #3 (review-followup): partial-set recovery — если предыдущий run создал
|
||||
// не все platforms (e.g. B1+B2 OK, B3 escalated), re-attempt missing via
|
||||
// multi-flag save с platforms=$missingPlatforms (srcrt/srcbl/srcmt только missing).
|
||||
$existingPlatforms = $existingSps->pluck('platform')->all();
|
||||
$missingPlatforms = array_values(array_diff($platforms, $existingPlatforms));
|
||||
|
||||
if ($missingPlatforms !== []) {
|
||||
$missingDto = new SupplierProjectDto(
|
||||
platform: $missingPlatforms[0],
|
||||
signalType: (string) $project->signal_type,
|
||||
uniqueKey: $identifier,
|
||||
limit: (int) $project->daily_limit_target,
|
||||
workdays: [1, 2, 3, 4, 5, 6, 7],
|
||||
regions: $regions,
|
||||
regionsReverse: false,
|
||||
status: 'active',
|
||||
tag: $tag,
|
||||
platforms: $missingPlatforms,
|
||||
);
|
||||
|
||||
try {
|
||||
$missingIdMap = $client->saveProjectMultiFlag($missingDto);
|
||||
} catch (TierEscalatedException $e) {
|
||||
Log::info("SyncSupplierProjectJob: project {$project->id} subject={$subject} missing-platform re-attempt escalated #{$e->queueRowId}");
|
||||
$missingIdMap = [];
|
||||
} catch (WindowDeferredException) {
|
||||
Log::info("SyncSupplierProjectJob: project {$project->id} subject={$subject} missing-platform deferred by portal window");
|
||||
$missingIdMap = [];
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning("SyncSupplierProjectJob: missing-platform multi-flag failed for project {$project->id} subject={$subject}: ".$e->getMessage());
|
||||
$missingIdMap = [];
|
||||
}
|
||||
|
||||
foreach ($missingPlatforms as $platform) {
|
||||
$externalId = $missingIdMap[$platform] ?? null;
|
||||
if ($externalId === null) {
|
||||
continue;
|
||||
}
|
||||
$sp = SupplierProject::create([
|
||||
'platform' => $platform,
|
||||
'signal_type' => (string) $project->signal_type,
|
||||
'unique_key' => $identifier,
|
||||
'subject_code' => $subject,
|
||||
'supplier_external_id' => (string) $externalId,
|
||||
'current_limit' => (int) $project->daily_limit_target,
|
||||
'current_workdays' => [1, 2, 3, 4, 5, 6, 7],
|
||||
'current_regions' => $regions,
|
||||
'sync_status' => 'ok',
|
||||
'last_synced_at' => now(),
|
||||
]);
|
||||
$existingSps->push($sp);
|
||||
}
|
||||
}
|
||||
|
||||
// Fix #2 (review-followup): per-platform DTO в update-loop, чтобы portal
|
||||
// получал корректные srcrt/srcbl/srcmt флаги для конкретной редактируемой строки
|
||||
// (не первой из mixed-platform existing set). R6 one shared limit/regions сохраняется.
|
||||
foreach ($existingSps as $sp) {
|
||||
if ($sp->supplier_external_id === null) {
|
||||
continue;
|
||||
}
|
||||
$channel->updateProject((int) $sp->supplier_external_id, $dto);
|
||||
$perPlatformDto = new SupplierProjectDto(
|
||||
platform: $sp->platform,
|
||||
signalType: (string) $project->signal_type,
|
||||
uniqueKey: $identifier,
|
||||
limit: (int) $project->daily_limit_target,
|
||||
workdays: [1, 2, 3, 4, 5, 6, 7],
|
||||
regions: $regions,
|
||||
regionsReverse: false,
|
||||
status: 'active',
|
||||
tag: $tag,
|
||||
platforms: [$sp->platform],
|
||||
);
|
||||
$channel->updateProject((int) $sp->supplier_external_id, $perPlatformDto);
|
||||
$sp->forceFill([
|
||||
'current_limit' => (int) $project->daily_limit_target,
|
||||
'current_regions' => $regions,
|
||||
|
||||
Reference in New Issue
Block a user