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