feat(admin): IP pool allocation, billing exports, service attachments
- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke with notification email (ip_allocated/ip_revoked), ip_pool_logs history. - Orders/invoices: Excel export on branded template (BrandedReportWriter), shared billing filters, order item service info in list responses. - Web design quote Excel export; activity log export. - Service attachments with copy-to-invoice; deployment invoice attachments. - Customer email attachments stored on disk and attached by path. - Admin password change endpoint; user.fullname backfill migration; orders.user_id backfill from logs. - Disable deploy workflow until CI/CD is set up. - Ignore stored customer email and service attachment uploads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ea5697cb81
commit
46aa138eec
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\ActivityLog;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ActivityLogManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $admin;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->assignRole('Full Administrator');
|
||||
}
|
||||
|
||||
public function test_export_uses_branded_template_and_selected_filters(): void
|
||||
{
|
||||
$this->createLog(['log_name' => 'orders', 'description' => 'Tạo đơn hàng']);
|
||||
$this->createLog(['log_name' => 'customers', 'description' => 'Cập nhật khách hàng']);
|
||||
|
||||
$response = $this->actingAs($this->admin, 'admin')
|
||||
->get('/api/v1/administrator/activity-logs/export?log_name=orders')
|
||||
->assertOk();
|
||||
|
||||
$sheet = IOFactory::load($response->baseResponse->getFile()->getPathname())->getActiveSheet();
|
||||
|
||||
$this->assertStringStartsWith('CÔNG TY', $sheet->getCell('C1')->getValue());
|
||||
$this->assertCount(1, $sheet->getDrawingCollection());
|
||||
$this->assertSame('LỊCH SỬ HOẠT ĐỘNG', $sheet->getCell('A6')->getValue());
|
||||
$this->assertSame('ID', $sheet->getCell('A7')->getValue());
|
||||
$this->assertSame('orders', $sheet->getCell('B8')->getValue());
|
||||
$this->assertSame(8, $sheet->getHighestDataRow());
|
||||
}
|
||||
|
||||
public function test_delete_by_date_range_keeps_logs_outside_range(): void
|
||||
{
|
||||
$selected = $this->createLog(['log_name' => 'orders']);
|
||||
$remaining = $this->createLog(['log_name' => 'customers']);
|
||||
$remaining->forceFill(['created_at' => now()->subDays(10)])->saveQuietly();
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->deleteJson('/api/v1/administrator/activity-logs', [
|
||||
'mode' => 'date_range',
|
||||
'date_from' => now()->toDateString(),
|
||||
'date_to' => now()->toDateString(),
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$this->assertDatabaseMissing('activity_logs', ['id' => $selected->id]);
|
||||
$this->assertDatabaseHas('activity_logs', ['id' => $remaining->id]);
|
||||
}
|
||||
|
||||
public function test_delete_all_removes_every_log(): void
|
||||
{
|
||||
$this->createLog();
|
||||
$this->createLog();
|
||||
$total = ActivityLog::query()->count();
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->deleteJson('/api/v1/administrator/activity-logs', ['mode' => 'all'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.deleted', $total);
|
||||
|
||||
$this->assertDatabaseCount('activity_logs', 0);
|
||||
}
|
||||
|
||||
public function test_delete_date_range_requires_valid_dates(): void
|
||||
{
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->deleteJson('/api/v1/administrator/activity-logs', [
|
||||
'mode' => 'date_range',
|
||||
'date_from' => '2026-10-15',
|
||||
'date_to' => '2026-10-14',
|
||||
])
|
||||
->assertUnprocessable();
|
||||
}
|
||||
|
||||
private function createLog(array $attributes = []): ActivityLog
|
||||
{
|
||||
return ActivityLog::query()->create(array_merge([
|
||||
'log_name' => 'system',
|
||||
'event' => 'updated',
|
||||
'description' => 'Thay đổi dữ liệu',
|
||||
'causer_type' => 'admin',
|
||||
'causer_id' => $this->admin->id,
|
||||
'causer_name' => $this->admin->name,
|
||||
'ip_address' => '127.0.0.1',
|
||||
], $attributes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\Billing;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class OrderInvoiceExportTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $admin;
|
||||
|
||||
private Customer $customer;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->assignRole('Full Administrator');
|
||||
|
||||
$customer = $this->customer = Customer::factory()->create();
|
||||
$product = Product::factory()->create(['product_type' => 'domain']);
|
||||
|
||||
foreach (['first.vn', 'second.vn'] as $serverName) {
|
||||
$this->actingAs($this->admin, 'admin')->postJson(
|
||||
"/api/v1/administrator/customers/{$customer->uuid}/services",
|
||||
['product_id' => $product->id, 'payment_method' => 'transfer', 'server_name' => $serverName, 'amount_paid' => 0],
|
||||
)->assertCreated();
|
||||
}
|
||||
}
|
||||
|
||||
public static function exports(): array
|
||||
{
|
||||
return [
|
||||
'orders' => ['/api/v1/administrator/orders/export', 'Mã đơn hàng', 'DANH SÁCH ĐƠN HÀNG'],
|
||||
'invoices' => ['/api/v1/administrator/invoices/export', 'Mã hoá đơn', 'DANH SÁCH HOÁ ĐƠN'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('exports')]
|
||||
public function test_exports_every_row_into_branded_template(string $url, string $firstHeading, string $title): void
|
||||
{
|
||||
$response = $this->actingAs($this->admin, 'admin')->get("{$url}?per_page=1&page=1")->assertOk();
|
||||
|
||||
$sheet = IOFactory::load($response->baseResponse->getFile()->getPathname())->getActiveSheet();
|
||||
|
||||
$this->assertStringStartsWith('CÔNG TY', $sheet->getCell('C1')->getValue());
|
||||
$this->assertCount(1, $sheet->getDrawingCollection());
|
||||
$this->assertSame($title, $sheet->getCell('A6')->getValue());
|
||||
$this->assertSame($firstHeading, $sheet->getCell('A7')->getValue());
|
||||
$this->assertSame('A8', $sheet->getFreezePane());
|
||||
$this->assertSame(9, $sheet->getHighestDataRow());
|
||||
|
||||
$services = collect($sheet->toArray())->flatten()->implode(' ');
|
||||
$this->assertStringContainsString('first.vn', $services);
|
||||
$this->assertStringContainsString('second.vn', $services);
|
||||
}
|
||||
|
||||
#[DataProvider('exports')]
|
||||
public function test_export_applies_selected_filters(string $url): void
|
||||
{
|
||||
$this->assertSame(9, $this->exportedLastRow("{$url}?customer_id={$this->customer->id}"));
|
||||
$this->assertSame(7, $this->exportedLastRow("{$url}?customer_id=" . ($this->customer->id + 1)));
|
||||
$this->assertSame(7, $this->exportedLastRow("{$url}?status[]=0&status[]=3"));
|
||||
$this->assertSame(8, $this->exportedLastRow("{$url}?filter=first"));
|
||||
$this->assertSame(7, $this->exportedLastRow("{$url}?date_from=" . now()->addDay()->toDateString()));
|
||||
}
|
||||
|
||||
public function test_invoice_list_filters_by_numeric_status(): void
|
||||
{
|
||||
$this->actingAs($this->admin, 'admin')->getJson('/api/v1/administrator/invoices?status=2')
|
||||
->assertOk()->assertJsonPath('data.total', 2);
|
||||
$this->actingAs($this->admin, 'admin')->getJson('/api/v1/administrator/invoices?status=1')
|
||||
->assertOk()->assertJsonPath('data.total', 0);
|
||||
}
|
||||
|
||||
public function test_export_rejects_unknown_filter_shapes(): void
|
||||
{
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->getJson('/api/v1/administrator/invoices/export?status=abc')
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
private function exportedLastRow(string $url): int
|
||||
{
|
||||
$response = $this->actingAs($this->admin, 'admin')->get($url)->assertOk();
|
||||
|
||||
return IOFactory::load($response->baseResponse->getFile()->getPathname())->getActiveSheet()->getHighestDataRow();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ use App\Models\Customer;
|
||||
use App\Models\Template;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -158,4 +160,36 @@ class SendTemplateEmailTest extends TestCase
|
||||
|
||||
$this->assertDatabaseMissing('mail', ['customer_id' => $this->customer->id]);
|
||||
}
|
||||
|
||||
public function test_send_attaches_uploaded_files(): void
|
||||
{
|
||||
Mail::fake();
|
||||
Storage::fake('local');
|
||||
$base = "/api/v1/administrator/customers/{$this->customer->uuid}/send-email";
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->post($base, [
|
||||
'subject' => 'Hợp đồng',
|
||||
'message' => '<p>Gửi hợp đồng</p>',
|
||||
'to' => $this->customer->email,
|
||||
'attachments' => [
|
||||
UploadedFile::fake()->create('hop-dong.pdf', 100, 'application/pdf'),
|
||||
UploadedFile::fake()->create('phu-luc.docx', 50),
|
||||
],
|
||||
], ['Accept' => 'application/json'])
|
||||
->assertOk();
|
||||
|
||||
Mail::assertQueued(ManualCustomerEmail::class, function (ManualCustomerEmail $mail) {
|
||||
$names = array_map(fn ($attachment) => $attachment->as, $mail->attachments());
|
||||
|
||||
return $names === ['hop-dong.pdf', 'phu-luc.docx'];
|
||||
});
|
||||
$this->assertCount(2, Storage::disk('local')->allFiles("customer-emails/{$this->customer->id}"));
|
||||
|
||||
$this->post($base, [
|
||||
'subject' => 'Hợp đồng',
|
||||
'message' => '<p>Gửi hợp đồng</p>',
|
||||
'attachments' => [UploadedFile::fake()->create('script.php', 1)],
|
||||
], ['Accept' => 'application/json'])->assertStatus(422);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\IpPool;
|
||||
|
||||
use App\Mail\IpAllocationMail;
|
||||
use App\Models\Customer;
|
||||
use App\Models\IpPool;
|
||||
use App\Models\IpPoolLog;
|
||||
use App\Models\Mail as MailLog;
|
||||
use App\Models\ModuleIpAddress;
|
||||
use App\Models\ModuleNetwork;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IpAllocationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const BASE = '/api/v1/administrator/ip-pool';
|
||||
|
||||
private User $admin;
|
||||
private Customer $customer;
|
||||
private ModuleNetwork $network;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->seed(\Database\Seeders\TemplateSeeder::class);
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$this->admin = User::factory()->create(['username' => 'kythuat']);
|
||||
$this->admin->assignRole('Full Administrator');
|
||||
$this->customer = Customer::factory()->create(['fullname' => 'Công ty A', 'email' => 'a@example.com']);
|
||||
$this->network = ModuleNetwork::create(['name' => 'VM Network 56']);
|
||||
}
|
||||
|
||||
private function pool(string $ip, array $attributes = []): IpPool
|
||||
{
|
||||
return IpPool::create([
|
||||
'ip_address' => $ip, 'module_network_id' => $this->network->id, 'cidr' => 22,
|
||||
'gateway' => '10.0.0.1', 'status' => 'free', ...$attributes,
|
||||
]);
|
||||
}
|
||||
|
||||
private function allocation(array $ids): array
|
||||
{
|
||||
return [
|
||||
'ids' => $ids, 'customer_uuid' => $this->customer->uuid, 'to' => 'a@example.com',
|
||||
'cc' => 'kythuat@tgs.com.vn', 'date' => '2026-10-15', 'service_name' => 'Colo <b>1U</b>',
|
||||
];
|
||||
}
|
||||
|
||||
public function test_preview_lists_ips_without_changing_them(): void
|
||||
{
|
||||
$pool = $this->pool('10.0.0.5');
|
||||
|
||||
$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');
|
||||
|
||||
$this->assertStringContainsString('10.0.0.5', $response->json('data.html'));
|
||||
$this->assertStringContainsString('Colo <b>1U</b>', $response->json('data.html'));
|
||||
$this->assertSame('free', $pool->fresh()->status);
|
||||
}
|
||||
|
||||
public function test_allocate_assigns_ips_logs_and_emails_customer(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$first = $this->pool('10.0.0.5');
|
||||
$second = $this->pool('10.0.0.6');
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/allocate', $this->allocation([$first->id, $second->id]))
|
||||
->assertOk();
|
||||
|
||||
$first->refresh();
|
||||
$this->assertSame('assigned', $first->status);
|
||||
$this->assertSame($this->customer->id, $first->customer_id);
|
||||
$this->assertSame('Công ty A', $first->customer_name);
|
||||
$this->assertSame('2026-10-15', $first->allocated_at->toDateString());
|
||||
$this->assertSame('ALLOCATED_SENT', $first->last_email_type);
|
||||
$this->assertSame('kythuat', $first->updated_by);
|
||||
|
||||
$log = IpPoolLog::where('ip_pool_id', $first->id)->sole();
|
||||
$this->assertSame([IpPoolLog::ALLOCATED, 'free', 'assigned', $this->admin->id], [$log->action, $log->status_from, $log->status_to, $log->user_id]);
|
||||
$this->assertSame(['2026-10-15', 'Colo <b>1U</b>'], [$log->effective_date->toDateString(), $log->service_name]);
|
||||
|
||||
Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => $mail->type === IpAllocationMail::ALLOCATED
|
||||
&& count($mail->ips) === 2
|
||||
&& $mail->hasTo('a@example.com')
|
||||
&& $mail->hasCc('kythuat@tgs.com.vn'));
|
||||
$this->assertSame(1, MailLog::where('customer_id', $this->customer->id)->count());
|
||||
}
|
||||
|
||||
public function test_each_ip_can_have_its_own_service(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$first = $this->pool('10.0.0.5');
|
||||
$second = $this->pool('10.0.0.6');
|
||||
$payload = [...$this->allocation([$first->id, $second->id]), 'service_name' => 'Dịch vụ IP', 'services' => [$second->id => 'CLOUD VPS SILVER']];
|
||||
|
||||
$html = $this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/allocation/preview', $payload)
|
||||
->assertOk()
|
||||
->json('data.html');
|
||||
$this->assertStringContainsString('CLOUD VPS SILVER', $html);
|
||||
$this->assertStringContainsString('Dịch vụ IP', $html);
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->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');
|
||||
$this->assertSame('Dịch vụ IP', $first->fresh()->service_name);
|
||||
$this->assertSame('CLOUD VPS SILVER', $second->fresh()->service_name);
|
||||
}
|
||||
|
||||
public function test_admin_can_override_email_subject(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$pool = $this->pool('10.0.0.5');
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/allocate', [...$this->allocation([$pool->id]), 'subject' => 'Bàn giao IP cho Công ty A'])
|
||||
->assertOk();
|
||||
|
||||
Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => $mail->envelope()->subject === 'Bàn giao IP cho Công ty A');
|
||||
$this->assertSame('Bàn giao IP cho Công ty A', MailLog::where('customer_id', $this->customer->id)->value('subject'));
|
||||
}
|
||||
|
||||
public function test_allocation_email_carries_uploaded_attachments(): void
|
||||
{
|
||||
Mail::fake();
|
||||
Storage::fake('local');
|
||||
$pool = $this->pool('10.0.0.5');
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->post(self::BASE . '/allocate', [
|
||||
...$this->allocation([$pool->id]),
|
||||
'attachments' => [UploadedFile::fake()->createWithContent('bien-ban.pdf', "%PDF-1.4\n\xff\xfe\x00\x9c binary")],
|
||||
], ['Accept' => 'application/json'])
|
||||
->assertOk();
|
||||
|
||||
Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => count($mail->storedFiles) === 1
|
||||
&& $mail->storedFiles[0]['name'] === 'bien-ban.pdf'
|
||||
&& Storage::disk('local')->exists($mail->storedFiles[0]['path'])
|
||||
&& count($mail->attachments()) === 1
|
||||
&& json_encode(serialize($mail)) !== false);
|
||||
}
|
||||
|
||||
public function test_failed_email_dispatch_rolls_back_the_allocation(): void
|
||||
{
|
||||
$pool = $this->pool('10.0.0.5');
|
||||
Mail::shouldReceive('to')->andThrow(new \RuntimeException('queue down'));
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/allocate', $this->allocation([$pool->id]))
|
||||
->assertStatus(500);
|
||||
|
||||
$this->assertSame('free', $pool->fresh()->status);
|
||||
$this->assertNull($pool->fresh()->customer_id);
|
||||
$this->assertSame(0, IpPoolLog::count());
|
||||
$this->assertSame(0, MailLog::count());
|
||||
}
|
||||
|
||||
public function test_allocate_rejects_ips_that_are_not_free(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$free = $this->pool('10.0.0.5');
|
||||
$taken = $this->pool('10.0.0.6', ['status' => 'assigned']);
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/allocate', $this->allocation([$free->id, $taken->id]))
|
||||
->assertStatus(422)
|
||||
->assertJsonFragment(['ids' => ['Chỉ cấp phát được IP đang trống. IP không hợp lệ: 10.0.0.6']]);
|
||||
|
||||
$this->assertSame('free', $free->fresh()->status);
|
||||
Mail::assertNothingQueued();
|
||||
}
|
||||
|
||||
public function test_revoke_groups_by_customer_and_frees_ips(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$other = Customer::factory()->create(['fullname' => 'Công ty B']);
|
||||
$a = $this->pool('10.0.0.5', ['status' => 'assigned', 'customer_id' => $this->customer->id, 'customer_name' => 'Công ty A', 'service_name' => 'CLOUD VPS SILVER', 'contact_email' => 'a@example.com']);
|
||||
$b = $this->pool('10.0.0.6', ['status' => 'assigned', 'customer_id' => $other->id, 'customer_name' => 'Công ty B']);
|
||||
|
||||
$groups = $this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/revocation/preview', ['ids' => [$a->id, $b->id], 'date' => '2026-10-31'])
|
||||
->assertOk()
|
||||
->assertJsonCount(2, 'data')
|
||||
->json('data');
|
||||
|
||||
$missingEmail = collect($groups)->firstWhere('customer_name', 'Công ty B');
|
||||
$this->assertNull($missingEmail['to']);
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/revoke', [
|
||||
'ids' => [$a->id, $b->id], 'date' => '2026-10-31',
|
||||
'recipients' => collect($groups)->map(fn ($g) => ['key' => $g['key'], 'to' => $g['to']])->all(),
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/revoke', [
|
||||
'ids' => [$a->id, $b->id], 'date' => '2026-10-31',
|
||||
'recipients' => collect($groups)->map(fn ($g) => ['key' => $g['key'], 'to' => $g['to'] ?? 'b@example.com'])->all(),
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$a->refresh();
|
||||
$this->assertSame('free', $a->status);
|
||||
$this->assertNull($a->customer_id);
|
||||
$this->assertSame('2026-10-31', $a->revoked_at->toDateString());
|
||||
$log = IpPoolLog::where('ip_pool_id', $a->id)->sole();
|
||||
$this->assertSame([IpPoolLog::REVOKED, 'Công ty A', $this->customer->id], [$log->action, $log->customer_name, $log->customer_id]);
|
||||
$this->assertSame(['2026-10-31', 'CLOUD VPS SILVER'], [$log->effective_date->toDateString(), $log->service_name]);
|
||||
$this->assertNull($a->service_name);
|
||||
Mail::assertQueuedCount(2);
|
||||
}
|
||||
|
||||
public function test_revoke_refuses_ips_used_by_a_vm(): void
|
||||
{
|
||||
$pool = $this->pool('10.0.0.7', ['status' => 'assigned']);
|
||||
ModuleIpAddress::create(['ip' => '10.0.0.7', 'module_network_id' => $this->network->id, 'status' => 1]);
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->postJson(self::BASE . '/revocation/preview', ['ids' => [$pool->id], 'date' => '2026-10-31'])
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
public function test_logs_endpoint_returns_history(): void
|
||||
{
|
||||
$pool = $this->pool('10.0.0.8');
|
||||
IpPoolLog::record($pool, IpPoolLog::ALLOCATED, 'free', ['customer_name' => 'Công ty A']);
|
||||
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->getJson(self::BASE . "/{$pool->id}/logs")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.action', IpPoolLog::ALLOCATED)
|
||||
->assertJsonPath('data.0.customer_name', 'Công ty A');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\IpPool;
|
||||
|
||||
use App\Models\IpPool;
|
||||
use App\Models\ModuleIpAddress;
|
||||
use App\Models\ModuleNetwork;
|
||||
use App\Models\User;
|
||||
use App\Services\IpPool\IpPoolSheetImporter;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IpPoolSheetImportTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const HEADER = [
|
||||
'TEN_MANG', 'CIDR', 'OWNER_TYPE', 'IP_ADDRESS', 'IP_ROLE', 'PARENT_IP', 'SUBNET_MASK', 'GATEWAY',
|
||||
'TEN_KHACH_HANG', 'TEN_DICH_VU', 'NGAY_CAP_PHAT', 'NGAY_THU_HOI', 'TRANG_THAI', 'CONTACT_EMAIL',
|
||||
'CONTACT_CC', 'CONTACT_PHONE', 'MONTHLY_COST', 'SELLING_PRICE', 'THONG_TIN_BO_SUNG',
|
||||
'LAST_EMAIL_TYPE', 'LAST_EMAIL_AT', 'UPDATED_AT', 'UPDATED_BY', 'GHI_CHU', 'NOTE',
|
||||
];
|
||||
|
||||
private function admin(): User
|
||||
{
|
||||
$role = Role::create(['name' => 'role-' . uniqid(), 'guard_name' => 'admin']);
|
||||
$role->givePermissionTo(Permission::firstOrCreate(['name' => 'module-vcenter.manager', 'guard_name' => 'admin']));
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole($role);
|
||||
|
||||
return $admin;
|
||||
}
|
||||
|
||||
private function row(array $values): array
|
||||
{
|
||||
return array_map(fn ($header) => $values[$header] ?? null, self::HEADER);
|
||||
}
|
||||
|
||||
private function xlsx(array $rows): UploadedFile
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$spreadsheet->getActiveSheet()->setTitle('CONFIG');
|
||||
$sheet = $spreadsheet->createSheet()->setTitle('IP_POOL');
|
||||
$sheet->fromArray([self::HEADER, ...$rows]);
|
||||
$sheet->setCellValue('Y2', '=XLOOKUP(D2,CONFIG!A:A,CONFIG!B:B)');
|
||||
|
||||
$path = tempnam(sys_get_temp_dir(), 'ip') . '.xlsx';
|
||||
(new Xlsx($spreadsheet))->setPreCalculateFormulas(false)->save($path);
|
||||
|
||||
return new UploadedFile($path, 'ip.xlsx', null, null, true);
|
||||
}
|
||||
|
||||
public function test_status_follows_sheet_unless_the_system_owns_the_ip(): void
|
||||
{
|
||||
$this->assertSame('assigned', IpPoolSheetImporter::resolveStatus(null, false, 'ALLOCATED'));
|
||||
$this->assertSame('assigned', IpPoolSheetImporter::resolveStatus('free', false, 'ALLOCATED'));
|
||||
$this->assertSame('free', IpPoolSheetImporter::resolveStatus('assigned', false, 'FREE'));
|
||||
$this->assertSame('assigned', IpPoolSheetImporter::resolveStatus('assigned', true, 'FREE'));
|
||||
$this->assertSame('reserved', IpPoolSheetImporter::resolveStatus('reserved', true, 'ALLOCATED'));
|
||||
$this->assertSame('free', IpPoolSheetImporter::resolveStatus(null, false, null));
|
||||
}
|
||||
|
||||
public function test_preview_reports_changes_without_writing_and_import_applies_them(): void
|
||||
{
|
||||
$network = ModuleNetwork::create(['name' => 'VM Network 56']);
|
||||
IpPool::create([
|
||||
'ip_address' => '10.0.0.5', 'module_network_id' => $network->id, 'cidr' => 24,
|
||||
'gateway' => '10.0.0.1', 'status' => 'free',
|
||||
]);
|
||||
IpPool::create([
|
||||
'ip_address' => '10.0.0.6', 'module_network_id' => $network->id, 'cidr' => 24, 'status' => 'free',
|
||||
]);
|
||||
ModuleIpAddress::create(['ip' => '10.0.0.6', 'module_network_id' => $network->id, 'status' => 1]);
|
||||
|
||||
$file = $this->xlsx([
|
||||
$this->row([
|
||||
'TEN_MANG' => "THEGIOISO-VN\t\t", 'CIDR' => '/22', 'OWNER_TYPE' => 'thế giới số',
|
||||
'IP_ADDRESS' => '10.0.0.5', 'TEN_KHACH_HANG' => 'Khách A', 'TRANG_THAI' => 'ALLOCATED',
|
||||
'NGAY_CAP_PHAT' => ExcelDate::PHPToExcel(new \DateTime('2026-04-15')),
|
||||
'CONTACT_EMAIL' => "\ta@example.com; b@example.com", 'CONTACT_PHONE' => 835123959,
|
||||
'MONTHLY_COST' => 60000,
|
||||
]),
|
||||
$this->row(['TEN_MANG' => 'THEGIOISO-VN', 'IP_ADDRESS' => '10.0.0.6', 'TRANG_THAI' => 'FREE']),
|
||||
$this->row(['TEN_MANG' => 'HOITUSO-VN', 'IP_ADDRESS' => '10.0.1.9', 'TRANG_THAI' => 'ALLOCATED']),
|
||||
$this->row(['CONTACT_CC' => 'kythuat@tgs.com.vn']),
|
||||
$this->row(['TEN_MANG' => 'HOITUSO-VN', 'IP_ADDRESS' => '10.0.1.10', 'CONTACT_EMAIL' => 'not-an-email']),
|
||||
]);
|
||||
|
||||
$admin = $this->admin();
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->post('/api/v1/administrator/ip-pool/import/preview', ['file' => $file], ['Accept' => 'application/json'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.valid', 3)
|
||||
->assertJsonPath('data.skipped_blank', 1)
|
||||
->assertJsonPath('data.error_count', 1)
|
||||
->assertJsonPath('data.errors.0.row', 6)
|
||||
->assertJsonPath('data.create', 1)
|
||||
->assertJsonPath('data.update', 2)
|
||||
->assertJsonPath('data.to_assigned', 2)
|
||||
->assertJsonPath('data.kept_by_system', 1);
|
||||
|
||||
$this->assertSame(2, IpPool::count());
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->post('/api/v1/administrator/ip-pool/import', ['file' => $file], ['Accept' => 'application/json'])
|
||||
->assertOk();
|
||||
|
||||
$updated = IpPool::where('ip_address', '10.0.0.5')->sole();
|
||||
$this->assertSame($network->id, $updated->module_network_id);
|
||||
$this->assertSame('assigned', $updated->status);
|
||||
$this->assertSame('THEGIOISO-VN', $updated->ip_block);
|
||||
$this->assertSame('THẾ GIỚI SỐ', $updated->owner_type);
|
||||
$this->assertSame(22, $updated->cidr);
|
||||
$this->assertSame('10.0.0.1', $updated->gateway);
|
||||
$this->assertSame('a@example.com, b@example.com', $updated->contact_email);
|
||||
$this->assertSame('0835123959', $updated->contact_phone);
|
||||
$this->assertSame('2026-04-15', $updated->allocated_at->toDateString());
|
||||
$this->assertSame('60000.00', $updated->monthly_cost);
|
||||
|
||||
$this->assertSame('free', IpPool::where('ip_address', '10.0.0.6')->value('status'));
|
||||
|
||||
$created = IpPool::where('ip_address', '10.0.1.9')->sole();
|
||||
$this->assertNull($created->module_network_id);
|
||||
$this->assertSame('assigned', $created->status);
|
||||
$this->assertFalse(IpPool::where('ip_address', '10.0.1.10')->exists());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\IpPool;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\IpPool;
|
||||
use App\Models\ModuleIpAddress;
|
||||
use App\Models\ModuleNetwork;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
use App\Models\VirtualMachines;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IpPoolUsageTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_links_ips_to_their_vm_service_customer_and_vcenter_vm(): void
|
||||
{
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('Full Administrator');
|
||||
|
||||
$network = ModuleNetwork::create(['name' => 'VM Network 56']);
|
||||
$customer = Customer::factory()->create(['fullname' => 'Công ty A']);
|
||||
foreach (['10.0.0.5', '10.0.0.6', '10.0.0.7'] as $ip) {
|
||||
IpPool::create(['ip_address' => $ip, 'module_network_id' => $network->id, 'cidr' => 24, 'status' => 'assigned']);
|
||||
ModuleIpAddress::create(['ip' => $ip, 'module_network_id' => $network->id, 'status' => 1, 'vm_name' => "{$ip}_vm"]);
|
||||
}
|
||||
|
||||
$vmware = ModuleVmware::create(['name' => '10.0.0.5_customer-vm', 'ip' => '10.0.0.5', 'customer_id' => $customer->id]);
|
||||
$service = Service::create(['customer_id' => $customer->id, 'status' => 'active', 'server_name' => 'vps-a', 'vm_id' => $vmware->id]);
|
||||
Schema::disableForeignKeyConstraints();
|
||||
$vcenterVm = VirtualMachines::create(['uuid' => (string) Str::uuid(), 'vm_id' => 'vm-1', 'name' => '10.0.0.6_test-vm', 'datacenter_id' => 1, 'host_id' => 1]);
|
||||
|
||||
$items = collect($this->actingAs($admin, 'admin')
|
||||
->getJson('/api/v1/administrator/ip-pool?all=1')
|
||||
->assertOk()
|
||||
->json('data.items'))
|
||||
->keyBy('ip_address');
|
||||
|
||||
$this->assertSame($service->id, $items['10.0.0.5']['usage']['service_id']);
|
||||
$this->assertSame('vps-a', $items['10.0.0.5']['usage']['service_name']);
|
||||
$this->assertSame($customer->uuid, $items['10.0.0.5']['usage']['customer_uuid']);
|
||||
$this->assertSame($vcenterVm->id, $items['10.0.0.6']['usage']['vcenter_vm_id']);
|
||||
$this->assertNull($items['10.0.0.6']['usage']['service_id']);
|
||||
$this->assertSame('10.0.0.7_vm', $items['10.0.0.7']['usage']['vm_name']);
|
||||
$this->assertNull($items['10.0.0.7']['usage']['vcenter_vm_id']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\Service;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Enums\Order\OrderStatus;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceAttachment;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ServiceAttachmentTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $admin;
|
||||
private Service $service;
|
||||
private Invoice $invoice;
|
||||
private Invoice $foreignInvoice;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('public');
|
||||
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->assignRole('Full Administrator');
|
||||
|
||||
$customer = Customer::factory()->create();
|
||||
$order = $this->makeOrder($customer);
|
||||
$item = OrderItem::create(['order_id' => $order->id, 'product_id' => null, 'billing_cycle' => 'monthly', 'months' => 1, 'quantity' => 1, 'price' => 100000]);
|
||||
$this->service = Service::create(['customer_id' => $customer->id, 'order_item_id' => $item->id, 'status' => 'active']);
|
||||
$this->invoice = $this->makeInvoice($customer, $order);
|
||||
$this->foreignInvoice = $this->makeInvoice($customer, $this->makeOrder($customer));
|
||||
}
|
||||
|
||||
public function test_upload_list_copy_to_invoice_and_delete(): void
|
||||
{
|
||||
$base = "/api/v1/administrator/services/{$this->service->id}/attachments";
|
||||
$admin = $this->actingAs($this->admin, 'admin');
|
||||
|
||||
$id = $admin->post($base, ['file' => UploadedFile::fake()->create('spec.pdf', 10, 'application/pdf')], ['Accept' => 'application/json'])
|
||||
->assertCreated()
|
||||
->json('data.id');
|
||||
|
||||
$admin->getJson($base)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.attachments.0.filename', 'spec.pdf')
|
||||
->assertJsonCount(1, 'data.invoices')
|
||||
->assertJsonPath('data.invoices.0.id', $this->invoice->id);
|
||||
|
||||
$admin->postJson("$base/$id/copy-to-invoice", ['invoice_id' => $this->foreignInvoice->id])->assertNotFound();
|
||||
|
||||
$admin->postJson("$base/$id/copy-to-invoice", ['invoice_id' => $this->invoice->id])->assertCreated();
|
||||
$admin->postJson("$base/$id/copy-to-invoice", ['invoice_id' => $this->invoice->id])->assertCreated();
|
||||
$copy = InvoiceAttachment::where('invoice_id', $this->invoice->id)->sole();
|
||||
$this->assertSame(InvoiceAttachment::TYPE_DEPLOYMENT, $copy->type);
|
||||
Storage::disk('public')->assertExists($copy->path);
|
||||
|
||||
$admin->deleteJson("$base/$id")->assertOk();
|
||||
$this->assertDatabaseCount('service_attachments', 0);
|
||||
Storage::disk('public')->assertExists($copy->path);
|
||||
}
|
||||
|
||||
public function test_list_and_detail_expose_linked_order_and_invoices(): void
|
||||
{
|
||||
$admin = $this->actingAs($this->admin, 'admin');
|
||||
|
||||
$admin->getJson('/api/v1/administrator/services')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items.0.order_item.order.order_num', $this->invoice->order->order_num)
|
||||
->assertJsonCount(1, 'data.items.0.linked_invoices')
|
||||
->assertJsonPath('data.items.0.linked_invoices.0.invoice_number', $this->invoice->invoice_number);
|
||||
|
||||
$admin->getJson("/api/v1/administrator/services/{$this->service->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.linked_invoices.0.id', $this->invoice->id);
|
||||
}
|
||||
|
||||
public function test_rejects_unsupported_file_type(): void
|
||||
{
|
||||
$this->actingAs($this->admin, 'admin')
|
||||
->post("/api/v1/administrator/services/{$this->service->id}/attachments", ['file' => UploadedFile::fake()->create('x.exe', 10)], ['Accept' => 'application/json'])
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
private function makeOrder(Customer $customer): Order
|
||||
{
|
||||
return Order::create(['order_num' => 'ORD-' . uniqid(), 'customer_id' => $customer->id, 'sub_total' => 100_000, 'total' => 100_000, 'status' => OrderStatus::PENDING->value]);
|
||||
}
|
||||
|
||||
private function makeInvoice(Customer $customer, Order $order): Invoice
|
||||
{
|
||||
return Invoice::create([
|
||||
'invoice_number' => 'INV-' . uniqid(),
|
||||
'customer_id' => $customer->id,
|
||||
'order_id' => $order->id,
|
||||
'invoice_type' => 1,
|
||||
'sub_total' => 100_000,
|
||||
'tax' => 0,
|
||||
'total' => 100_000,
|
||||
'status' => InvoiceStatus::UNPAID->value,
|
||||
'due_date' => now()->addDays(5)->toDateString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UpdatePasswordTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_admin_can_change_own_password(): void
|
||||
{
|
||||
$user = User::factory()->create(['password' => Hash::make('OldPassword1')]);
|
||||
|
||||
$this->actingAs($user, 'admin')
|
||||
->putJson('/api/v1/administrator/password', [
|
||||
'current_password' => 'OldPassword1',
|
||||
'password' => 'NewPassword2',
|
||||
'password_confirmation' => 'NewPassword2',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('error', false);
|
||||
|
||||
$this->assertTrue(Hash::check('NewPassword2', $user->fresh()->password));
|
||||
}
|
||||
|
||||
public function test_admin_must_provide_the_correct_current_password(): void
|
||||
{
|
||||
$user = User::factory()->create(['password' => Hash::make('OldPassword1')]);
|
||||
|
||||
$this->actingAs($user, 'admin')
|
||||
->putJson('/api/v1/administrator/password', [
|
||||
'current_password' => 'WrongPassword',
|
||||
'password' => 'NewPassword2',
|
||||
'password_confirmation' => 'NewPassword2',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('current_password');
|
||||
|
||||
$this->assertTrue(Hash::check('OldPassword1', $user->fresh()->password));
|
||||
}
|
||||
|
||||
public function test_new_password_must_be_confirmed_and_different(): void
|
||||
{
|
||||
$user = User::factory()->create(['password' => Hash::make('OldPassword1')]);
|
||||
|
||||
$this->actingAs($user, 'admin')
|
||||
->putJson('/api/v1/administrator/password', [
|
||||
'current_password' => 'OldPassword1',
|
||||
'password' => 'OldPassword1',
|
||||
'password_confirmation' => 'DifferentPassword2',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('password');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UpdateProfileTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_admin_can_update_fullname(): void
|
||||
{
|
||||
$user = User::factory()->create(['fullname' => 'Old Name']);
|
||||
|
||||
$this->actingAs($user, 'admin')
|
||||
->putJson('/api/v1/administrator/profile', [
|
||||
'fullname' => 'Nguyễn Văn A',
|
||||
'phone' => '0901234567',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.user.fullname', 'Nguyễn Văn A');
|
||||
|
||||
$this->assertSame('Nguyễn Văn A', $user->fresh()->fullname);
|
||||
$this->assertSame('0901234567', $user->fresh()->phone);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\WebDesign;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WebDesignQuote;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WebDesignQuoteXlsxTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_downloads_quote_into_pricing_template_with_live_totals(): void
|
||||
{
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('Full Administrator');
|
||||
|
||||
$quote = WebDesignQuote::create([
|
||||
'quote_number' => 'BG-WEB-202610-0001',
|
||||
'status' => 'draft',
|
||||
'customer_name' => 'Nguyễn Văn A',
|
||||
'website_name' => 'shop.vn',
|
||||
'website_category' => 'Thương mại điện tử',
|
||||
'platform' => 'wordpress_elementor',
|
||||
'items' => [
|
||||
['name' => 'Thiết kế UI', 'description' => '<ul><li>Trang chủ</li><li>Sản phẩm</li></ul>', 'quantity' => 1, 'unit_price' => 8000000],
|
||||
['name' => '=HYPERLINK("x")', 'description' => null, 'quantity' => 2, 'unit_price' => 1000000],
|
||||
],
|
||||
'subtotal' => 10000000,
|
||||
'discount' => 500000,
|
||||
'vat_percent' => 8,
|
||||
'total' => 10260000,
|
||||
'currency' => 'VND',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin, 'admin')
|
||||
->get("/api/v1/administrator/web-design-quotes/{$quote->id}/xlsx")
|
||||
->assertOk();
|
||||
|
||||
$book = IOFactory::load($response->baseResponse->getFile()->getPathname());
|
||||
$sheet = $book->getActiveSheet();
|
||||
$cells = collect($sheet->toArray(null, true, false, true));
|
||||
|
||||
$this->assertSame(1, $book->getSheetCount());
|
||||
$this->assertCount(1, $sheet->getDrawingCollection());
|
||||
$this->assertSame('BÁO GIÁ THIẾT KẾ WEBSITE SHOP.VN', $sheet->getCell('A6')->getValue());
|
||||
|
||||
$itemRow = $cells->search(fn (array $row) => $row['B'] === 'Thiết kế UI');
|
||||
$this->assertSame("- Trang chủ\n- Sản phẩm", $sheet->getCell("C{$itemRow}")->getValue());
|
||||
$this->assertSame('=HYPERLINK("x")', $sheet->getCell('B' . ($itemRow + 1))->getCalculatedValue());
|
||||
|
||||
$totalRow = $cells->search(fn (array $row) => $row['A'] === 'Tổng giá trị báo giá');
|
||||
$this->assertEquals($quote->total, $sheet->getCell("F{$totalRow}")->getCalculatedValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Mail;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Models\Setting;
|
||||
use App\Support\MailSettings;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MailSettingsQueueTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function saveSmtp(string $host): void
|
||||
{
|
||||
Setting::updateOrCreate(['key' => 'setting'], ['value' => json_encode(['company' => 'Thế Giới Số'])]);
|
||||
Setting::updateOrCreate(['key' => 'smtp'], ['value' => json_encode([
|
||||
'host' => $host, 'port' => 465, 'user' => 'noreply@tgs.com.vn',
|
||||
'password' => SignRSA::EncryptFunc('secret'), 'encryption' => 'ssl',
|
||||
'from_address' => 'noreply@tgs.com.vn', 'from_name' => 'TGS',
|
||||
])]);
|
||||
Cache::forget(MailSettings::cacheKey());
|
||||
}
|
||||
|
||||
private function hostSeenByQueuedJob(): ?string
|
||||
{
|
||||
dispatch(fn () => Cache::put('test:queued-mail-host', config('mail.mailers.smtp.host')));
|
||||
|
||||
return Cache::pull('test:queued-mail-host');
|
||||
}
|
||||
|
||||
public function test_queued_jobs_use_smtp_from_settings_and_pick_up_changes(): void
|
||||
{
|
||||
config(['queue.default' => 'sync']);
|
||||
|
||||
$this->saveSmtp('smtp.settings-one.test');
|
||||
$this->assertSame('smtp.settings-one.test', $this->hostSeenByQueuedJob());
|
||||
$this->assertSame('noreply@tgs.com.vn', config('mail.from.address'));
|
||||
$this->assertSame('secret', config('mail.mailers.smtp.password'));
|
||||
|
||||
$this->saveSmtp('smtp.settings-two.test');
|
||||
$this->assertSame('smtp.settings-two.test', $this->hostSeenByQueuedJob());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user