- 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.
575 lines
18 KiB
PHP
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)
|
|
);
|
|
});
|