run(); $user->givePermissionTo([ 'purchase.view', 'purchase.create', 'purchase.update', 'purchase.delete', ]); } function makeValidPurchasePayload(array $overrides = []): array { return array_merge([ 'name' => 'Bahan Baku Test', 'unit' => 'kg', 'variants' => [ [ 'variant' => 'Varian Default', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/test-photo.jpg', ], ], 'supplier_id' => Supplier::factory()->create()->id, 'discount' => 0, 'shipping_cost' => 0, 'notes' => 'Belanja test', 'photo_key' => null, ], $overrides); } /* |-------------------------------------------------------------------------- | AUTHENTICATION |-------------------------------------------------------------------------- */ test('guests are redirected to the login page', function () { $response = $this->get(route('admin.manage.purchases.index')); $response->assertRedirect(route('login')); }); test('guests are redirected when visiting create page', function () { $response = $this->get(route('admin.manage.purchases.create')); $response->assertRedirect(route('login')); }); test('guests are redirected when visiting edit page', function () { $purchase = Purchase::factory()->create(); $response = $this->get(route('admin.manage.purchases.edit', $purchase)); $response->assertRedirect(route('login')); }); test('guest cannot create purchase', function () { $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $response->assertRedirect(route('login')); $this->assertDatabaseCount('purchases', 0); }); test('guest cannot update purchase', function () { $purchase = Purchase::factory()->create(); $response = $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload()); $response->assertRedirect(route('login')); }); test('guest cannot delete purchase', function () { $purchase = Purchase::factory()->create(); $response = $this->delete(route('admin.manage.purchases.destroy', $purchase)); $response->assertRedirect(route('login')); $this->assertDatabaseHas('purchases', ['id' => $purchase->id, 'deleted_at' => null]); }); test('authenticated users can visit the purchase index page', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->get(route('admin.manage.purchases.index')); $response->assertOk(); }); test('authenticated users can visit the purchase create page', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->get(route('admin.manage.purchases.create')); $response->assertOk(); }); test('authenticated users can visit the purchase edit page', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $purchase = Purchase::factory()->create(); $response = $this->get(route('admin.manage.purchases.edit', $purchase)); $response->assertOk(); }); /* |-------------------------------------------------------------------------- | INDEX PAGE |-------------------------------------------------------------------------- */ test('purchase index page displays purchases', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); Purchase::factory()->count(3)->create(); $response = $this->get(route('admin.manage.purchases.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/manage/purchase/index') ->has('purchases.data', 3) ->where('purchases.total', 3) ->where('purchases.current_page', 1) ->where('purchases.per_page', 25) ); }); test('index page works with zero purchases', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->get(route('admin.manage.purchases.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/manage/purchase/index') ->has('purchases.data', 0) ->where('purchases.total', 0) ); }); test('index page does not display soft-deleted purchases', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); Purchase::factory()->create(['notes' => 'Active Purchase']); Purchase::factory()->create(['notes' => 'Deleted Purchase'])->delete(); $response = $this->get(route('admin.manage.purchases.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/manage/purchase/index') ->has('purchases.data', 1) ->where('purchases.total', 1) ); }); test('index page includes purchase items with nested data', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $response = $this->get(route('admin.manage.purchases.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('purchases.data.0.purchase_items', 1) ->has('purchases.data.0.purchase_items.0.raw_material_price') ->has('purchases.data.0.purchase_items.0.raw_material_price.raw_material') ); }); test('index page returns correct purchase item data', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ], ])); $response = $this->get(route('admin.manage.purchases.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('purchases.data.0.purchase_items.0.unit_price', 50000) ->where('purchases.data.0.purchase_items.0.quantity', '10.0000') ->where('purchases.data.0.purchase_items.0.subtotal', 500000) ->where('purchases.data.0.purchase_items.0.raw_material_price.variant', 'Merah') ->where('purchases.data.0.purchase_items.0.raw_material_price.raw_material.name', 'Bahan Baku Test') ->where('purchases.data.0.purchase_items.0.raw_material_price.raw_material.unit', 'kg') ); }); test('index page returns multiple purchase items', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ['variant' => 'Biru', 'price' => 60000, 'stock' => 5, 'photo_key' => 'raw-material-variant/blue.jpg'], ], ])); $response = $this->get(route('admin.manage.purchases.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('purchases.data.0.purchase_items', 2) ->where('purchases.data.0.purchase_items.0.raw_material_price.variant', 'Merah') ->where('purchases.data.0.purchase_items.1.raw_material_price.variant', 'Biru') ); }); /* |-------------------------------------------------------------------------- | CREATE / STORE - BASIC |-------------------------------------------------------------------------- */ test('purchase can be created', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $response ->assertSessionHasNoErrors() ->assertRedirect(route('admin.manage.purchases.index')); $this->assertDatabaseCount('purchases', 1); $this->assertDatabaseCount('raw_materials', 1); $this->assertDatabaseCount('raw_material_prices', 1); $this->assertDatabaseCount('purchase_items', 1); }); test('purchase creates raw material with correct data', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'name' => 'Kain Katun', 'unit' => 'meter', ])); $this->assertDatabaseHas('raw_materials', [ 'name' => 'Kain Katun', 'unit' => 'meter', 'is_active' => true, ]); }); test('purchase creates raw material variants', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ['variant' => 'Biru', 'price' => 60000, 'stock' => 5, 'photo_key' => 'raw-material-variant/blue.jpg'], ], ])); $this->assertDatabaseCount('raw_material_prices', 2); $this->assertDatabaseHas('raw_material_prices', ['variant' => 'Merah', 'price' => 50000, 'stock' => 10]); $this->assertDatabaseHas('raw_material_prices', ['variant' => 'Biru', 'price' => 60000, 'stock' => 5]); }); test('purchase calculates correct subtotal and total', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Varian 1', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/v1.jpg'], ], 'discount' => 10000, 'shipping_cost' => 5000, ])); $purchase = Purchase::first(); expect($purchase->subtotal)->toBe(500000); expect($purchase->discount)->toBe(10000); expect($purchase->shipping_cost)->toBe(5000); expect($purchase->total)->toBe(495000); }); test('purchase sets created_by_id to current user', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $purchase = Purchase::first(); expect($purchase->created_by_id)->toBe($user->id); }); test('purchase can be created with notes', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'notes' => 'Belanja bahan baku untuk produksi', ])); $purchase = Purchase::first(); expect($purchase->notes)->toBe('Belanja bahan baku untuk produksi'); }); test('purchase can be created without notes', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'notes' => null, ])); $purchase = Purchase::first(); expect($purchase->notes)->toBeNull(); }); test('multiple purchases can be created sequentially', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['name' => 'Bahan 1'])); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['name' => 'Bahan 2'])); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['name' => 'Bahan 3'])); $this->assertDatabaseCount('purchases', 3); $this->assertDatabaseCount('raw_materials', 3); }); test('purchase creates purchase items with correct relationships', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $purchase = Purchase::first(); $item = PurchaseItem::first(); expect($item->purchase_id)->toBe($purchase->id); expect($item->raw_material_price_id)->not->toBeNull(); expect($item->quantity)->toBe('10.0000'); expect($item->unit_price)->toBe(50000); expect($item->subtotal)->toBe(500000); }); test('purchase item links to correct raw material price', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ], ])); $item = PurchaseItem::first(); $price = RawMaterialPrice::find($item->raw_material_price_id); expect($price)->not->toBeNull(); expect($price->variant)->toBe('Merah'); expect($price->price)->toBe(50000); expect($price->stock)->toBe('10.0000'); }); test('purchase calculates multiple variant subtotals correctly', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ['variant' => 'Biru', 'price' => 60000, 'stock' => 5, 'photo_key' => 'raw-material-variant/blue.jpg'], ], 'discount' => 10000, 'shipping_cost' => 5000, ])); $purchase = Purchase::first(); expect($purchase->subtotal)->toBe(800000); // (50000*10) + (60000*5) expect($purchase->total)->toBe(795000); // 800000 - 10000 + 5000 }); test('purchase can be created from existing raw material', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $price = RawMaterialPrice::factory()->create(['stock' => 100]); $materialId = RawMaterial::first()->id; $response = $this->post(route('admin.manage.purchases.store'), [ 'mode' => 'existing', 'existing_items' => [ ['raw_material_price_id' => $price->id, 'quantity' => 5, 'unit_price' => 50000], ], 'supplier_id' => Supplier::factory()->create()->id, 'discount' => 0, 'shipping_cost' => 0, 'notes' => null, 'photo_key' => null, ]); $response ->assertSessionHasNoErrors() ->assertRedirect(route('admin.manage.purchases.index')); $this->assertDatabaseCount('raw_materials', 1); $this->assertDatabaseHas('raw_materials', ['id' => $materialId, 'deleted_at' => null]); expect((float) $price->fresh()->stock)->toBe(105.0); expect(PurchaseItem::count())->toBe(1); expect(PurchaseItem::first()->raw_material_price_id)->toBe($price->id); }); /* |-------------------------------------------------------------------------- | STORE VALIDATION - REQUIRED FIELDS |-------------------------------------------------------------------------- */ test('purchase name is required', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['name' => ''])); $response->assertSessionHasErrors('name'); }); test('purchase unit is required', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['unit' => ''])); $response->assertSessionHasErrors('unit'); }); test('purchase variants is required', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['variants' => []])); $response->assertSessionHasErrors('variants'); }); test('purchase variant name is required', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $payload = makeValidPurchasePayload(); $payload['variants'][0]['variant'] = ''; $response = $this->post(route('admin.manage.purchases.store'), $payload); $response->assertSessionHasErrors('variants.0.variant'); }); test('purchase variant price is required', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $payload = makeValidPurchasePayload(); unset($payload['variants'][0]['price']); $response = $this->post(route('admin.manage.purchases.store'), $payload); $response->assertSessionHasErrors('variants.0.price'); }); test('purchase variant stock is required', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $payload = makeValidPurchasePayload(); unset($payload['variants'][0]['stock']); $response = $this->post(route('admin.manage.purchases.store'), $payload); $response->assertSessionHasErrors('variants.0.stock'); }); test('purchase variant photo_key is required', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $payload = makeValidPurchasePayload(); unset($payload['variants'][0]['photo_key']); $response = $this->post(route('admin.manage.purchases.store'), $payload); $response->assertSessionHasErrors('variants.0.photo_key'); }); test('purchase supplier_id is required', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['supplier_id' => ''])); $response->assertSessionHasErrors('supplier_id'); }); test('purchase discount must be >= 0', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['discount' => -100])); $response->assertSessionHasErrors('discount'); }); test('purchase shipping_cost must be >= 0', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['shipping_cost' => -100])); $response->assertSessionHasErrors('shipping_cost'); }); test('purchase notes must not exceed 100 characters', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload(['notes' => str_repeat('a', 101)])); $response->assertSessionHasErrors('notes'); }); /* |-------------------------------------------------------------------------- | EDIT PAGE |-------------------------------------------------------------------------- */ test('edit page shows purchase data', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $purchase = Purchase::factory()->create([ 'discount' => 10000, 'shipping_cost' => 5000, 'notes' => 'Test notes', ]); $rawMaterial = RawMaterial::factory()->create(['name' => 'Kain Test']); $variant = RawMaterialPrice::factory()->create([ 'raw_material_id' => $rawMaterial->id, 'variant' => 'Merah', 'price' => 50000, 'stock' => 10, ]); PurchaseItem::factory()->create([ 'purchase_id' => $purchase->id, 'raw_material_price_id' => $variant->id, 'quantity' => 10, 'unit_price' => 50000, 'subtotal' => 500000, ]); $response = $this->get(route('admin.manage.purchases.edit', $purchase)); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/manage/purchase/edit') ->where('purchase.id', $purchase->id) ->where('purchase.name', 'Kain Test') ->has('purchase.unit') ->has('purchase.variants', 1) ->has('data.suppliers') ); }); test('edit page returns suppliers list', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); Supplier::factory()->count(3)->create(); $purchase = Purchase::factory()->create(); $response = $this->get(route('admin.manage.purchases.edit', $purchase)); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('data.suppliers', 4) ); }); /* |-------------------------------------------------------------------------- | UPDATE / PUT |-------------------------------------------------------------------------- */ test('purchase can be updated', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $purchase = Purchase::first(); $response = $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload([ 'name' => 'Bahan Updated', 'unit' => 'meter', 'discount' => 20000, 'shipping_cost' => 10000, 'notes' => 'Updated notes', ])); $response ->assertSessionHasNoErrors() ->assertRedirect(); $purchase->refresh(); expect($purchase->discount)->toBe(20000); expect($purchase->shipping_cost)->toBe(10000); expect($purchase->notes)->toBe('Updated notes'); }); test('purchase update recalculates totals', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Varian 1', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/v1.jpg'], ], 'discount' => 10000, 'shipping_cost' => 5000, ])); $purchase = Purchase::first(); $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Varian 1', 'price' => 60000, 'stock' => 20, 'photo_key' => 'raw-material-variant/v1.jpg'], ], 'discount' => 20000, 'shipping_cost' => 10000, ])); $purchase->refresh(); expect($purchase->subtotal)->toBe(1200000); expect($purchase->discount)->toBe(20000); expect($purchase->shipping_cost)->toBe(10000); expect($purchase->total)->toBe(1190000); }); test('purchase update reuses the same raw material in place', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'name' => 'Old Material', 'variants' => [ ['variant' => 'Old Variant', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/old.jpg'], ], ])); $oldRawMaterialId = RawMaterial::where('name', 'Old Material')->first()->id; $purchase = Purchase::first(); $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload([ 'name' => 'New Material', 'variants' => [ ['variant' => 'New Variant', 'price' => 60000, 'stock' => 20, 'photo_key' => 'raw-material-variant/new.jpg'], ], ])); $this->assertDatabaseCount('raw_materials', 1); $this->assertDatabaseHas('raw_materials', ['id' => $oldRawMaterialId, 'name' => 'New Material']); }); test('purchase update never deletes removed variants, only their stock', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'name' => 'Bahan Baku Test', 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ], ])); $price = RawMaterialPrice::where('variant', 'Merah')->first(); $materialId = RawMaterial::first()->id; $purchase = Purchase::first(); $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload([ 'name' => 'Bahan Baku Test', 'variants' => [ ['variant' => 'Biru', 'price' => 60000, 'stock' => 5, 'photo_key' => 'raw-material-variant/blue.jpg'], ['variant' => 'Hijau', 'price' => 70000, 'stock' => 3, 'photo_key' => 'raw-material-variant/green.jpg'], ], ])); expect(PurchaseItem::count())->toBe(2); $this->assertDatabaseCount('raw_material_prices', 3); $this->assertDatabaseHas('raw_material_prices', ['raw_material_id' => $materialId, 'variant' => 'Merah', 'stock' => 0]); $this->assertDatabaseHas('raw_material_prices', ['raw_material_id' => $materialId, 'variant' => 'Biru', 'stock' => 5]); $this->assertDatabaseHas('raw_material_prices', ['raw_material_id' => $materialId, 'variant' => 'Hijau', 'stock' => 3]); expect($price->fresh()->trashed())->toBeFalse(); }); test('purchase update adds new variants to the same raw material', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ], ])); $materialId = RawMaterial::first()->id; $purchase = Purchase::first(); $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ['variant' => 'Hijau', 'price' => 70000, 'stock' => 3, 'photo_key' => 'raw-material-variant/green.jpg'], ], ])); $this->assertDatabaseCount('raw_materials', 1); $this->assertDatabaseHas('raw_material_prices', ['raw_material_id' => $materialId, 'variant' => 'Hijau', 'stock' => 3]); $this->assertDatabaseHas('raw_material_prices', ['raw_material_id' => $materialId, 'variant' => 'Merah', 'stock' => 10]); expect(PurchaseItem::count())->toBe(2); }); test('purchase update adjusts stock by difference', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Varian 1', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/v1.jpg'], ], ])); $price = RawMaterialPrice::first(); expect((float) $price->stock)->toBe(10.0); $purchase = Purchase::first(); $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Varian 1', 'price' => 50000, 'stock' => 4, 'photo_key' => 'raw-material-variant/v1.jpg'], ], ])); expect((float) $price->fresh()->stock)->toBe(4.0); }); test('purchase update in existing mode adjusts stock by difference', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Varian 1', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/v1.jpg'], ], ])); $price = RawMaterialPrice::first(); $purchase = Purchase::first(); $response = $this->put(route('admin.manage.purchases.update', $purchase), [ 'mode' => 'existing', 'existing_items' => [ ['raw_material_price_id' => $price->id, 'quantity' => 4, 'unit_price' => 50000], ], 'supplier_id' => $purchase->supplier_id, 'discount' => 0, 'shipping_cost' => 0, 'notes' => null, 'photo_key' => null, ]); $response->assertSessionHasNoErrors(); expect((float) $price->fresh()->stock)->toBe(4.0); expect(PurchaseItem::count())->toBe(1); }); test('purchase update does not change the raw material unit', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'unit' => 'kg', ])); $materialId = RawMaterial::first()->id; $purchase = Purchase::first(); $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload([ 'unit' => 'meter', ])); $this->assertDatabaseHas('raw_materials', ['id' => $materialId, 'unit' => 'kg']); }); test('purchase update replaces old purchase items', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ], ])); $oldItemCount = PurchaseItem::count(); $purchase = Purchase::first(); $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Biru', 'price' => 60000, 'stock' => 5, 'photo_key' => 'raw-material-variant/blue.jpg'], ['variant' => 'Hijau', 'price' => 70000, 'stock' => 3, 'photo_key' => 'raw-material-variant/green.jpg'], ], ])); expect(PurchaseItem::count())->toBe(2); $this->assertDatabaseHas('raw_material_prices', ['variant' => 'Biru']); $this->assertDatabaseHas('raw_material_prices', ['variant' => 'Hijau']); }); test('updating non-existent purchase returns 404', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->put(route('admin.manage.purchases.update', 999999), makeValidPurchasePayload()); $response->assertStatus(404); }); /* |-------------------------------------------------------------------------- | DELETE / DESTROY |-------------------------------------------------------------------------- */ test('purchase can be deleted and reduces raw material stock', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $purchase = Purchase::first(); $price = RawMaterialPrice::where('variant', 'Varian Default')->first(); expect((float) $price->stock)->toBe(10.0); $response = $this->delete(route('admin.manage.purchases.destroy', $purchase)); $response ->assertSessionHasNoErrors() ->assertRedirect(route('admin.manage.purchases.index')); $this->assertSoftDeleted('purchases', ['id' => $purchase->id]); $this->assertDatabaseHas('raw_materials', ['name' => 'Bahan Baku Test', 'deleted_at' => null]); $this->assertDatabaseHas('raw_material_prices', ['variant' => 'Varian Default', 'stock' => 0, 'deleted_at' => null]); }); test('purchase is soft-deleted not permanently removed', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $purchase = Purchase::first(); $this->delete(route('admin.manage.purchases.destroy', $purchase)); $this->assertDatabaseHas('purchases', ['id' => $purchase->id]); $this->assertSoftDeleted('purchases', ['id' => $purchase->id]); }); test('delete cascades to purchase items', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ['variant' => 'Biru', 'price' => 60000, 'stock' => 5, 'photo_key' => 'raw-material-variant/blue.jpg'], ], ])); $purchase = Purchase::first(); expect(PurchaseItem::count())->toBe(2); $this->delete(route('admin.manage.purchases.destroy', $purchase)); expect(PurchaseItem::where('purchase_id', $purchase->id)->count())->toBe(0); }); test('delete reduces stock but keeps raw materials and variants', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload([ 'variants' => [ ['variant' => 'Merah', 'price' => 50000, 'stock' => 10, 'photo_key' => 'raw-material-variant/red.jpg'], ['variant' => 'Biru', 'price' => 60000, 'stock' => 5, 'photo_key' => 'raw-material-variant/blue.jpg'], ], ])); $purchase = Purchase::first(); $rawMaterial = RawMaterial::first(); $this->delete(route('admin.manage.purchases.destroy', $purchase)); $this->assertDatabaseHas('raw_materials', ['id' => $rawMaterial->id, 'deleted_at' => null]); $this->assertDatabaseHas('raw_material_prices', ['variant' => 'Merah', 'stock' => 0, 'deleted_at' => null]); $this->assertDatabaseHas('raw_material_prices', ['variant' => 'Biru', 'stock' => 0, 'deleted_at' => null]); }); test('deleting non-existent purchase returns 404', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $response = $this->delete(route('admin.manage.purchases.destroy', 999999)); $response->assertStatus(404); }); test('updating soft-deleted purchase via route returns 404', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $purchase = Purchase::first(); $purchase->delete(); $response = $this->put(route('admin.manage.purchases.update', $purchase), makeValidPurchasePayload()); $response->assertStatus(404); }); test('deleting soft-deleted purchase via route returns 404', function () { $user = User::factory()->create(); givePurchasePermissions($user); $this->actingAs($user); $this->post(route('admin.manage.purchases.store'), makeValidPurchasePayload()); $purchase = Purchase::first(); $purchase->delete(); $response = $this->delete(route('admin.manage.purchases.destroy', $purchase)); $response->assertStatus(404); });