- Updated ProductIndex component to improve category and product filtering with memoization. - Refactored Combobox components for better performance and usability. - Added PurchaseController routes for managing purchases with appropriate permissions. - Created comprehensive tests for purchase management, covering creation, updating, and deletion scenarios. - Ensured proper handling of raw materials and their variants during purchase operations. - Implemented validation for required fields in purchase creation and updates.
1013 lines
35 KiB
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.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);
|
|
});
|