dstpabuaran.com/tests/Feature/Admin/Manage/PurchaseTest.php

1013 lines
35 KiB
PHP

<?php
use App\Models\Purchase;
use App\Models\PurchaseItem;
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Models\Supplier;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
uses(RefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| HELPERS
|--------------------------------------------------------------------------
*/
function givePurchasePermissions(User $user): void
{
$seeder = new RolePermissionSeeder;
$seeder->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)
->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);
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);
});
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);
});