Files
portal-api/tests/Feature/Api/Admin/Service/ServiceAttachmentTest.php
T
baodev0andClaude Opus 5.5 46aa138eec 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>
2026-10-15 19:58:36 +07:00

118 lines
4.7 KiB
PHP

<?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(),
]);
}
}