run(); $user->givePermissionTo([ 'restock.view', 'restock.create', 'restock.update', 'restock.delete', ]); } function makeRestockVariant(array $overrides = []): ProductVariant { $variant = ProductVariant::factory()->create(array_merge([ 'stock' => 10, 'reject_stock' => 5, ], $overrides)); $variant->productPrices()->create([ 'type' => PriceType::CAPITAL, 'price' => 50000, ]); return $variant; } function makeValidRestockPayload(array $overrides = []): array { $variant = makeRestockVariant(); return array_merge([ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 10], ], 'notes' => 'Restock test', ], $overrides); } /* |-------------------------------------------------------------------------- | AUTHENTICATION |-------------------------------------------------------------------------- */ test('guests are redirected to the login page', function () { $response = $this->get(route('admin.manage.restocks.index')); $response->assertRedirect(route('login')); }); test('guests are redirected when visiting create page', function () { $response = $this->get(route('admin.manage.restocks.create')); $response->assertRedirect(route('login')); }); test('guests are redirected when visiting edit page', function () { $restock = Restock::factory()->create(); $response = $this->get(route('admin.manage.restocks.edit', $restock)); $response->assertRedirect(route('login')); }); test('guest cannot create restock', function () { $response = $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload()); $response->assertRedirect(route('login')); $this->assertDatabaseCount('restocks', 0); }); test('guest cannot update restock', function () { $restock = Restock::factory()->create(); $response = $this->put(route('admin.manage.restocks.update', $restock), makeValidRestockPayload()); $response->assertRedirect(route('login')); }); test('guest cannot delete restock', function () { $restock = Restock::factory()->create(); $response = $this->delete(route('admin.manage.restocks.destroy', $restock)); $response->assertRedirect(route('login')); $this->assertDatabaseHas('restocks', ['id' => $restock->id, 'deleted_at' => null]); }); test('authenticated users can visit the restock index page', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->get(route('admin.manage.restocks.index')); $response->assertOk(); }); test('authenticated users can visit the restock create page', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->get(route('admin.manage.restocks.create')); $response->assertOk(); }); test('authenticated users can visit the restock edit page', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $restock = Restock::factory()->create(); $response = $this->get(route('admin.manage.restocks.edit', $restock)); $response->assertOk(); }); /* |-------------------------------------------------------------------------- | INDEX PAGE |-------------------------------------------------------------------------- */ test('restock index page displays restocks', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); Restock::factory()->count(3)->create(); $response = $this->get(route('admin.manage.restocks.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/manage/restock/index') ->has('restocks.data', 3) ->where('restocks.total', 3) ->where('restocks.current_page', 1) ->where('restocks.per_page', 25) ); }); test('index page works with zero restocks', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->get(route('admin.manage.restocks.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/manage/restock/index') ->has('restocks.data', 0) ->where('restocks.total', 0) ); }); test('index page does not display soft-deleted restocks', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); Restock::factory()->create(['notes' => 'Active Restock']); Restock::factory()->create(['notes' => 'Deleted Restock'])->delete(); $response = $this->get(route('admin.manage.restocks.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/manage/restock/index') ->has('restocks.data', 1) ->where('restocks.total', 1) ); }); test('index page includes restock items with nested data', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload()); $response = $this->get(route('admin.manage.restocks.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('restocks.data.0.restock_items', 1) ->has('restocks.data.0.restock_items.0.product_variant') ->has('restocks.data.0.restock_items.0.product_variant.product') ); }); test('index page returns correct restock item data', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['name' => 'Hitam M']); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 10], ], 'notes' => 'Restock test', ]); $response = $this->get(route('admin.manage.restocks.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('restocks.data.0.stock_type', 'good') ->where('restocks.data.0.restock_items.0.quantity', 10) ->where('restocks.data.0.restock_items.0.unit_price', 50000) ->where('restocks.data.0.restock_items.0.subtotal', 500000) ->where('restocks.data.0.restock_items.0.product_variant.name', 'Hitam M') ->where('restocks.data.0.restock_items.0.product_variant.product.name', $variant->product->name) ); }); /* |-------------------------------------------------------------------------- | CREATE / STORE - BASIC |-------------------------------------------------------------------------- */ test('restock can be created', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload()); $response ->assertSessionHasNoErrors() ->assertRedirect(route('admin.manage.restocks.index')); $this->assertDatabaseCount('restocks', 1); $this->assertDatabaseCount('restock_items', 1); }); test('restock increases product variant stock for good quality', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['stock' => 10, 'reject_stock' => 5]); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 7], ], 'notes' => null, ]); $variant->refresh(); expect($variant->stock)->toBe(17); expect($variant->reject_stock)->toBe(5); }); test('restock increases product variant reject stock for reject quality', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['stock' => 10, 'reject_stock' => 5]); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'reject', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 7], ], 'notes' => null, ]); $variant->refresh(); expect($variant->stock)->toBe(10); expect($variant->reject_stock)->toBe(12); }); test('restock uses capital price for unit price and subtotal', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(); $variant->productPrices()->updateOrCreate( ['type' => PriceType::CAPITAL], ['price' => 60000], ); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 10], ], 'notes' => null, ]); $restock = Restock::first(); expect($restock->subtotal)->toBe(600000); expect($restock->total)->toBe(600000); $item = RestockItem::first(); expect($item->unit_price)->toBe(60000); expect($item->subtotal)->toBe(600000); }); test('restock calculates subtotal across multiple items', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variantA = makeRestockVariant(); $variantB = makeRestockVariant(); $variantB->productPrices()->updateOrCreate( ['type' => PriceType::CAPITAL], ['price' => 70000], ); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variantA->id, 'quantity' => 10], ['product_variant_id' => $variantB->id, 'quantity' => 5], ], 'notes' => null, ]); $restock = Restock::first(); expect($restock->subtotal)->toBe(850000); // (50000*10) + (70000*5) expect($restock->total)->toBe(850000); }); test('restock sets created_by_id to current user', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload()); $restock = Restock::first(); expect($restock->created_by_id)->toBe($user->id); }); test('restock can be created with notes', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'notes' => 'Restock tambahan untuk toko', ])); $restock = Restock::first(); expect($restock->notes)->toBe('Restock tambahan untuk toko'); }); test('restock can be created without notes', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'notes' => null, ])); $restock = Restock::first(); expect($restock->notes)->toBeNull(); }); test('restock creates items with correct relationships', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload()); $restock = Restock::first(); $item = RestockItem::first(); expect($item->restock_id)->toBe($restock->id); expect($item->user_id)->toBe($user->id); expect($item->product_variant_id)->not->toBeNull(); expect($item->quantity)->toBe(10); expect($item->unit_price)->toBe(50000); expect($item->subtotal)->toBe(500000); }); /* |-------------------------------------------------------------------------- | STORE VALIDATION |-------------------------------------------------------------------------- */ test('restock items are required', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'items' => [], ])); $response->assertSessionHasErrors('items'); }); test('restock item quantity must be at least 1', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'items' => [ ['product_variant_id' => ProductVariant::factory()->create()->id, 'quantity' => 0], ], ])); $response->assertSessionHasErrors('items.0.quantity'); }); test('restock item product variant must exist', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'items' => [ ['product_variant_id' => 999999, 'quantity' => 5], ], ])); $response->assertSessionHasErrors('items.0.product_variant_id'); }); test('restock stock_type must be valid', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'stock_type' => 'invalid', ])); $response->assertSessionHasErrors('stock_type'); }); test('restock notes must not exceed 100 characters', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'notes' => str_repeat('a', 101), ])); $response->assertSessionHasErrors('notes'); }); test('restock photo_key must not exceed 500 characters', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'photo_key' => str_repeat('a', 501), ])); $response->assertSessionHasErrors('photo_key'); }); test('restock photo is stored on create', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'photo_key' => 'restock/test-photo.jpg', ])); $restock = Restock::first(); expect($restock->getFirstMedia('photos')?->file_name) ->toBe('restock/test-photo.jpg'); }); test('restock photo is returned on edit page', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'photo_key' => 'restock/test-photo.jpg', ])); $restock = Restock::first(); $response = $this->get(route('admin.manage.restocks.edit', $restock)); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('restock.photo_key', 'restock/test-photo.jpg') ); }); test('restock photo is replaced on update', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'photo_key' => 'restock/old-photo.jpg', ])); $restock = Restock::first(); $this->put(route('admin.manage.restocks.update', $restock), makeValidRestockPayload([ 'photo_key' => 'restock/new-photo.jpg', ])); $media = $restock->fresh()->getFirstMedia('photos'); expect($media?->file_name)->toBe('restock/new-photo.jpg'); expect($restock->getMedia('photos'))->toHaveCount(1); }); test('restock photo is removed when photo_key is null', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'photo_key' => 'restock/test-photo.jpg', ])); $restock = Restock::first(); $this->put(route('admin.manage.restocks.update', $restock), makeValidRestockPayload([ 'photo_key' => null, ])); expect($restock->fresh()->getMedia('photos'))->toHaveCount(0); }); test('restock photo is kept when photo_key is unchanged', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'photo_key' => 'restock/same-photo.jpg', ])); $restock = Restock::first(); $this->put(route('admin.manage.restocks.update', $restock), makeValidRestockPayload([ 'photo_key' => 'restock/same-photo.jpg', ])); $media = $restock->fresh()->getFirstMedia('photos'); expect($media?->file_name)->toBe('restock/same-photo.jpg'); expect($restock->getMedia('photos'))->toHaveCount(1); }); test('restock stock quality enum has labels', function () { expect(ProductStockQuality::GOOD->label())->toBe('Bagus'); expect(ProductStockQuality::REJECT->label())->toBe('Reject'); }); /* |-------------------------------------------------------------------------- | EDIT PAGE |-------------------------------------------------------------------------- */ test('edit page shows restock data', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['name' => 'Hitam M']); $restock = Restock::factory()->create([ 'stock_type' => 'good', 'notes' => 'Test notes', ]); RestockItem::factory()->create([ 'restock_id' => $restock->id, 'product_variant_id' => $variant->id, 'quantity' => 10, 'unit_price' => 50000, 'subtotal' => 500000, ]); $response = $this->get(route('admin.manage.restocks.edit', $restock)); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/manage/restock/edit') ->where('restock.id', $restock->id) ->where('restock.stock_type', 'good') ->where('restock.notes', 'Test notes') ->has('restock.items', 1) ->where('restock.items.0.product_variant_id', $variant->id) ->where('restock.items.0.quantity', 10) ->has('data.products') ); }); test('create page returns products with capital price', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['name' => 'Hitam M']); $response = $this->get(route('admin.manage.restocks.create')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('data.products', 1) ->has('data.products.0.product_variants', 1) ->where('data.products.0.product_variants.0.id', $variant->id) ->where('data.products.0.product_variants.0.capital_price', 50000) ); }); /* |-------------------------------------------------------------------------- | UPDATE / PUT |-------------------------------------------------------------------------- */ test('restock can be updated', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload()); $restock = Restock::first(); $variant = makeRestockVariant(); $response = $this->put(route('admin.manage.restocks.update', $restock), [ 'stock_type' => 'reject', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 4], ], 'notes' => 'Updated notes', ]); $response ->assertSessionHasNoErrors() ->assertRedirect(); $restock->refresh(); expect($restock->stock_type->value)->toBe('reject'); expect($restock->notes)->toBe('Updated notes'); expect($restock->subtotal)->toBe(200000); expect(RestockItem::where('restock_id', $restock->id)->count())->toBe(1); expect(RestockItem::first()->product_variant_id)->toBe($variant->id); }); test('restock update adjusts stock by difference', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['stock' => 10, 'reject_stock' => 5]); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 10], ], 'notes' => null, ]); expect($variant->fresh()->stock)->toBe(20); $restock = Restock::first(); $this->put(route('admin.manage.restocks.update', $restock), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 4], ], 'notes' => null, ]); expect($variant->fresh()->stock)->toBe(14); expect($variant->fresh()->reject_stock)->toBe(5); }); test('restock update moves stock when quality changes', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['stock' => 10, 'reject_stock' => 5]); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 10], ], 'notes' => null, ]); expect($variant->fresh()->stock)->toBe(20); $restock = Restock::first(); $this->put(route('admin.manage.restocks.update', $restock), [ 'stock_type' => 'reject', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 10], ], 'notes' => null, ]); $variant->refresh(); expect($variant->stock)->toBe(10); expect($variant->reject_stock)->toBe(15); }); test('updating non-existent restock returns 404', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->put(route('admin.manage.restocks.update', 999999), makeValidRestockPayload()); $response->assertStatus(404); }); /* |-------------------------------------------------------------------------- | DELETE / DESTROY |-------------------------------------------------------------------------- */ test('restock can be deleted and reduces product variant stock', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['stock' => 10, 'reject_stock' => 5]); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 10], ], 'notes' => null, ]); $restock = Restock::first(); expect($variant->fresh()->stock)->toBe(20); $response = $this->delete(route('admin.manage.restocks.destroy', $restock)); $response ->assertSessionHasNoErrors() ->assertRedirect(route('admin.manage.restocks.index')); $this->assertSoftDeleted('restocks', ['id' => $restock->id]); expect($variant->fresh()->stock)->toBe(10); expect($variant->fresh()->reject_stock)->toBe(5); }); test('restock delete reduces reject stock for reject restocks', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(['stock' => 10, 'reject_stock' => 5]); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'reject', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 7], ], 'notes' => null, ]); $restock = Restock::first(); expect($variant->fresh()->reject_stock)->toBe(12); $this->delete(route('admin.manage.restocks.destroy', $restock)); $this->assertSoftDeleted('restocks', ['id' => $restock->id]); expect($variant->fresh()->reject_stock)->toBe(5); }); test('delete cascades to restock items', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $variant = makeRestockVariant(); $this->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => [ ['product_variant_id' => $variant->id, 'quantity' => 10], ], 'notes' => null, ]); $restock = Restock::first(); expect(RestockItem::where('restock_id', $restock->id)->count())->toBe(1); $this->delete(route('admin.manage.restocks.destroy', $restock)); expect(RestockItem::where('restock_id', $restock->id)->count())->toBe(0); }); test('restock delete removes attached photo', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $this->post(route('admin.manage.restocks.store'), makeValidRestockPayload([ 'photo_key' => 'restock/delete-photo.jpg', ])); $restock = Restock::first(); expect($restock->getFirstMedia('photos')?->file_name) ->toBe('restock/delete-photo.jpg'); $this->delete(route('admin.manage.restocks.destroy', $restock)); expect($restock->fresh()->getMedia('photos'))->toHaveCount(0); }); test('deleting non-existent restock returns 404', function () { $user = User::factory()->create(); giveRestockPermissions($user); $this->actingAs($user); $response = $this->delete(route('admin.manage.restocks.destroy', 999999)); $response->assertStatus(404); });