feat(mail-campaigns): sender info + resolve typed emails against customers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-22 21:18:53 +07:00
co-authored by Claude Opus 5.5
parent 54c14c41d8
commit e1efc88fd2
10 changed files with 277 additions and 18 deletions
+15
View File
@@ -55,6 +55,21 @@ class LeadApiTest extends TestCase
$this->assertSame(0, Lead::count());
}
public function test_index_filters_by_source_and_recorded_date(): void
{
$admin = $this->admin(['lead.view']);
$match = Lead::create(['email' => 'a@example.com', 'source' => 'whmcs_contact']);
Lead::create(['email' => 'b@example.com', 'source' => 'manual']);
$old = Lead::create(['email' => 'c@example.com', 'source' => 'whmcs_contact']);
$old->forceFill(['created_at' => now()->subMonth()])->saveQuietly();
$this->actingAs($admin, 'admin')
->getJson(self::BASE . '?' . http_build_query(['source' => 'whmcs_contact', 'date_from' => now()->subDay()->toDateString(), 'date_to' => now()->toDateString()]))
->assertOk()
->assertJsonPath('data.total', 1)
->assertJsonPath('data.items.0.uuid', $match->uuid);
}
public function test_permissions_are_enforced(): void
{
$viewer = $this->admin(['lead.view']);
@@ -7,7 +7,10 @@ namespace Tests\Feature\Api\Admin;
use App\Jobs\Mail\SendMailCampaignRecipient;
use App\Models\Mail;
use App\Models\MailCampaign;
use App\Models\Setting;
use App\Models\User;
use App\Support\MailSettings;
use Illuminate\Support\Facades\Cache;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail as MailFacade;
use Illuminate\Support\Facades\Queue;
@@ -36,6 +39,41 @@ class MailCampaignBulkActionTest extends TestCase
]);
}
public function test_index_filters_by_search_status_and_date(): void
{
$match = $this->campaign('completed');
$match->update(['name' => 'Khuyến mãi Tết']);
$this->campaign('failed')->update(['name' => 'Khuyến mãi hè']);
$this->campaign('completed')->update(['name' => 'Bảo trì']);
$old = $this->campaign('completed');
$old->update(['name' => 'Khuyến mãi cũ']);
$old->forceFill(['created_at' => now()->subMonth()])->saveQuietly();
$this->actingAs($this->admin, 'admin')
->getJson('/api/v1/administrator/mail-campaigns?' . http_build_query([
'search' => 'khuyến mãi', 'status' => ['completed'], 'date_from' => now()->subDay()->toDateString(),
]))
->assertOk()
->assertJsonPath('data.total', 1)
->assertJsonPath('data.items.0.uuid', $match->uuid);
}
public function test_sender_reports_smtp_from_address(): void
{
Setting::updateOrCreate(['key' => 'smtp'], ['value' => json_encode([
'host' => 'smtp.example.com', 'user' => 'u@example.com', 'from_address' => 'noreply@tgs.test', 'from_name' => 'TGS',
])]);
Cache::forget(MailSettings::cacheKey());
MailSettings::apply();
$this->actingAs($this->admin, 'admin')
->getJson('/api/v1/administrator/mail-campaigns/sender')
->assertOk()
->assertJsonPath('data.from_address', 'noreply@tgs.test')
->assertJsonPath('data.from_name', 'TGS')
->assertJsonPath('data.smtp_configured', true);
}
public function test_cancel_stops_pending_jobs_and_keeps_finished_campaigns(): void
{
MailFacade::fake();
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api\Admin;
use App\Jobs\Mail\SendMailCampaignRecipient;
use App\Models\Customer;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
class MailCampaignResolveEmailsTest extends TestCase
{
use RefreshDatabase;
private function admin(): User
{
$role = Role::create(['name' => 'role-' . uniqid(), 'guard_name' => 'admin']);
$role->givePermissionTo(Permission::firstOrCreate(['name' => 'mail-campaign.create', 'guard_name' => 'admin']));
$admin = User::factory()->create();
$admin->assignRole($role);
return $admin;
}
public function test_existing_customer_email_is_matched_case_insensitively(): void
{
$customer = Customer::factory()->create(['email' => 'Bao.Dev@Example.com']);
$this->actingAs($this->admin(), 'admin')
->postJson('/api/v1/administrator/mail-campaigns/resolve-emails', [
'emails' => [' bao.dev@example.COM ', 'new.person@example.com'],
])
->assertOk()
->assertJsonCount(1, 'data.customers')
->assertJsonPath('data.customers.0.uuid', $customer->uuid)
->assertJsonPath('data.unregistered', ['new.person@example.com']);
}
public function test_invalid_emails_are_rejected_with_clear_message(): void
{
$this->actingAs($this->admin(), 'admin')
->postJson('/api/v1/administrator/mail-campaigns/resolve-emails', [
'emails' => ['not-an-email'],
])
->assertUnprocessable()
->assertJsonValidationErrors(['emails.0' => 'Email "not-an-email" không hợp lệ.']);
$this->actingAs($this->admin(), 'admin')
->postJson('/api/v1/administrator/mail-campaigns/resolve-emails', ['emails' => []])
->assertUnprocessable()
->assertJsonValidationErrors(['emails' => 'Nhập ít nhất một email.']);
}
public function test_store_attaches_customer_typed_as_email_with_different_case(): void
{
Queue::fake();
$customer = Customer::factory()->create(['email' => 'Bao.Dev@Example.com', 'email_marketing' => 1]);
$this->actingAs($this->admin(), 'admin')
->postJson('/api/v1/administrator/mail-campaigns', [
'subject' => 'Tin tức',
'message' => '<p>Hi</p>',
'recipient_type' => 'selected',
'uuids' => [$customer->uuid],
'emails' => ['bao.dev@example.com'],
])
->assertOk()
->assertJsonPath('data.total_recipients', 1);
Queue::assertPushed(
SendMailCampaignRecipient::class,
fn (SendMailCampaignRecipient $job) => $this->jobCustomerId($job) === $customer->id
);
}
private function jobCustomerId(SendMailCampaignRecipient $job): ?int
{
return (fn () => $this->customerId)->call($job);
}
}