feat(portal): lead module, WHMCS sync report/contacts/mapping, service lifecycle email templates
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
3c7710f1de
commit
a443ec3bf6
@@ -65,7 +65,7 @@ class IpAllocationTest extends TestCase
|
||||
$response = $this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/allocation/preview', $this->allocation([$pool->id]))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.subject', 'Thông báo cấp phát 1 địa chỉ IP');
|
||||
->assertJsonPath('data.subject', 'V/v: Thế Giới Số cấp phát 1 địa chỉ IP | Công ty A');
|
||||
|
||||
$this->assertStringContainsString('10.0.0.5', $response->json('data.html'));
|
||||
$this->assertStringContainsString('Colo <b>1U</b>', $response->json('data.html'));
|
||||
@@ -122,7 +122,7 @@ class IpAllocationTest extends TestCase
|
||||
->postJson(self::BASE . '/allocate', $payload)
|
||||
->assertOk();
|
||||
|
||||
Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => $mail->envelope()->subject === 'Thông báo cấp phát 2 địa chỉ IP');
|
||||
Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => $mail->envelope()->subject === 'V/v: Thế Giới Số cấp phát 2 địa chỉ IP | Công ty A');
|
||||
$this->assertSame('Dịch vụ IP', $first->fresh()->service_name);
|
||||
$this->assertSame('CLOUD VPS SILVER', $second->fresh()->service_name);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Lead\App\Models\Lead;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LeadApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const BASE = '/api/v1/administrator/leads';
|
||||
|
||||
private function admin(array $permissions): User
|
||||
{
|
||||
$role = Role::create(['name' => 'role-' . uniqid(), 'guard_name' => 'admin']);
|
||||
foreach ($permissions as $name) {
|
||||
$role->givePermissionTo(Permission::firstOrCreate(['name' => $name, 'guard_name' => 'admin']));
|
||||
}
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole($role);
|
||||
|
||||
return $admin;
|
||||
}
|
||||
|
||||
public function test_crud_flow_and_email_is_normalized_and_unique(): void
|
||||
{
|
||||
$admin = $this->admin(['lead.view', 'lead.create', 'lead.edit', 'lead.destroy']);
|
||||
|
||||
$uuid = $this->actingAs($admin, 'admin')
|
||||
->postJson(self::BASE, ['fullname' => 'Khách A', 'email' => ' Khach.A@Example.com ', 'status' => 'new'])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.email', 'khach.a@example.com')
|
||||
->json('data.uuid');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(self::BASE, ['email' => 'khach.a@example.com'])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->putJson(self::BASE . "/{$uuid}", ['email' => 'khach.a@example.com', 'status' => 'contacted', 'phone' => '0901234567'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.status', 'contacted');
|
||||
|
||||
$this->actingAs($admin, 'admin')->getJson(self::BASE . '?filter=0901')->assertOk()->assertJsonPath('data.total', 1);
|
||||
$this->actingAs($admin, 'admin')->getJson(self::BASE . "/{$uuid}")->assertOk()->assertJsonPath('data.phone', '0901234567');
|
||||
|
||||
$this->actingAs($admin, 'admin')->deleteJson(self::BASE . "/{$uuid}")->assertOk();
|
||||
$this->assertSame(0, Lead::count());
|
||||
}
|
||||
|
||||
public function test_permissions_are_enforced(): void
|
||||
{
|
||||
$viewer = $this->admin(['lead.view']);
|
||||
|
||||
$this->actingAs($viewer, 'admin')->getJson(self::BASE)->assertOk();
|
||||
$this->actingAs($viewer, 'admin')->postJson(self::BASE, ['email' => 'x@example.com'])->assertForbidden();
|
||||
$this->actingAs($viewer, 'admin')->deleteJson(self::BASE . '/' . uniqid())->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_bulk_status_and_bulk_delete_only_touch_given_uuids(): void
|
||||
{
|
||||
$admin = $this->admin(['lead.view', 'lead.edit', 'lead.destroy']);
|
||||
$leads = collect(['a', 'b', 'c'])->map(fn ($name) => Lead::create(['email' => "{$name}@example.com"]));
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(self::BASE . '/bulk-status', ['uuids' => [$leads[0]->uuid, $leads[1]->uuid], 'status' => 'contacted'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.affected', 2);
|
||||
$this->assertSame(2, Lead::where('status', 'contacted')->count());
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(self::BASE . '/bulk-status', ['uuids' => [$leads[2]->uuid], 'status' => 'bogus'])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(self::BASE . '/bulk-delete', ['uuids' => [$leads[0]->uuid, $leads[2]->uuid]])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.affected', 2);
|
||||
$this->assertSame([$leads[1]->uuid], Lead::pluck('uuid')->all());
|
||||
|
||||
$this->actingAs($this->admin(['lead.view']), 'admin')
|
||||
->postJson(self::BASE . '/bulk-delete', ['uuids' => [$leads[1]->uuid]])
|
||||
->assertForbidden();
|
||||
}
|
||||
}
|
||||
@@ -86,4 +86,22 @@ class MarketingEmailOptOutTest extends TestCase
|
||||
|
||||
Queue::assertNotPushed(SendMailCampaignRecipient::class);
|
||||
}
|
||||
|
||||
public function test_campaign_with_template_uses_edited_message_when_given(): void
|
||||
{
|
||||
Queue::fake();
|
||||
$admin = $this->admin();
|
||||
$customer = Customer::factory()->create(['email_marketing' => 1]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson('/api/v1/administrator/mail-campaigns', [
|
||||
'template_id' => $this->template(null)->id,
|
||||
'message' => '<p>Nội dung đã sửa</p>',
|
||||
'recipient_type' => 'selected',
|
||||
'uuids' => [$customer->uuid],
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$this->assertSame('<p>Nội dung đã sửa</p>', \App\Models\MailCampaign::latest('id')->first()->message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ class HostingAccountCreatedInvoiceTest extends TestCase
|
||||
$this->seed(\Database\Seeders\TemplateSeeder::class);
|
||||
$subject = fn (array $data) => (new HostingAccountCreated($data + ['server_name' => 'HOST-01']))->envelope()->subject;
|
||||
|
||||
$this->assertSame('V/v: '.\App\Support\MailGlobals::values()['company_name'].' gửi thông tin truy cập - HOST-01 - Nguyen Van A', $subject(['customer_name' => 'Nguyen Van A']));
|
||||
$this->assertStringEndsWith(' - Cong ty ABC', $subject(['customer_name' => ' ', 'customer_company' => 'Cong ty ABC']));
|
||||
$this->assertStringEndsWith(' - Công ty', $subject([]));
|
||||
$this->assertSame('V/v: Thế Giới Số gửi thông tin truy cập Hosting - HOST-01 | Nguyen Van A', $subject(['customer_name' => 'Nguyen Van A']));
|
||||
$this->assertStringEndsWith(' | Cong ty ABC', $subject(['customer_name' => ' ', 'customer_company' => 'Cong ty ABC']));
|
||||
$this->assertStringEndsWith(' | Công ty', $subject([]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Support\MailPlaceholders;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MailPlaceholdersConditionalTest extends TestCase
|
||||
{
|
||||
public function test_block_is_kept_only_when_variable_has_value(): void
|
||||
{
|
||||
$content = 'A{{#if kyc_url}}<a href="{{kyc_url}}">KYC</a>{{else}}none{{/if}}B';
|
||||
|
||||
$this->assertSame('AnoneB', MailPlaceholders::fill($content, ['kyc_url' => ' ']));
|
||||
$this->assertSame('AnoneB', MailPlaceholders::fill($content, []));
|
||||
$this->assertSame('A<a href="https://x.vn/k?a=1&b=2">KYC</a>B', MailPlaceholders::fill($content, ['kyc_url' => 'https://x.vn/k?a=1&b=2']));
|
||||
}
|
||||
|
||||
public function test_extract_ignores_conditional_keywords(): void
|
||||
{
|
||||
$this->assertSame(['kyc_url'], MailPlaceholders::extract('{{#if kyc_url}}{{kyc_url}}{{else}}x{{/if}}'));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use App\Services\Email\EmailTemplateService;
|
||||
use App\Support\EmailScenarioRegistry;
|
||||
use App\Support\MailPlaceholders;
|
||||
use Database\Seeders\EmailTemplates\TemplateVariables;
|
||||
use Database\Seeders\EmailTemplates\ServiceLifecycleTemplates;
|
||||
use Database\Seeders\TemplateSeeder;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
@@ -39,6 +40,22 @@ class TemplateSeederTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_service_lifecycle_templates_are_seeded_once_as_marketing(): void
|
||||
{
|
||||
$items = ServiceLifecycleTemplates::all();
|
||||
$this->assertCount(27, $items);
|
||||
|
||||
$this->seed(TemplateSeeder::class);
|
||||
$this->seed(TemplateSeeder::class);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$this->assertSame(1, Template::where('name', $item['name'])->count(), $item['name']);
|
||||
$template = Template::where('name', $item['name'])->first();
|
||||
$this->assertTrue($template->isMarketing(), $item['name']);
|
||||
$this->assertStringContainsString('class="Footer"', $template->message, $item['name']);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_every_placeholder_has_a_variable_definition(): void
|
||||
{
|
||||
foreach ([...TemplateSeeder::templates(), ...TemplateSeeder::legacyTemplates()] as $item) {
|
||||
|
||||
@@ -61,4 +61,23 @@ class AdminProductResourceTest extends TestCase
|
||||
$this->assertSame('onetime', $data['billing_type']);
|
||||
$this->assertSame('15000000.00', $data['price_onetime']);
|
||||
}
|
||||
|
||||
public function test_custom_prices_serialize_as_json_object_even_when_empty(): void
|
||||
{
|
||||
$product = new Product(['name' => 'No custom cycles', 'is_active' => true, 'setup_fee' => 0]);
|
||||
$product->id = 6;
|
||||
$product->setRelation('pricings', new Collection([
|
||||
new ProductPricings(['billing_cycle' => 'annually', 'price' => 1000000, 'currency' => 'VND']),
|
||||
new ProductPricings(['billing_cycle' => 'months_60', 'price' => 5000000, 'currency' => 'VND']),
|
||||
]));
|
||||
|
||||
$withCustom = json_decode((new ProductResource($product))->toResponse(request())->getContent());
|
||||
$this->assertIsObject($withCustom->data->custom_prices ?? $withCustom->custom_prices);
|
||||
|
||||
$product->setRelation('pricings', new Collection([
|
||||
new ProductPricings(['billing_cycle' => 'annually', 'price' => 1000000, 'currency' => 'VND']),
|
||||
]));
|
||||
$empty = json_decode((new ProductResource($product))->toResponse(request())->getContent());
|
||||
$this->assertIsObject($empty->data->custom_prices ?? $empty->custom_prices);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user