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) ); });