dstpabuaran.com/tests/Feature/Admin/Manage/InvoiceTest.php
Yoga Pangestu 2a0b99c599 feat: implement invoice management features including index, create, update, delete, and print functionalities
- Added InvoiceIndex component for displaying a list of invoices with pagination and search capabilities.
- Created InvoiceShareButton component for sharing invoice details via WhatsApp.
- Developed InvoicePrint component for printing invoice details.
- Defined routes for invoice management including share and print functionalities.
- Implemented InvoiceTest to cover authentication, authorization, and CRUD operations for invoices.
2026-08-22 15:13:28 +07:00

575 lines
18 KiB
PHP

<?php
use App\Models\Invoice;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
uses(RefreshDatabase::class);
function giveInvoicePermissions(): User
{
$seeder = new RolePermissionSeeder;
$seeder->run();
$user = User::factory()->create();
$user->givePermissionTo([
'invoices.view',
'invoices.create',
'invoices.update',
'invoices.delete',
]);
return $user;
}
/*
|--------------------------------------------------------------------------
| AUTHENTICATION & AUTHORIZATION
|--------------------------------------------------------------------------
*/
test('guests are redirected to the login page', function () {
$response = $this->get(route('admin.manage.invoices.index'));
$response->assertRedirect(route('login'));
});
test('authenticated users can visit the invoice index page', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->get(route('admin.manage.invoices.index'));
$response->assertOk();
});
test('authenticated users can visit the invoice create page', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->get(route('admin.manage.invoices.create'));
$response->assertOk();
});
test('authenticated users can visit the invoice edit page', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$invoice = Invoice::factory()->create();
$response = $this->get(route('admin.manage.invoices.edit', $invoice));
$response->assertOk();
});
test('authenticated users can visit the invoice print page', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$invoice = Invoice::factory()->create();
$invoice->items()->create(['name' => 'Item', 'quantity' => 2, 'price' => 25000, 'subtotal' => 50000]);
$response = $this->get(route('admin.manage.invoices.print', $invoice));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/manage/invoice/print')
->where('invoice.invoice_number', $invoice->invoice_number)
->has('invoice.items', 1)
->has('invoice.company')
);
});
test('anyone can visit the public invoice share page without logging in', function () {
$invoice = Invoice::factory()->create();
$invoice->items()->create(['name' => 'Item', 'quantity' => 1, 'price' => 25000, 'subtotal' => 25000]);
$response = $this->get(route('admin.manage.invoices.share', $invoice));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/manage/invoice/print')
->where('invoice.invoice_number', $invoice->invoice_number)
);
});
test('users without invoices.view permission cannot visit the index page', function () {
(new RolePermissionSeeder)->run();
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.manage.invoices.index'));
$response->assertForbidden();
});
test('developer role has invoices permissions by default', function () {
(new RolePermissionSeeder)->run();
$user = User::factory()->create();
$user->assignRole('developer');
expect($user->can('invoices.view'))->toBeTrue();
expect($user->can('invoices.create'))->toBeTrue();
expect($user->can('invoices.update'))->toBeTrue();
expect($user->can('invoices.delete'))->toBeTrue();
});
test('owner role has invoices permissions by default', function () {
(new RolePermissionSeeder)->run();
$user = User::factory()->create();
$user->assignRole('owner');
expect($user->can('invoices.view'))->toBeTrue();
expect($user->can('invoices.create'))->toBeTrue();
expect($user->can('invoices.update'))->toBeTrue();
expect($user->can('invoices.delete'))->toBeTrue();
});
test('admin-toko role does not have invoices permissions', function () {
(new RolePermissionSeeder)->run();
$user = User::factory()->create();
$user->assignRole('admin-toko');
expect($user->can('invoices.view'))->toBeFalse();
});
/*
|--------------------------------------------------------------------------
| INDEX PAGE
|--------------------------------------------------------------------------
*/
test('invoice index page displays invoices', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
Invoice::factory()->count(3)->create();
$response = $this->get(route('admin.manage.invoices.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/manage/invoice/index')
->has('invoices.data', 3)
->where('invoices.total', 3)
->where('invoices.current_page', 1)
->where('invoices.per_page', 25)
);
});
test('index page works with zero invoices', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->get(route('admin.manage.invoices.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/manage/invoice/index')
->has('invoices.data', 0)
->where('invoices.total', 0)
);
});
test('index page does not display soft-deleted invoices', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
Invoice::factory()->create(['invoice_number' => 'INV-000001']);
Invoice::factory()->create(['invoice_number' => 'INV-000002'])->delete();
$response = $this->get(route('admin.manage.invoices.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/manage/invoice/index')
->has('invoices.data', 1)
->where('invoices.data.0.invoice_number', 'INV-000001')
);
});
test('index page can search by invoice number', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
Invoice::factory()->create(['invoice_number' => 'INV-ALPHA']);
Invoice::factory()->create(['invoice_number' => 'INV-BETA']);
$response = $this->get(route('admin.manage.invoices.index', ['search' => 'ALPHA']));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/manage/invoice/index')
->has('invoices.data', 1)
->where('invoices.data.0.invoice_number', 'INV-ALPHA')
);
});
test('index page can search by customer name', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
Invoice::factory()->create(['customer_name' => 'Budi Santoso']);
Invoice::factory()->create(['customer_name' => 'Siti Aminah']);
$response = $this->get(route('admin.manage.invoices.index', ['search' => 'Budi']));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/manage/invoice/index')
->has('invoices.data', 1)
->where('invoices.data.0.customer_name', 'Budi Santoso')
);
});
/*
|--------------------------------------------------------------------------
| CREATE / STORE
|--------------------------------------------------------------------------
*/
test('invoice can be created with items', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000123',
'customer_name' => 'Budi Santoso',
'customer_phone' => '081234567890',
'customer_address' => 'Jl. Merdeka No. 1',
'date' => '2026-08-22',
'due_date' => '2026-09-05',
'status' => 'unpaid',
'description' => 'Pembayaran jasa cutting',
'items' => [
['name' => 'Jasa Cutting', 'quantity' => 2, 'price' => 50000],
['name' => 'Jasa Jahit', 'quantity' => 1, 'price' => 50000],
],
]);
$response
->assertSessionHasNoErrors()
->assertRedirect();
$this->assertDatabaseHas('invoices', [
'invoice_number' => 'INV-000123',
'customer_name' => 'Budi Santoso',
'customer_phone' => '081234567890',
'customer_address' => 'Jl. Merdeka No. 1',
'amount' => 150000,
'status' => 'unpaid',
]);
$invoice = Invoice::where('invoice_number', 'INV-000123')->first();
$this->assertDatabaseHas('invoice_items', [
'invoice_id' => $invoice->id,
'name' => 'Jasa Cutting',
'quantity' => 2,
'price' => 50000,
'subtotal' => 100000,
]);
$this->assertDatabaseHas('invoice_items', [
'invoice_id' => $invoice->id,
'name' => 'Jasa Jahit',
'quantity' => 1,
'price' => 50000,
'subtotal' => 50000,
]);
expect($invoice->items()->count())->toBe(2);
});
test('invoice can be created without optional fields', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000124',
'customer_name' => 'Minimal Customer',
'date' => '2026-08-22',
'status' => 'unpaid',
'items' => [
['name' => 'Item Tunggal', 'quantity' => 1, 'price' => 50000],
],
]);
$response
->assertSessionHasNoErrors()
->assertRedirect();
$this->assertDatabaseHas('invoices', [
'invoice_number' => 'INV-000124',
'amount' => 50000,
'due_date' => null,
'description' => null,
]);
});
test('invoice_number is required', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => '',
'customer_name' => 'Test',
'date' => '2026-08-22',
'status' => 'unpaid',
'items' => [
['name' => 'Item', 'quantity' => 1, 'price' => 1000],
],
]);
$response->assertSessionHasErrors('invoice_number');
});
test('invoice_number must be unique', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
Invoice::factory()->create(['invoice_number' => 'INV-DUPLICATE']);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-DUPLICATE',
'customer_name' => 'Test',
'date' => '2026-08-22',
'status' => 'unpaid',
'items' => [
['name' => 'Item', 'quantity' => 1, 'price' => 1000],
],
]);
$response->assertSessionHasErrors('invoice_number');
});
test('customer_name is required', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000125',
'customer_name' => '',
'date' => '2026-08-22',
'status' => 'unpaid',
'items' => [
['name' => 'Item', 'quantity' => 1, 'price' => 1000],
],
]);
$response->assertSessionHasErrors('customer_name');
});
test('items is required', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000126',
'customer_name' => 'Test',
'date' => '2026-08-22',
'status' => 'unpaid',
'items' => [],
]);
$response->assertSessionHasErrors('items');
});
test('item name is required', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000127',
'customer_name' => 'Test',
'date' => '2026-08-22',
'status' => 'unpaid',
'items' => [
['name' => '', 'quantity' => 1, 'price' => 1000],
],
]);
$response->assertSessionHasErrors('items.0.name');
});
test('item quantity must be at least 1', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000128',
'customer_name' => 'Test',
'date' => '2026-08-22',
'status' => 'unpaid',
'items' => [
['name' => 'Item', 'quantity' => 0, 'price' => 1000],
],
]);
$response->assertSessionHasErrors('items.0.quantity');
});
test('item price must be an integer', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000129',
'customer_name' => 'Test',
'date' => '2026-08-22',
'status' => 'unpaid',
'items' => [
['name' => 'Item', 'quantity' => 1, 'price' => 'not-a-number'],
],
]);
$response->assertSessionHasErrors('items.0.price');
});
test('date is required', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000130',
'customer_name' => 'Test',
'status' => 'unpaid',
'items' => [
['name' => 'Item', 'quantity' => 1, 'price' => 1000],
],
]);
$response->assertSessionHasErrors('date');
});
test('due_date must be on or after date', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000131',
'customer_name' => 'Test',
'date' => '2026-08-22',
'due_date' => '2026-08-01',
'status' => 'unpaid',
'items' => [
['name' => 'Item', 'quantity' => 1, 'price' => 1000],
],
]);
$response->assertSessionHasErrors('due_date');
});
test('status must be a valid enum value', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$response = $this->post(route('admin.manage.invoices.store'), [
'invoice_number' => 'INV-000132',
'customer_name' => 'Test',
'date' => '2026-08-22',
'status' => 'cancelled',
'items' => [
['name' => 'Item', 'quantity' => 1, 'price' => 1000],
],
]);
$response->assertSessionHasErrors('status');
});
/*
|--------------------------------------------------------------------------
| UPDATE
|--------------------------------------------------------------------------
*/
test('invoice can be updated with new items', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$invoice = Invoice::factory()->create([
'invoice_number' => 'INV-000200',
'status' => 'unpaid',
]);
$invoice->items()->create(['name' => 'Old Item', 'quantity' => 1, 'price' => 10000, 'subtotal' => 10000]);
$response = $this->put(route('admin.manage.invoices.update', $invoice), [
'invoice_number' => 'INV-000200',
'customer_name' => 'Updated Customer',
'date' => '2026-08-22',
'status' => 'paid',
'items' => [
['name' => 'New Item', 'quantity' => 2, 'price' => 100000],
],
]);
$response
->assertSessionHasNoErrors()
->assertRedirect();
$this->assertDatabaseHas('invoices', [
'id' => $invoice->id,
'customer_name' => 'Updated Customer',
'amount' => 200000,
'status' => 'paid',
]);
$this->assertDatabaseMissing('invoice_items', ['name' => 'Old Item']);
$this->assertDatabaseHas('invoice_items', [
'invoice_id' => $invoice->id,
'name' => 'New Item',
'quantity' => 2,
'price' => 100000,
'subtotal' => 200000,
]);
});
test('invoice_number unique rule ignores current invoice on update', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$invoice = Invoice::factory()->create(['invoice_number' => 'INV-000201']);
$invoice->items()->create(['name' => 'Item', 'quantity' => 1, 'price' => 10000, 'subtotal' => 10000]);
$response = $this->put(route('admin.manage.invoices.update', $invoice), [
'invoice_number' => 'INV-000201',
'customer_name' => $invoice->customer_name,
'date' => $invoice->date->format('Y-m-d'),
'status' => $invoice->status,
'items' => [
['name' => 'Item', 'quantity' => 1, 'price' => 10000],
],
]);
$response->assertSessionHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| DELETE
|--------------------------------------------------------------------------
*/
test('invoice can be deleted', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
$invoice = Invoice::factory()->create();
$invoice->items()->create(['name' => 'Item', 'quantity' => 1, 'price' => 10000, 'subtotal' => 10000]);
$response = $this->delete(route('admin.manage.invoices.destroy', $invoice));
$response
->assertSessionHasNoErrors()
->assertRedirect();
$this->assertSoftDeleted('invoices', ['id' => $invoice->id]);
$this->assertDatabaseMissing('invoice_items', ['invoice_id' => $invoice->id]);
});
test('index page displays correct count after delete', function () {
$user = giveInvoicePermissions();
$this->actingAs($user);
Invoice::factory()->count(5)->create();
$invoice = Invoice::first();
$this->delete(route('admin.manage.invoices.destroy', $invoice));
$response = $this->get(route('admin.manage.invoices.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/manage/invoice/index')
->has('invoices.data', 4)
->where('invoices.total', 4)
);
});