dstpabuaran.com/tests/Feature/Admin/Manage/RestockTest.php
Yoga Pangestu cf0e772159 feat: implement restock management functionality
- Add RestockIndex component for displaying and managing restocks.
- Create RestockCardRow component for rendering individual restock items.
- Implement RestockItemSubRow component for displaying detailed item information.
- Define routes for restock management in web.php.
- Create RestockTest to cover various scenarios for restock creation, updating, and deletion.
- Ensure proper handling of permissions for restock actions.
- Add validation for restock data and ensure correct relationships are maintained.
2026-08-02 22:34:10 +07:00

839 lines
26 KiB
PHP

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