80275c6417
Закрывает два бага sync поставщика, обнаруженные при live-проверке создания проекта «мой номер» (call, 79135191264, лимит 15, дни Пн-Пт): 1. SyncSupplierProjectJob хардкодил workdays=[1..7] в 7 местах и в DTO для portal, и в supplier_projects.current_workdays. Заменено на реальную маску через приватный workdaysFromMask() (зеркало bitmaskToList ночного батча). 2. forceFill в update-path online mode не включал current_workdays — после первого create со старыми [1..7] последующий ресинк не подтягивал реальные дни в локальную БД (на portal летели корректные, в нашей таблице оставались stale). 3. ProjectService::update() ресинкал только при смене sms_*/signal_identifier/ regions. Добавлены daily_limit_target и delivery_days_mask — поставщик видит новый лимит и дни сразу, не дожидаясь ночного батча 18:00 МСК. Тесты: - SyncSupplierProjectJobTest: +2 specs (real-workdays create-path, update-path current_workdays refresh). - ProjectsUpdateTest: «without resync» переписан в name-only, +2 specs (daily_limit_target и delivery_days_mask change → resync). - Pest 146/146 (Supplier + Plan5/Projects scope), Pint passed, Larastan 0. Live-ресинк проекта id=5 «мой номер» в dev DB выполнен — current_workdays теперь [1,2,3,4,5], HTTP ушёл к crm.bp-gr.ru с теми же днями. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
253 lines
11 KiB
PHP
253 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\SyncSupplierProjectJob;
|
|
use App\Models\Project;
|
|
use App\Models\SupplierProject;
|
|
use App\Models\Tenant;
|
|
use App\Services\Supplier\Channel\SupplierProjectChannel;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Tests\Concerns\SharesSupplierPdo;
|
|
|
|
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
|
|
|
beforeEach(function (): void {
|
|
Carbon::setTestNow(Carbon::parse('2026-05-12 10:00:00', 'Europe/Moscow'));
|
|
|
|
Cache::store('redis')->put('supplier:session', [
|
|
'phpsessid' => 'sess123',
|
|
'csrf' => 'csrf123',
|
|
'refreshed_at' => now()->toIso8601String(),
|
|
], now()->addHours(6));
|
|
|
|
config(['services.supplier.portal_url' => 'https://crm.bp-gr.ru']);
|
|
config(['services.supplier.alert_email' => 'ops@liderra.test']);
|
|
|
|
// Default to batch mode so existing Plan5 tests are unaffected
|
|
DB::table('system_settings')->where('key', 'supplier_export_mode')->update(['value' => 'batch']);
|
|
});
|
|
|
|
afterEach(function (): void {
|
|
Cache::store('redis')->forget('supplier:session');
|
|
Carbon::setTestNow();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Online mode: single-group supplier_projects + pivot
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('online mode creates single-group supplier_projects with full regions + pivot', function (): void {
|
|
DB::table('system_settings')->where('key', 'supplier_export_mode')->update(['value' => 'online']);
|
|
|
|
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
|
|
$project = Project::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'signal_type' => 'site',
|
|
'signal_identifier' => 'okna.ru',
|
|
'is_active' => true,
|
|
'daily_limit_target' => 12,
|
|
'regions' => [82],
|
|
'delivery_days_mask' => 127,
|
|
]);
|
|
|
|
// saveProjectMultiFlag → rt-project-save + listProjects → 3 ids
|
|
Http::fake([
|
|
'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
|
|
['status' => 'OK', 'message' => '', 'result' => null, 'id' => '1001'],
|
|
200,
|
|
),
|
|
'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
|
|
['projects' => [
|
|
['id' => '1001', 'src' => 'rt', 'name' => 'okna.ru', 'tag' => 'Москва', 'type' => 'hosts', 'content' => 'okna.ru'],
|
|
['id' => '1002', 'src' => 'bl', 'name' => 'okna.ru', 'tag' => 'Москва', 'type' => 'hosts', 'content' => 'okna.ru'],
|
|
['id' => '1003', 'src' => 'mt', 'name' => 'okna.ru', 'tag' => 'Москва', 'type' => 'hosts', 'content' => 'okna.ru'],
|
|
]],
|
|
200,
|
|
),
|
|
]);
|
|
|
|
(new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class));
|
|
|
|
// 3 supplier_projects: subject_code=null (single group), platforms B1/B2/B3
|
|
expect(SupplierProject::where('unique_key', 'okna.ru')->whereNull('subject_code')->count())->toBe(3);
|
|
|
|
// pivot: 3 links for this project
|
|
expect(DB::table('project_supplier_links')->where('project_id', $project->id)->count())->toBe(3);
|
|
});
|
|
|
|
it('online mode passes real workdays from delivery_days_mask (not hardcoded [1..7])', function (): void {
|
|
// Regression: до фикса хардкодилось [1,2,3,4,5,6,7] независимо от delivery_days_mask.
|
|
// delivery_days_mask=31 = 0b0011111 = Пн-Пт (ISO дни 1-5). Workdays поставщика должны быть [1,2,3,4,5].
|
|
DB::table('system_settings')->where('key', 'supplier_export_mode')->update(['value' => 'online']);
|
|
|
|
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
|
|
$project = Project::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'signal_type' => 'call',
|
|
'signal_identifier' => '79135191264',
|
|
'is_active' => true,
|
|
'daily_limit_target' => 15,
|
|
'regions' => [],
|
|
'delivery_days_mask' => 31, // Пн-Пт
|
|
]);
|
|
|
|
$capturedWorkdays = null;
|
|
Http::fake([
|
|
'crm.bp-gr.ru/admin/visit/rt-project-save' => function ($request) use (&$capturedWorkdays) {
|
|
$body = $request->data();
|
|
if (isset($body['workdays'])) {
|
|
$capturedWorkdays = $body['workdays'];
|
|
}
|
|
|
|
return Http::response(['status' => 'OK', 'message' => '', 'result' => null, 'id' => '2001'], 200);
|
|
},
|
|
'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
|
|
['projects' => [
|
|
['id' => '2001', 'src' => 'rt', 'name' => '79135191264', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79135191264'],
|
|
['id' => '2002', 'src' => 'bl', 'name' => '79135191264', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79135191264'],
|
|
['id' => '2003', 'src' => 'mt', 'name' => '79135191264', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79135191264'],
|
|
]],
|
|
200,
|
|
),
|
|
]);
|
|
|
|
(new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class));
|
|
|
|
// 1) supplier_projects записаны с реальными буднями, не all-7.
|
|
$sps = SupplierProject::where('unique_key', '79135191264')->get();
|
|
expect($sps)->toHaveCount(3);
|
|
foreach ($sps as $sp) {
|
|
expect($sp->current_workdays)->toBe([1, 2, 3, 4, 5]);
|
|
}
|
|
|
|
// 2) HTTP payload к порталу содержал ["1","2","3","4","5"], не ["1".."7"].
|
|
expect($capturedWorkdays)->toBe(['1', '2', '3', '4', '5']);
|
|
});
|
|
|
|
it('online mode update-path: existing supplier_projects.current_workdays is refreshed (not just regions/limit)', function (): void {
|
|
// Regression: forceFill ранее не включал current_workdays — после первого create со
|
|
// старым хардкод-[1..7] последующий ресинк не подтягивал реальные дни.
|
|
DB::table('system_settings')->where('key', 'supplier_export_mode')->update(['value' => 'online']);
|
|
|
|
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
|
|
$project = Project::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'signal_type' => 'call',
|
|
'signal_identifier' => '79991234567',
|
|
'is_active' => true,
|
|
'daily_limit_target' => 9,
|
|
'regions' => [],
|
|
'delivery_days_mask' => 31, // Пн-Пт
|
|
]);
|
|
|
|
// Pre-seed existing supplier_projects со старыми (хардкод-)workdays.
|
|
foreach (['B1', 'B2', 'B3'] as $platform) {
|
|
SupplierProject::create([
|
|
'platform' => $platform,
|
|
'signal_type' => 'call',
|
|
'unique_key' => '79991234567',
|
|
'subject_code' => null,
|
|
'supplier_external_id' => '99'.$platform,
|
|
'current_limit' => 6,
|
|
'current_workdays' => [1, 2, 3, 4, 5, 6, 7],
|
|
'current_regions' => [],
|
|
'sync_status' => 'ok',
|
|
'last_synced_at' => now()->subDay(),
|
|
]);
|
|
}
|
|
|
|
$this->mock(SupplierProjectChannel::class, function ($mock): void {
|
|
$mock->shouldReceive('updateProject')->times(3)->andReturn(true);
|
|
});
|
|
|
|
(new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class));
|
|
|
|
$sps = SupplierProject::where('unique_key', '79991234567')->get();
|
|
expect($sps)->toHaveCount(3);
|
|
foreach ($sps as $sp) {
|
|
expect($sp->current_workdays)->toBe([1, 2, 3, 4, 5]);
|
|
expect($sp->current_limit)->toBe(9);
|
|
}
|
|
});
|
|
|
|
it('online mode all-RF (no regions): 1 group subject_code=null, 3 supplier_projects + 3 pivot links', function (): void {
|
|
DB::table('system_settings')->where('key', 'supplier_export_mode')->update(['value' => 'online']);
|
|
|
|
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
|
|
$project = Project::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'signal_type' => 'site',
|
|
'signal_identifier' => 'allrf.example.com',
|
|
'is_active' => true,
|
|
'daily_limit_target' => 6,
|
|
'regions' => [],
|
|
'delivery_days_mask' => 127,
|
|
]);
|
|
|
|
Http::fake([
|
|
'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
|
|
['status' => 'OK', 'message' => '', 'result' => null, 'id' => '500'],
|
|
200,
|
|
),
|
|
'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
|
|
['projects' => [
|
|
['id' => '500', 'src' => 'rt', 'name' => 'allrf.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'allrf.example.com'],
|
|
['id' => '501', 'src' => 'bl', 'name' => 'allrf.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'allrf.example.com'],
|
|
['id' => '502', 'src' => 'mt', 'name' => 'allrf.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'allrf.example.com'],
|
|
]],
|
|
200,
|
|
),
|
|
]);
|
|
|
|
(new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class));
|
|
|
|
$sps = SupplierProject::where('unique_key', 'allrf.example.com')->get();
|
|
expect($sps)->toHaveCount(3);
|
|
expect($sps->pluck('subject_code')->unique()->all())->toContain(null);
|
|
|
|
expect(DB::table('project_supplier_links')->where('project_id', $project->id)->count())->toBe(3);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Batch mode: keeps каркас (limit 0, no per-subject save, no pivot)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('batch mode keeps каркас (limit=0, sets supplier_b{1,2,3}_project_id, no project_supplier_links pivot)', function (): void {
|
|
// batch is already set in beforeEach — no change needed
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
$project = Project::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'signal_type' => 'site',
|
|
'signal_identifier' => 'batch-test.ru',
|
|
'is_active' => true,
|
|
'daily_limit_target' => 10,
|
|
'regions' => [82],
|
|
'delivery_days_mask' => 127,
|
|
]);
|
|
|
|
$this->mock(SupplierProjectChannel::class, function ($mock): void {
|
|
$mock->shouldReceive('createProject')->times(3)->andReturn(200001, 200002, 200003);
|
|
});
|
|
|
|
(new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class));
|
|
|
|
$project->refresh();
|
|
// Batch: the old FK columns are set
|
|
expect($project->supplier_b1_project_id)->not->toBeNull();
|
|
expect($project->supplier_b2_project_id)->not->toBeNull();
|
|
expect($project->supplier_b3_project_id)->not->toBeNull();
|
|
|
|
// Batch: каркас → limit=0
|
|
$sp = SupplierProject::find($project->supplier_b1_project_id);
|
|
expect($sp->current_limit)->toBe(0);
|
|
|
|
// Batch: no pivot rows (nightly job fills them)
|
|
expect(DB::table('project_supplier_links')->where('project_id', $project->id)->count())->toBe(0);
|
|
});
|