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,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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user