Files
portal/app/tests/Feature/Admin/AdminTenantActivityFeedTest.php
T

167 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Models\User;
use App\Services\Admin\TenantActivityFeedService;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
function feedTenant(): array
{
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id, 'email' => 'client@example.test']);
return [(int) $tenant->id, (int) $user->id];
}
function feedOps(int $tenantId, ?int $userId, string $event, ?int $entityId, ?array $after, ?array $before, string $mskTime): void
{
DB::table('tenant_operations_log')->insert([
'tenant_id' => $tenantId,
'user_id' => $userId,
'entity_type' => 'project',
'entity_id' => $entityId,
'event' => $event,
'payload_before' => $before !== null ? json_encode($before, JSON_UNESCAPED_UNICODE) : null,
'payload_after' => $after !== null ? json_encode($after, JSON_UNESCAPED_UNICODE) : null,
'created_at' => CarbonImmutable::parse($mskTime, 'Europe/Moscow')->utc(),
]);
}
function feedActivity(int $tenantId, ?int $userId, string $event, int $dealId, string $mskTime): void
{
DB::table('activity_log')->insert([
'tenant_id' => $tenantId,
'user_id' => $userId,
'deal_id' => $dealId,
'event' => $event,
'created_at' => CarbonImmutable::parse($mskTime, 'Europe/Moscow')->utc(),
]);
}
it('берёт имя проекта из журнала, а не номер', function () {
[$tenantId, $userId] = feedTenant();
feedOps($tenantId, $userId, 'project.created', 999_001, ['name' => 'Двери'], null, '2026-08-05 10:10:00');
$items = app(TenantActivityFeedService::class)->forTenant($tenantId, 50, 0);
expect($items)->toHaveCount(1);
expect($items[0]['kind'])->toBe('project');
expect($items[0]['event'])->toBe('project.created');
expect($items[0]['subject'])->toBe('Двери');
expect($items[0]['actor'])->toBe('client@example.test');
});
it('берёт имя удалённого проекта из снимка «до»', function () {
[$tenantId, $userId] = feedTenant();
feedOps($tenantId, $userId, 'project.deleted', 999_002, null, ['name' => 'Окна ЕКБ'], '2026-08-05 11:00:00');
$items = app(TenantActivityFeedService::class)->forTenant($tenantId, 50, 0);
expect($items[0]['subject'])->toBe('Окна ЕКБ');
});
it('считает, сколько проектов задето групповым действием', function () {
[$tenantId, $userId] = feedTenant();
feedOps($tenantId, $userId, 'project.bulk_pause', null, ['ids' => [1, 2, 3]], null, '2026-08-05 12:00:00');
$items = app(TenantActivityFeedService::class)->forTenant($tenantId, 50, 0);
expect($items[0]['bulk_count'])->toBe(3);
});
it('пускает ручную работу со сделками и НЕ пускает системную', function () {
[$tenantId, $userId] = feedTenant();
feedActivity($tenantId, $userId, 'deal.status_changed', 555, '2026-08-05 13:00:00');
feedActivity($tenantId, null, 'deal.created', 556, '2026-08-05 13:05:00');
$items = app(TenantActivityFeedService::class)->forTenant($tenantId, 50, 0);
expect($items)->toHaveCount(1);
expect($items[0]['event'])->toBe('deal.status_changed');
expect($items[0]['subject'])->toBe('555');
});
it('пускает пополнение и НЕ пускает списания за лид и правку баланса админом', function () {
[$tenantId, $userId] = feedTenant();
DB::table('balance_transactions')->insert([
['tenant_id' => $tenantId, 'user_id' => $userId, 'type' => 'topup',
'amount_rub' => '1000.00', 'created_at' => CarbonImmutable::parse('2026-08-05 14:00:00', 'Europe/Moscow')->utc()],
['tenant_id' => $tenantId, 'user_id' => null, 'type' => 'lead_charge',
'amount_rub' => '-20.00', 'created_at' => CarbonImmutable::parse('2026-08-05 14:05:00', 'Europe/Moscow')->utc()],
['tenant_id' => $tenantId, 'user_id' => null, 'type' => 'manual_adjustment',
'amount_rub' => '500.00', 'created_at' => CarbonImmutable::parse('2026-08-05 14:10:00', 'Europe/Moscow')->utc()],
]);
$items = app(TenantActivityFeedService::class)->forTenant($tenantId, 50, 0);
expect($items)->toHaveCount(1);
expect($items[0]['kind'])->toBe('money');
expect($items[0]['event'])->toBe('balance.topup');
expect($items[0]['amount_rub'])->toBe('1000.00');
});
it('отдаёт свежее первым и умеет постранично', function () {
[$tenantId, $userId] = feedTenant();
feedOps($tenantId, $userId, 'project.created', 999_010, ['name' => 'Первый'], null, '2026-08-01 10:00:00');
feedOps($tenantId, $userId, 'project.created', 999_011, ['name' => 'Второй'], null, '2026-08-02 10:00:00');
feedOps($tenantId, $userId, 'project.created', 999_012, ['name' => 'Третий'], null, '2026-08-03 10:00:00');
$service = app(TenantActivityFeedService::class);
$first = $service->forTenant($tenantId, 2, 0);
expect(array_column($first, 'subject'))->toBe(['Третий', 'Второй']);
$second = $service->forTenant($tenantId, 2, 2);
expect(array_column($second, 'subject'))->toBe(['Первый']);
});
it('считает дела за период тем же отбором, что и лента', function () {
[$tenantId, $userId] = feedTenant();
feedOps($tenantId, $userId, 'project.created', 999_020, ['name' => 'Свежий'], null, '2026-08-05 10:00:00');
feedOps($tenantId, $userId, 'project.created', 999_021, ['name' => 'Старый'], null, '2026-05-01 10:00:00');
feedActivity($tenantId, null, 'deal.created', 777, '2026-08-05 10:30:00');
$since = CarbonImmutable::parse('2026-07-09 00:00:00', 'Europe/Moscow')->utc();
expect(app(TenantActivityFeedService::class)->countSince($tenantId, $since))->toBe(1);
});
it('карточка клиента отдаёт блок «жив ли» и ленту без системного шума', function () {
$tenant = Tenant::factory()->create(['subdomain' => 'testalive']);
$user = User::factory()->create(['tenant_id' => $tenant->id, 'email' => 'client@example.test']);
feedOps((int) $tenant->id, (int) $user->id, 'project.created', 999_100, ['name' => 'Двери'], null, '2026-08-05 10:10:00');
feedActivity((int) $tenant->id, null, 'deal.created', 888, '2026-08-05 11:00:00');
$resp = $this->getJson('/api/admin/tenants/testalive');
$resp->assertOk()
->assertJsonPath('aliveness.state', 'no_data')
->assertJsonCount(1, 'activity.items')
->assertJsonPath('activity.items.0.event', 'project.created')
->assertJsonPath('activity.items.0.subject', 'Двери')
->assertJsonPath('activity.has_more', false);
});
it('отдаёт следующую страницу ленты отдельной ручкой', function () {
$tenant = Tenant::factory()->create(['subdomain' => 'testpage']);
$user = User::factory()->create(['tenant_id' => $tenant->id]);
feedOps((int) $tenant->id, (int) $user->id, 'project.created', 999_200, ['name' => 'Первый'], null, '2026-08-01 10:00:00');
feedOps((int) $tenant->id, (int) $user->id, 'project.created', 999_201, ['name' => 'Второй'], null, '2026-08-02 10:00:00');
$resp = $this->getJson('/api/admin/tenants/testpage/activity?offset=1&limit=1');
$resp->assertOk()
->assertJsonCount(1, 'items')
->assertJsonPath('items.0.subject', 'Первый')
->assertJsonPath('has_more', false);
});