2026-07-25 21:30:36 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
use App\Models\AdCampaign;
|
|
|
|
|
use App\Models\Tenant;
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
use Illuminate\Http\UploadedFile;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* T17 — загрузка «моего списка» номеров: POST /api/advertising/campaigns/{id}/phones.
|
|
|
|
|
*
|
|
|
|
|
* Фикстуры — только синтетические номера (+7 900 000-00-0X / 7900000000X), не ПДн.
|
|
|
|
|
*/
|
|
|
|
|
beforeEach(function () {
|
|
|
|
|
$this->tenant = Tenant::factory()->create();
|
|
|
|
|
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
|
|
|
|
$this->actingAs($this->user);
|
|
|
|
|
|
|
|
|
|
$this->campaign = AdCampaign::create([
|
|
|
|
|
'tenant_id' => $this->tenant->id,
|
|
|
|
|
'name' => 'Кампания под список номеров',
|
|
|
|
|
'audience_days' => 10,
|
|
|
|
|
'weekly_budget_rub' => '1000.00',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('recognizes valid phones from text, dedupes and skips invalid ones', function () {
|
|
|
|
|
$text = implode("\n", [
|
|
|
|
|
'+7 900 000-00-01',
|
|
|
|
|
'79000000002',
|
|
|
|
|
'89000000003',
|
|
|
|
|
'9000000004', // 10 digits without leading 7/8 — valid RU mobile
|
|
|
|
|
'79000000002', // duplicate of line 2
|
|
|
|
|
'not-a-phone', // invalid
|
|
|
|
|
'123', // invalid
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
|
|
|
|
|
'text' => $text,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertOk()
|
|
|
|
|
->assertJsonPath('recognized', 4)
|
|
|
|
|
->assertJsonPath('skipped', 3);
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('ad_campaign_phones', [
|
|
|
|
|
'tenant_id' => $this->tenant->id,
|
|
|
|
|
'campaign_id' => $this->campaign->id,
|
|
|
|
|
'phone' => '79000000001',
|
|
|
|
|
]);
|
|
|
|
|
$this->assertDatabaseHas('ad_campaign_phones', [
|
|
|
|
|
'campaign_id' => $this->campaign->id,
|
|
|
|
|
'phone' => '79000000002',
|
|
|
|
|
]);
|
|
|
|
|
$this->assertDatabaseHas('ad_campaign_phones', [
|
|
|
|
|
'campaign_id' => $this->campaign->id,
|
|
|
|
|
'phone' => '79000000003',
|
|
|
|
|
]);
|
|
|
|
|
$this->assertDatabaseHas('ad_campaign_phones', [
|
|
|
|
|
'campaign_id' => $this->campaign->id,
|
|
|
|
|
'phone' => '79000000004',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$count = DB::table('ad_campaign_phones')->where('campaign_id', $this->campaign->id)->count();
|
|
|
|
|
expect($count)->toBe(4);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('recognizes valid phones from an uploaded csv file', function () {
|
|
|
|
|
$content = implode("\n", [
|
|
|
|
|
'+7 900 000-00-05',
|
|
|
|
|
'79000000006',
|
|
|
|
|
'garbage',
|
|
|
|
|
]);
|
|
|
|
|
$file = UploadedFile::fake()->createWithContent('phones.csv', $content);
|
|
|
|
|
|
|
|
|
|
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
|
|
|
|
|
'file' => $file,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertOk()
|
|
|
|
|
->assertJsonPath('recognized', 2)
|
|
|
|
|
->assertJsonPath('skipped', 1);
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('ad_campaign_phones', [
|
|
|
|
|
'campaign_id' => $this->campaign->id,
|
|
|
|
|
'phone' => '79000000005',
|
|
|
|
|
]);
|
|
|
|
|
$this->assertDatabaseHas('ad_campaign_phones', [
|
|
|
|
|
'campaign_id' => $this->campaign->id,
|
|
|
|
|
'phone' => '79000000006',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not fail on repeated upload thanks to the unique constraint', function () {
|
|
|
|
|
$this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
|
|
|
|
|
'text' => '79000000007',
|
|
|
|
|
])->assertOk();
|
|
|
|
|
|
|
|
|
|
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
|
|
|
|
|
'text' => '79000000007',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertOk()->assertJsonPath('recognized', 1);
|
|
|
|
|
|
|
|
|
|
$count = DB::table('ad_campaign_phones')
|
|
|
|
|
->where('campaign_id', $this->campaign->id)
|
|
|
|
|
->where('phone', '79000000007')
|
|
|
|
|
->count();
|
|
|
|
|
expect($count)->toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-25 22:36:24 +03:00
|
|
|
it('returns 422 when neither file nor text is provided ("тихий ноль")', function () {
|
|
|
|
|
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", []);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(422)
|
|
|
|
|
->assertJsonPath('message', 'Загрузите файл или вставьте номера');
|
|
|
|
|
|
|
|
|
|
$count = DB::table('ad_campaign_phones')->where('campaign_id', $this->campaign->id)->count();
|
|
|
|
|
expect($count)->toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns 422 when text is only whitespace and no file is provided', function () {
|
|
|
|
|
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
|
|
|
|
|
'text' => ' ',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(422)
|
|
|
|
|
->assertJsonPath('message', 'Загрузите файл или вставьте номера');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects a file with a disallowed extension', function () {
|
|
|
|
|
$file = UploadedFile::fake()->createWithContent('phones.pdf', '%PDF-1.4 not a real pdf');
|
|
|
|
|
|
|
|
|
|
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
|
|
|
|
|
'file' => $file,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(422)->assertJsonValidationErrors('file');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects a file larger than the 5 MB limit', function () {
|
|
|
|
|
$file = UploadedFile::fake()->create('phones.csv', 5121); // KB, чуть больше лимита
|
|
|
|
|
|
|
|
|
|
$response = $this->postJson("/api/advertising/campaigns/{$this->campaign->id}/phones", [
|
|
|
|
|
'file' => $file,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(422)->assertJsonValidationErrors('file');
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-25 21:30:36 +03:00
|
|
|
it('returns 404 when uploading phones for another tenants campaign', function () {
|
|
|
|
|
$tenantB = Tenant::factory()->create();
|
|
|
|
|
$campaignB = AdCampaign::create([
|
|
|
|
|
'tenant_id' => $tenantB->id,
|
|
|
|
|
'name' => 'Чужая кампания',
|
|
|
|
|
'audience_days' => 10,
|
|
|
|
|
'weekly_budget_rub' => '1000.00',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->postJson("/api/advertising/campaigns/{$campaignB->id}/phones", [
|
|
|
|
|
'text' => '79000000008',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(404);
|
|
|
|
|
$this->assertDatabaseMissing('ad_campaign_phones', ['campaign_id' => $campaignB->id]);
|
|
|
|
|
});
|