228 lines
7.3 KiB
PHP
228 lines
7.3 KiB
PHP
<?php
|
|
|
|
use App\Models\Country;
|
|
use App\Models\Ingredient;
|
|
use App\Models\Location;
|
|
use App\Models\MaterialQuality;
|
|
use App\Models\StockEntry;
|
|
use App\Models\Supplier;
|
|
use App\Repositories\StockEntryRepository;
|
|
use App\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
function phase3MakeUser(int $adminLevel): User
|
|
{
|
|
$user = User::query()->create([
|
|
'email' => uniqid('inv3_', true).'@test.example',
|
|
'password' => bcrypt('password'),
|
|
]);
|
|
$user->forceFill([
|
|
'admin' => $adminLevel,
|
|
'confirmed' => true,
|
|
'active' => true,
|
|
'wizard' => 100,
|
|
'blocked' => false,
|
|
])->save();
|
|
|
|
return $user->fresh();
|
|
}
|
|
|
|
function phase3EnsureCountry(): Country
|
|
{
|
|
return Country::query()->firstOrCreate(
|
|
['code' => 'DE'],
|
|
[
|
|
'phone' => '00',
|
|
'en' => 'Germany',
|
|
'de' => 'Deutschland',
|
|
'es' => 'Germany',
|
|
'fr' => 'Germany',
|
|
'it' => 'Germany',
|
|
'ru' => 'Germany',
|
|
'active' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
function phase3MakeIngredient(): Ingredient
|
|
{
|
|
return Ingredient::query()->create([
|
|
'name' => 'Test-INCI-'.uniqid('', true),
|
|
'trans_name' => '',
|
|
'inci' => '',
|
|
'trans_inci' => '',
|
|
'effect' => '',
|
|
'trans_effect' => '',
|
|
'active' => true,
|
|
'pos' => 0,
|
|
]);
|
|
}
|
|
|
|
test('copyreader can list stock entries and receive but not create form', function () {
|
|
$user = phase3MakeUser(1);
|
|
$this->actingAs($user, 'user');
|
|
|
|
$this->get(route('admin.inventory.stock-entries.index'))->assertSuccessful();
|
|
$this->get(route('admin.inventory.stock-entries.create'))->assertRedirect(route('home'));
|
|
});
|
|
|
|
test('non-copyreader cannot access stock entries', function () {
|
|
$user = phase3MakeUser(0);
|
|
$this->actingAs($user, 'user');
|
|
|
|
$this->get(route('admin.inventory.stock-entries.index'))->assertRedirect(route('home'));
|
|
});
|
|
|
|
test('admin creates pending stock entry with unit gram', function () {
|
|
phase3EnsureCountry();
|
|
$admin = phase3MakeUser(7);
|
|
$ingredient = phase3MakeIngredient();
|
|
$location = Location::factory()->create();
|
|
$supplier = Supplier::factory()->create();
|
|
|
|
$this->actingAs($admin, 'user');
|
|
|
|
$this->post(route('admin.inventory.stock-entries.store'), [
|
|
'entry_type' => 'ingredient',
|
|
'ingredient_id' => (string) $ingredient->id,
|
|
'supplier_id' => (string) $supplier->id,
|
|
'location_id' => (string) $location->id,
|
|
'ordered_at' => '2026-03-27',
|
|
'ordered_quantity' => '1000',
|
|
'price_per_kg' => '12,50',
|
|
])->assertRedirect(route('admin.inventory.stock-entries.index'));
|
|
|
|
$entry = StockEntry::query()->latest('id')->firstOrFail();
|
|
expect($entry->status)->toBe('pending');
|
|
expect($entry->unit)->toBe('gram');
|
|
expect((float) $entry->ordered_quantity)->toBe(1000.0);
|
|
});
|
|
|
|
test('copyreader can book receive for pending entry', function () {
|
|
phase3EnsureCountry();
|
|
$admin = phase3MakeUser(7);
|
|
$copy = phase3MakeUser(1);
|
|
$ingredient = phase3MakeIngredient();
|
|
$location = Location::factory()->create();
|
|
$supplier = Supplier::factory()->create();
|
|
$mq = MaterialQuality::query()->create(['name' => 'MQ-Test-'.uniqid('', true), 'pos' => 1]);
|
|
|
|
$entry = StockEntry::query()->create([
|
|
'entry_type' => 'ingredient',
|
|
'ingredient_id' => $ingredient->id,
|
|
'packaging_item_id' => null,
|
|
'supplier_id' => $supplier->id,
|
|
'location_id' => $location->id,
|
|
'unit' => 'gram',
|
|
'ordered_by' => $admin->id,
|
|
'ordered_at' => '2026-03-01',
|
|
'ordered_quantity' => 500,
|
|
'price_per_kg' => 10,
|
|
'price_total' => null,
|
|
'status' => 'pending',
|
|
]);
|
|
|
|
$this->actingAs($copy, 'user');
|
|
$this->put(route('admin.inventory.stock-entries.receive', $entry), [
|
|
'received_at' => '2026-03-15',
|
|
'received_quantity' => '500',
|
|
'batch_number' => 'B-001',
|
|
'best_before' => '2027-12-31',
|
|
'quality_id' => (string) $mq->id,
|
|
])->assertRedirect(route('admin.inventory.stock-entries.show', $entry));
|
|
|
|
$entry->refresh();
|
|
expect($entry->status)->toBe('received');
|
|
expect((float) $entry->received_quantity)->toBe(500.0);
|
|
expect($entry->received_by)->toBe($copy->id);
|
|
});
|
|
|
|
test('ingredient search returns select2 results shape', function () {
|
|
$user = phase3MakeUser(1);
|
|
$ingredient = phase3MakeIngredient();
|
|
|
|
$this->actingAs($user, 'user');
|
|
$response = $this->getJson(route('admin.inventory.api.ingredients.search', ['q' => $ingredient->name]));
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonStructure(['results' => [['id', 'text']]]);
|
|
expect($response->json('results.0.id'))->toBe($ingredient->id);
|
|
});
|
|
|
|
test('stock entry factory creates valid pending ingredient entry', function () {
|
|
phase3EnsureCountry();
|
|
$e = StockEntry::factory()->create();
|
|
expect($e->status)->toBe('pending');
|
|
expect($e->unit)->toBe('gram');
|
|
expect($e->ingredient_id)->toBeGreaterThan(0);
|
|
});
|
|
|
|
test('stock entry factory packaging and received states', function () {
|
|
phase3EnsureCountry();
|
|
MaterialQuality::query()->create(['name' => 'MQ-Factory-'.uniqid('', true), 'pos' => 99]);
|
|
|
|
$p = StockEntry::factory()->packaging()->create();
|
|
expect($p->entry_type)->toBe('packaging');
|
|
expect($p->unit)->toBe('piece');
|
|
expect($p->price_total)->not->toBeNull();
|
|
|
|
$r = StockEntry::factory()->received()->create();
|
|
expect($r->status)->toBe('received');
|
|
expect($r->received_quantity)->not->toBeNull();
|
|
});
|
|
|
|
test('listForIndex sorts pending before received', function () {
|
|
phase3EnsureCountry();
|
|
$admin = phase3MakeUser(7);
|
|
$ingredient = phase3MakeIngredient();
|
|
$location = Location::factory()->create();
|
|
$supplier = Supplier::factory()->create();
|
|
|
|
$this->actingAs($admin, 'user');
|
|
|
|
$this->post(route('admin.inventory.stock-entries.store'), [
|
|
'entry_type' => 'ingredient',
|
|
'ingredient_id' => (string) $ingredient->id,
|
|
'supplier_id' => (string) $supplier->id,
|
|
'location_id' => (string) $location->id,
|
|
'ordered_at' => '2026-01-01',
|
|
'ordered_quantity' => '100',
|
|
'price_per_kg' => '1',
|
|
]);
|
|
|
|
$this->post(route('admin.inventory.stock-entries.store'), [
|
|
'entry_type' => 'ingredient',
|
|
'ingredient_id' => (string) $ingredient->id,
|
|
'supplier_id' => (string) $supplier->id,
|
|
'location_id' => (string) $location->id,
|
|
'ordered_at' => '2026-06-01',
|
|
'ordered_quantity' => '200',
|
|
'price_per_kg' => '1',
|
|
]);
|
|
|
|
$entries = StockEntry::query()->orderBy('id')->get();
|
|
expect($entries)->toHaveCount(2);
|
|
$older = $entries[0];
|
|
$newer = $entries[1];
|
|
|
|
$this->actingAs($admin, 'user');
|
|
$this->put(route('admin.inventory.stock-entries.receive', $older), [
|
|
'received_at' => '2026-03-10',
|
|
'received_quantity' => '100',
|
|
'batch_number' => 'X',
|
|
'best_before' => '2028-01-01',
|
|
'quality_id' => '',
|
|
]);
|
|
|
|
$repo = app(StockEntryRepository::class);
|
|
$list = $repo->listForIndex();
|
|
|
|
expect($list)->toHaveCount(2);
|
|
expect($list[0]->id)->toBe($newer->id);
|
|
expect($list[0]->status)->toBe('pending');
|
|
expect($list[1]->id)->toBe($older->id);
|
|
expect($list[1]->status)->toBe('received');
|
|
});
|