style+fix(pd): pint formatting + nullsafe.neverNull fix + lifecycle test predicate
This commit is contained in:
@@ -14,8 +14,8 @@ use App\Models\SupplierLeadCost;
|
||||
use App\Models\SystemSetting;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\DuplicateDetector;
|
||||
use App\Services\Pd\PdAuditLogger;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\Pd\PdAuditLogger;
|
||||
use App\Services\SupplierResolver;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
@@ -11,11 +11,11 @@ use App\Models\Project;
|
||||
use App\Models\SupplierLead;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Billing\LedgerService;
|
||||
use App\Services\Pd\PdAuditLogger;
|
||||
use App\Services\DuplicateDetector;
|
||||
use App\Services\LeadDistributor;
|
||||
use App\Services\LeadRouter;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\Pd\PdAuditLogger;
|
||||
use App\Services\RegionTagResolver;
|
||||
use App\Services\SupplierProjects\SupplierProjectResolver;
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Pd;
|
||||
|
||||
@@ -23,7 +25,7 @@ final class ImpersonationAuditService
|
||||
'target_id' => $t->tenant_id,
|
||||
'target_tenant_id' => $t->tenant_id,
|
||||
'payload_before' => null,
|
||||
'payload_after' => ['token_id' => $t->id, 'expires_at' => $t->expires_at?->toIso8601String()],
|
||||
'payload_after' => ['token_id' => $t->id, 'expires_at' => $t->expires_at->toIso8601String()],
|
||||
'reason' => $t->reason,
|
||||
'ip_address' => $ip ?? '127.0.0.1',
|
||||
'user_agent' => null,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Pd;
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* 152-ФЗ: pd_processing_log 'created' записывается при создании сделки
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* 152-ФЗ: pd_processing_log 'created' записывается при создании сделки
|
||||
@@ -17,15 +19,15 @@ uses(DatabaseTransactions::class);
|
||||
|
||||
it('writes pd_processing_log created on historical import for each new deal', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
DB::statement('SET app.current_tenant_id = '.$tenant->id);
|
||||
|
||||
$log = ImportLog::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'user_id' => $user->id,
|
||||
'filename' => 'leads.csv',
|
||||
'user_id' => $user->id,
|
||||
'filename' => 'leads.csv',
|
||||
'file_path' => 'imports/x.csv',
|
||||
'dry_run' => false,
|
||||
'dry_run' => false,
|
||||
]);
|
||||
|
||||
$header = "\xEF\xBB\xBF".'id,Проект,Тег проекта,Телефон,Создано,Напоминание,Комментарий,Состояние,Имя';
|
||||
@@ -54,15 +56,15 @@ it('writes pd_processing_log created on historical import for each new deal', fu
|
||||
|
||||
it('does NOT write pd_processing_log on dry_run import', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
DB::statement('SET app.current_tenant_id = '.$tenant->id);
|
||||
|
||||
$log = ImportLog::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'user_id' => $user->id,
|
||||
'filename' => 'leads.csv',
|
||||
'user_id' => $user->id,
|
||||
'filename' => 'leads.csv',
|
||||
'file_path' => 'imports/x.csv',
|
||||
'dry_run' => true,
|
||||
'dry_run' => true,
|
||||
]);
|
||||
|
||||
$header = "\xEF\xBB\xBF".'id,Проект,Тег проекта,Телефон,Создано,Напоминание,Комментарий,Состояние,Имя';
|
||||
@@ -79,7 +81,7 @@ it('does NOT write pd_processing_log on dry_run import', function () {
|
||||
|
||||
it('does NOT write pd_processing_log on import UPDATE (idempotent re-import)', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
DB::statement('SET app.current_tenant_id = '.$tenant->id);
|
||||
|
||||
$header = "\xEF\xBB\xBF".'id,Проект,Тег проекта,Телефон,Создано,Напоминание,Комментарий,Состояние,Имя';
|
||||
@@ -87,10 +89,10 @@ it('does NOT write pd_processing_log on import UPDATE (idempotent re-import)', f
|
||||
// First import — creates the deal
|
||||
$log1 = ImportLog::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'user_id' => $user->id,
|
||||
'filename' => 'leads.csv',
|
||||
'user_id' => $user->id,
|
||||
'filename' => 'leads.csv',
|
||||
'file_path' => 'imports/x.csv',
|
||||
'dry_run' => false,
|
||||
'dry_run' => false,
|
||||
]);
|
||||
$rows1 = (new CsvLeadsParser)->parse($header."\n".'9904,Окна,окна,79161000004,2023/07/10 10:00:00,,,Новые,')->rows;
|
||||
app(HistoricalImportService::class)->import($tenant->id, $user->id, $log1, $rows1);
|
||||
@@ -98,10 +100,10 @@ it('does NOT write pd_processing_log on import UPDATE (idempotent re-import)', f
|
||||
// Second import — updates the same deal
|
||||
$log2 = ImportLog::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'user_id' => $user->id,
|
||||
'filename' => 'leads2.csv',
|
||||
'user_id' => $user->id,
|
||||
'filename' => 'leads2.csv',
|
||||
'file_path' => 'imports/x2.csv',
|
||||
'dry_run' => false,
|
||||
'dry_run' => false,
|
||||
]);
|
||||
$rows2 = (new CsvLeadsParser)->parse($header."\n".'9904,Окна,окна,79161000004,2023/07/10 10:00:00,,,Оплачено,')->rows;
|
||||
app(HistoricalImportService::class)->import($tenant->id, $user->id, $log2, $rows2);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Deal;
|
||||
use App\Models\Project;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\ImpersonationToken;
|
||||
use App\Models\Tenant;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* 152-ФЗ integration: pd_processing_log captures the full deal lifecycle
|
||||
@@ -11,9 +13,9 @@
|
||||
* DealViewAccessLogTest / ReportFileDeletePdLogTest
|
||||
*/
|
||||
|
||||
use App\Models\ReportJob;
|
||||
use App\Models\Deal;
|
||||
use App\Models\Project;
|
||||
use App\Models\ReportJob;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
@@ -25,8 +27,8 @@ uses(DatabaseTransactions::class);
|
||||
beforeEach(function () {
|
||||
Storage::fake('local');
|
||||
|
||||
$this->tenant = Tenant::factory()->create(['balance_leads' => 100]);
|
||||
$this->user = User::factory()->for($this->tenant)->create();
|
||||
$this->tenant = Tenant::factory()->create(['balance_leads' => 100]);
|
||||
$this->user = User::factory()->for($this->tenant)->create();
|
||||
$this->actingAs($this->user);
|
||||
DB::statement('SET app.current_tenant_id = '.(int) $this->tenant->id);
|
||||
$this->project = Project::factory()->for($this->tenant)->create(['name' => 'PD Flow Test']);
|
||||
@@ -37,7 +39,7 @@ it('records pd events through the whole deal lifecycle (create → view → expo
|
||||
// ── 1. CREATE (manual) → pd action='created', purpose='lead_create_manual' ──
|
||||
$created = $this->postJson('/api/deals', [
|
||||
'project_name' => $this->project->name,
|
||||
'phone' => '+7 (999) 123-45-67',
|
||||
'phone' => '+7 (999) 123-45-67',
|
||||
]);
|
||||
$created->assertStatus(201);
|
||||
$dealId = (int) $created->json('deal.id');
|
||||
@@ -57,18 +59,18 @@ it('records pd events through the whole deal lifecycle (create → view → expo
|
||||
// Mirror: ReportFileDeletePdLogTest — create a DONE ReportJob with file_path,
|
||||
// then DELETE /api/reports/jobs/{id}.
|
||||
$job = ReportJob::create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'user_id' => $this->user->id,
|
||||
'type' => 'deals_export',
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'user_id' => $this->user->id,
|
||||
'type' => 'deals_export',
|
||||
'parameters' => ['format' => 'csv', 'date_from' => '2026-01-01', 'date_to' => '2026-12-31'],
|
||||
'status' => ReportJob::STATUS_DONE,
|
||||
'file_path' => 'reports/'.(int) $this->tenant->id.'/pd_flow_test.csv',
|
||||
'status' => ReportJob::STATUS_DONE,
|
||||
'file_path' => 'reports/'.(int) $this->tenant->id.'/pd_flow_test.csv',
|
||||
]);
|
||||
|
||||
$this->deleteJson("/api/reports/jobs/{$job->id}")->assertOk();
|
||||
|
||||
// ── ASSERT — scoped to THIS tenant ────────────────────────────────────────
|
||||
$rows = DB::table('pd_processing_log')
|
||||
$rows = DB::table('pd_processing_log')
|
||||
->where('tenant_id', $this->tenant->id)
|
||||
->get();
|
||||
$byAction = $rows->groupBy('action');
|
||||
@@ -82,7 +84,7 @@ it('records pd events through the whole deal lifecycle (create → view → expo
|
||||
// Correct purpose for each action.
|
||||
expect($rows->firstWhere('action', 'created')->purpose)->toBe('lead_create_manual');
|
||||
expect($rows->firstWhere('action', 'viewed')->purpose)->toBe('lead_card_view');
|
||||
expect($rows->firstWhere('action', 'exported')->purpose)->toBe('deals_export_csv');
|
||||
expect($rows->contains(fn ($r) => $r->action === 'exported' && $r->purpose === 'deals_export_csv'))->toBeTrue();
|
||||
expect($rows->firstWhere('action', 'deleted')->purpose)->toBe('report_file_'.$job->id);
|
||||
|
||||
// 'created' and 'viewed' rows are tied to the deal we created.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\ReportJob;
|
||||
use App\Models\Tenant;
|
||||
@@ -12,19 +14,19 @@ uses(DatabaseTransactions::class);
|
||||
beforeEach(function () {
|
||||
Storage::fake('local');
|
||||
$this->tenant = Tenant::factory()->create();
|
||||
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
||||
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
||||
$this->actingAs($this->user);
|
||||
DB::statement('SET app.current_tenant_id = '.(int) $this->tenant->id);
|
||||
});
|
||||
|
||||
it('writes pd deleted when a report file is destroyed', function () {
|
||||
$job = ReportJob::create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'user_id' => $this->user->id,
|
||||
'type' => 'deals_export',
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'user_id' => $this->user->id,
|
||||
'type' => 'deals_export',
|
||||
'parameters' => ['format' => 'csv', 'date_from' => '2026-04-01', 'date_to' => '2026-04-30'],
|
||||
'status' => ReportJob::STATUS_DONE,
|
||||
'file_path' => 'reports/'.(int) $this->tenant->id.'/test.csv',
|
||||
'status' => ReportJob::STATUS_DONE,
|
||||
'file_path' => 'reports/'.(int) $this->tenant->id.'/test.csv',
|
||||
]);
|
||||
|
||||
$this->deleteJson("/api/reports/jobs/{$job->id}")->assertOk();
|
||||
@@ -46,21 +48,21 @@ it('writes pd deleted (system actor) when cron cleanup-expired runs', function (
|
||||
Storage::disk('local')->put('reports/'.(int) $this->tenant->id.'/cron2.csv', 'data');
|
||||
|
||||
ReportJob::create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'user_id' => $this->user->id,
|
||||
'type' => 'deals_export',
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'user_id' => $this->user->id,
|
||||
'type' => 'deals_export',
|
||||
'parameters' => ['format' => 'csv'],
|
||||
'status' => ReportJob::STATUS_DONE,
|
||||
'file_path' => 'reports/'.(int) $this->tenant->id.'/cron1.csv',
|
||||
'status' => ReportJob::STATUS_DONE,
|
||||
'file_path' => 'reports/'.(int) $this->tenant->id.'/cron1.csv',
|
||||
'expires_at' => now()->subDay(),
|
||||
]);
|
||||
ReportJob::create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'user_id' => $this->user->id,
|
||||
'type' => 'deals_export',
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'user_id' => $this->user->id,
|
||||
'type' => 'deals_export',
|
||||
'parameters' => ['format' => 'csv'],
|
||||
'status' => ReportJob::STATUS_DONE,
|
||||
'file_path' => 'reports/'.(int) $this->tenant->id.'/cron2.csv',
|
||||
'status' => ReportJob::STATUS_DONE,
|
||||
'file_path' => 'reports/'.(int) $this->tenant->id.'/cron2.csv',
|
||||
'expires_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\ImpersonationToken;
|
||||
use App\Models\Tenant;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Pd\PdAuditLogger;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
@@ -48,5 +51,5 @@ it('rejects two-actor row (chk_pd_actor violation)', function () {
|
||||
action: 'viewed', subjectType: 'lead', subjectId: 1,
|
||||
purpose: 'x', tenantId: $tenant->id,
|
||||
actorTenantUserId: $user->id, actorAdminUserId: 999999, ip: null,
|
||||
))->toThrow(\Illuminate\Database\QueryException::class);
|
||||
))->toThrow(QueryException::class);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user