Files
portal/app/tests/Feature/Supplier/CleanupInactiveSupplierProjectsJobTest.php
T
Дмитрий e87b1385cf feat(supplier): verify rt-project-* contract live on crm.bp-gr.ru
Live discovery через Playwright MCP (Task 1):
- создан LIDPOTOK_TEST_DELETE_ME (B1+B2+B3) → 3 rt-проекта на портале;
- записаны сетевые запросы /admin/visit/rt-*;
- все три проекта удалены вручную, портал чист.

Endpoints (verified):
- POST /admin/visit/rt-project-save (create id:0, update id:N — same URL)
- POST /admin/visit/rt-project-delete (id строкой)
- GET  /admin/visit/rt-projects-load?src=none

Все три — application/json. Конверт ответа:
- success: HTTP 200 + {status:OK, message, result, id?:string}
- error:   HTTP 200 + {status:Error, message, result:null}
ID — строка (12721245), приводится к int (fits в int64).
Один save с B1+B2+B3 включёнными создаёт 3 rt-проекта — toPayload()
шлёт ровно один платформенный флаг (srcrt|srcbl|srcmt).

SupplierPortalClient:
- docblock переписан под verified контракт
- listProjects: путь /admin/visit/rt-projects-load + ?src=none query
- saveProject: путь /admin/visit/rt-project-save, asJson, парсинг id
- updateProject: тот же endpoint что save, id:N в body
- deleteProject: путь /admin/visit/rt-project-delete, asJson, id строкой
- new assertStatusOk() — HTTP 200 + status:Error → SupplierClientException
- toPayload(): полный Vuex-payload с маппингом DTO → portal:
  - platform B1/B2/B3 → srcrt/srcbl/srcmt (single-true)
  - signalType site/call/sms → type:hosts/calls/sms
  - workdays int[] → string[]
  - status active/paused → bool
  - + tag:_lidpotok, name/content из uniqueKey, defaults для show/depth/etc

Tests:
- new: tests/Feature/Supplier/SupplierPortalClientRtProjectTest.php (7 tests,
  contract: save+update+delete+list + 2 status:Error error-paths + B2/calls
  mapping)
- Sync/Cleanup/Unit тесты обновлены под новый URL + envelope shape.

Закрывает spec §1 honest-caveat «placeholder, не верифицирован»
и журнал решений запись 9. Регрессия: Pest 944/941/0 failed / 3 skipped
/ 2768 assertions / 59.2s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:05 +03:00

164 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Jobs\Supplier\CleanupInactiveSupplierProjectsJob;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\SupplierSyncLog;
use App\Models\Tenant;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
beforeEach(function (): void {
Cache::store('redis')->put('supplier:session', [
'phpsessid' => 'sess',
'csrf' => 'csrf',
'refreshed_at' => now()->toIso8601String(),
], now()->addHours(6));
config(['services.supplier.portal_url' => 'https://crm.bp-gr.ru']);
});
afterEach(function (): void {
Cache::store('redis')->forget('supplier:session');
Carbon::setTestNow();
});
test('phase A re-activates supplier_project when active liderra project still references it', function (): void {
$tenant = Tenant::factory()->create();
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'reactivate.example.com',
'inactive_since' => now()->subDays(30),
]);
Project::factory()->create([
'tenant_id' => $tenant->id,
'is_active' => true,
'signal_type' => 'site',
'signal_identifier' => 'reactivate.example.com',
'supplier_b1_project_id' => $sp->id,
]);
(new CleanupInactiveSupplierProjectsJob)->handle();
expect($sp->fresh()->inactive_since)->toBeNull();
});
test('phase B marks inactive_since=NOW for newly orphaned supplier_project', function (): void {
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'orphan.example.com',
'inactive_since' => null,
]);
Carbon::setTestNow(Carbon::parse('2026-05-12 02:00:00'));
(new CleanupInactiveSupplierProjectsJob)->handle();
expect($sp->fresh()->inactive_since)->not->toBeNull();
});
test('phase C deletes supplier_project after 180 days inactive and writes audit row', function (): void {
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'old.example.com',
'supplier_external_id' => '999',
'inactive_since' => now()->subDays(181),
]);
Http::fake([
'crm.bp-gr.ru/admin/visit/rt-project-delete' => Http::response(
['status' => 'OK', 'message' => '', 'result' => null],
200,
),
]);
(new CleanupInactiveSupplierProjectsJob)->handle();
expect(SupplierProject::on('pgsql_supplier')->find($sp->id))->toBeNull();
// FK ON DELETE SET NULL зануляет supplier_project_id у лога после delete'а
// supplier_project — трассировка через request_payload (JSONB snapshot).
expect(
SupplierSyncLog::on('pgsql_supplier')
->where('action', 'delete')
->where('http_status', 200)
->whereRaw("request_payload->>'supplier_project_id' = ?", [(string) $sp->id])
->exists()
)->toBeTrue();
});
test('phase A runs before phase C (safety ordering): returned-active is reactivated, not deleted', function (): void {
$tenant = Tenant::factory()->create();
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'edge.example.com',
'supplier_external_id' => '888',
'inactive_since' => now()->subDays(185),
]);
Project::factory()->create([
'tenant_id' => $tenant->id,
'is_active' => true,
'signal_type' => 'site',
'signal_identifier' => 'edge.example.com',
'supplier_b1_project_id' => $sp->id,
]);
Http::fake();
(new CleanupInactiveSupplierProjectsJob)->handle();
expect($sp->fresh()?->inactive_since)->toBeNull();
expect(SupplierProject::on('pgsql_supplier')->find($sp->id))->not->toBeNull();
Http::assertNothingSent();
});
test('handles 404 from supplier as already-deleted: local delete + audit row with 404 status', function (): void {
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'ghost.example.com',
'supplier_external_id' => '777',
'inactive_since' => now()->subDays(181),
]);
Http::fake([
'crm.bp-gr.ru/admin/visit/rt-project-delete' => Http::response('not found', 404),
]);
(new CleanupInactiveSupplierProjectsJob)->handle();
expect(SupplierProject::on('pgsql_supplier')->find($sp->id))->toBeNull();
expect(
SupplierSyncLog::on('pgsql_supplier')
->where('action', 'delete')
->where('http_status', 404)
->whereRaw("request_payload->>'supplier_project_id' = ?", [(string) $sp->id])
->exists()
)->toBeTrue();
});
test('does not delete supplier_project marked inactive less than 180 days ago', function (): void {
$sp = SupplierProject::factory()->create([
'platform' => 'B1',
'signal_type' => 'site',
'unique_key' => 'recent.example.com',
'supplier_external_id' => '555',
'inactive_since' => now()->subDays(100),
]);
Http::fake();
(new CleanupInactiveSupplierProjectsJob)->handle();
expect(SupplierProject::on('pgsql_supplier')->find($sp->id))->not->toBeNull();
Http::assertNothingSent();
});