April 2026 waren Wirtschaft Feedback
This commit is contained in:
parent
02f2a4c23e
commit
9ce711d6b2
167 changed files with 25278 additions and 8518 deletions
294
tests/Feature/InventoryPhase2Test.php
Normal file
294
tests/Feature/InventoryPhase2Test.php
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\Location;
|
||||
use App\Models\MaterialQuality;
|
||||
use App\Models\PackagingItem;
|
||||
use App\Models\PackagingMaterial;
|
||||
use App\Models\Product;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\SupplierCategory;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function makeUser(int $adminLevel): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('inv_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
function ensureCountry(): Country
|
||||
{
|
||||
return Country::query()->firstOrCreate(
|
||||
['code' => 'DE'],
|
||||
[
|
||||
'phone' => '00',
|
||||
'en' => 'Germany',
|
||||
'de' => 'Deutschland',
|
||||
'es' => 'Germany',
|
||||
'fr' => 'Germany',
|
||||
'it' => 'Germany',
|
||||
'ru' => 'Germany',
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
test('locations full CRUD as superadmin', function () {
|
||||
$user = makeUser(8);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.locations.index'))->assertSuccessful();
|
||||
$this->get(route('admin.inventory.locations.create'))->assertSuccessful();
|
||||
|
||||
$this->post(route('admin.inventory.locations.store'), [
|
||||
'name' => 'CRUD-Lager-A',
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.locations.index'));
|
||||
|
||||
$location = Location::query()->where('name', 'CRUD-Lager-A')->firstOrFail();
|
||||
|
||||
$this->get(route('admin.inventory.locations.edit', $location))->assertSuccessful();
|
||||
|
||||
$this->put(route('admin.inventory.locations.update', $location), [
|
||||
'name' => 'CRUD-Lager-B',
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.locations.index'));
|
||||
|
||||
$location->refresh();
|
||||
expect($location->name)->toBe('CRUD-Lager-B');
|
||||
|
||||
$this->delete(route('admin.inventory.locations.destroy', $location))
|
||||
->assertRedirect(route('admin.inventory.locations.index'));
|
||||
|
||||
expect(Location::query()->whereKey($location->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('material qualities full CRUD as superadmin', function () {
|
||||
$user = makeUser(8);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.material-qualities.index'))->assertSuccessful();
|
||||
$this->get(route('admin.inventory.material-qualities.create'))->assertSuccessful();
|
||||
|
||||
$this->post(route('admin.inventory.material-qualities.store'), [
|
||||
'name' => 'CRUD-Qualität',
|
||||
'pos' => 7,
|
||||
])->assertRedirect(route('admin.inventory.material-qualities.index'));
|
||||
|
||||
$model = MaterialQuality::query()->where('name', 'CRUD-Qualität')->firstOrFail();
|
||||
expect($model->pos)->toBe(7);
|
||||
|
||||
$this->get(route('admin.inventory.material-qualities.edit', $model))->assertSuccessful();
|
||||
|
||||
$this->put(route('admin.inventory.material-qualities.update', $model), [
|
||||
'name' => 'CRUD-Qualität-upd',
|
||||
'pos' => 8,
|
||||
])->assertRedirect(route('admin.inventory.material-qualities.index'));
|
||||
|
||||
$model->refresh();
|
||||
expect($model->name)->toBe('CRUD-Qualität-upd');
|
||||
expect($model->pos)->toBe(8);
|
||||
|
||||
$this->delete(route('admin.inventory.material-qualities.destroy', $model))
|
||||
->assertRedirect(route('admin.inventory.material-qualities.index'));
|
||||
|
||||
expect(MaterialQuality::query()->whereKey($model->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('packaging materials full CRUD as superadmin', function () {
|
||||
$user = makeUser(8);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.packaging-materials.index'))->assertSuccessful();
|
||||
$this->get(route('admin.inventory.packaging-materials.create'))->assertSuccessful();
|
||||
|
||||
$this->post(route('admin.inventory.packaging-materials.store'), [
|
||||
'name' => 'CRUD-Verpackungsmat',
|
||||
'pos' => 3,
|
||||
])->assertRedirect(route('admin.inventory.packaging-materials.index'));
|
||||
|
||||
$model = PackagingMaterial::query()->where('name', 'CRUD-Verpackungsmat')->firstOrFail();
|
||||
|
||||
$this->get(route('admin.inventory.packaging-materials.edit', $model))->assertSuccessful();
|
||||
|
||||
$this->put(route('admin.inventory.packaging-materials.update', $model), [
|
||||
'name' => 'CRUD-Verpackungsmat-upd',
|
||||
'pos' => 4,
|
||||
])->assertRedirect(route('admin.inventory.packaging-materials.index'));
|
||||
|
||||
$model->refresh();
|
||||
expect($model->name)->toBe('CRUD-Verpackungsmat-upd');
|
||||
|
||||
$this->delete(route('admin.inventory.packaging-materials.destroy', $model))
|
||||
->assertRedirect(route('admin.inventory.packaging-materials.index'));
|
||||
|
||||
expect(PackagingMaterial::query()->whereKey($model->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('supplier categories full CRUD as admin', function () {
|
||||
$user = makeUser(7);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.supplier-categories.index'))->assertSuccessful();
|
||||
$this->get(route('admin.inventory.supplier-categories.create'))->assertSuccessful();
|
||||
|
||||
$this->post(route('admin.inventory.supplier-categories.store'), [
|
||||
'name' => 'CRUD-Lief-Kat',
|
||||
'pos' => 2,
|
||||
])->assertRedirect(route('admin.inventory.supplier-categories.index'));
|
||||
|
||||
$model = SupplierCategory::query()->where('name', 'CRUD-Lief-Kat')->firstOrFail();
|
||||
|
||||
$this->get(route('admin.inventory.supplier-categories.edit', $model))->assertSuccessful();
|
||||
|
||||
$this->put(route('admin.inventory.supplier-categories.update', $model), [
|
||||
'name' => 'CRUD-Lief-Kat-upd',
|
||||
'pos' => 3,
|
||||
])->assertRedirect(route('admin.inventory.supplier-categories.index'));
|
||||
|
||||
$model->refresh();
|
||||
expect($model->name)->toBe('CRUD-Lief-Kat-upd');
|
||||
|
||||
$this->delete(route('admin.inventory.supplier-categories.destroy', $model))
|
||||
->assertRedirect(route('admin.inventory.supplier-categories.index'));
|
||||
|
||||
expect(SupplierCategory::query()->whereKey($model->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('suppliers full CRUD with category sync as admin', function () {
|
||||
$country = ensureCountry();
|
||||
$cat = SupplierCategory::query()->create(['name' => 'CRUD-Kat-Sup', 'pos' => 1]);
|
||||
|
||||
$user = makeUser(7);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.suppliers.index'))->assertSuccessful();
|
||||
$this->get(route('admin.inventory.suppliers.create'))->assertSuccessful();
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.store'), [
|
||||
'name' => 'CRUD-Lieferant',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
'supplier_category_ids' => [(string) $cat->id],
|
||||
'email' => 'crud@example.test',
|
||||
])->assertRedirect(route('admin.inventory.suppliers.index'));
|
||||
|
||||
$supplier = Supplier::query()->where('name', 'CRUD-Lieferant')->firstOrFail();
|
||||
expect($supplier->supplierCategories()->pluck('supplier_categories.id')->all())->toEqual([$cat->id]);
|
||||
|
||||
$this->get(route('admin.inventory.suppliers.edit', $supplier))->assertSuccessful();
|
||||
|
||||
$this->put(route('admin.inventory.suppliers.update', $supplier), [
|
||||
'name' => 'CRUD-Lieferant-upd',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
'supplier_category_ids' => [(string) $cat->id],
|
||||
'email' => 'crud-upd@example.test',
|
||||
])->assertRedirect(route('admin.inventory.suppliers.index'));
|
||||
|
||||
$supplier->refresh();
|
||||
expect($supplier->name)->toBe('CRUD-Lieferant-upd');
|
||||
|
||||
$this->delete(route('admin.inventory.suppliers.destroy', $supplier))
|
||||
->assertRedirect(route('admin.inventory.suppliers.index'));
|
||||
|
||||
expect(Supplier::query()->whereKey($supplier->id)->exists())->toBeFalse();
|
||||
expect(Supplier::withTrashed()->whereKey($supplier->id)->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('packaging items full CRUD as admin', function () {
|
||||
$material = PackagingMaterial::factory()->create(['name' => 'CRUD-PM-'.uniqid('', true)]);
|
||||
$supplier = Supplier::factory()->create();
|
||||
$product = Product::query()->create([
|
||||
'name' => 'CRUD-Produkt',
|
||||
'title' => 'CRUD-Produkt',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$user = makeUser(7);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.packaging-items.index'))->assertSuccessful();
|
||||
$this->get(route('admin.inventory.packaging-items.create'))->assertSuccessful();
|
||||
|
||||
$this->post(route('admin.inventory.packaging-items.store'), [
|
||||
'packaging_material_id' => (string) $material->id,
|
||||
'supplier_id' => (string) $supplier->id,
|
||||
'name' => 'CRUD-Verpack-Art',
|
||||
'category' => 'packaging',
|
||||
'weight_grams' => '12.5',
|
||||
'min_stock_alert' => '100',
|
||||
'product_id' => (string) $product->id,
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.packaging-items.index', ['category' => 'packaging']));
|
||||
|
||||
$item = PackagingItem::query()->where('name', 'CRUD-Verpack-Art')->firstOrFail();
|
||||
expect($item->packaging_material_id)->toBe($material->id);
|
||||
expect($item->product_id)->toBe($product->id);
|
||||
|
||||
$this->get(route('admin.inventory.packaging-items.edit', $item))->assertSuccessful();
|
||||
|
||||
$this->put(route('admin.inventory.packaging-items.update', $item), [
|
||||
'packaging_material_id' => (string) $material->id,
|
||||
'supplier_id' => '',
|
||||
'name' => 'CRUD-Verpack-Art-upd',
|
||||
'category' => 'label',
|
||||
'weight_grams' => '5',
|
||||
'min_stock_alert' => '',
|
||||
'product_id' => '',
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.packaging-items.index', ['category' => 'shipping']));
|
||||
|
||||
$item->refresh();
|
||||
expect($item->name)->toBe('CRUD-Verpack-Art-upd');
|
||||
expect($item->category)->toBe('label');
|
||||
expect($item->supplier_id)->toBeNull();
|
||||
expect($item->product_id)->toBeNull();
|
||||
|
||||
$this->delete(route('admin.inventory.packaging-items.destroy', $item))
|
||||
->assertRedirect(route('admin.inventory.packaging-items.index', ['category' => 'shipping']));
|
||||
|
||||
expect(PackagingItem::query()->whereKey($item->id)->exists())->toBeFalse();
|
||||
expect(PackagingItem::withTrashed()->whereKey($item->id)->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin cannot access superadmin inventory routes', function () {
|
||||
$user = makeUser(7);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.locations.index'))->assertRedirect(route('home'));
|
||||
});
|
||||
|
||||
test('admin cannot store locations', function () {
|
||||
$user = makeUser(7);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->post(route('admin.inventory.locations.store'), [
|
||||
'name' => 'Forbidden-Lager',
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('home'));
|
||||
|
||||
expect(Location::query()->where('name', 'Forbidden-Lager')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('copyreader cannot access inventory suppliers', function () {
|
||||
$user = makeUser(1);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.suppliers.index'))->assertRedirect(route('home'));
|
||||
});
|
||||
228
tests/Feature/InventoryPhase3Test.php
Normal file
228
tests/Feature/InventoryPhase3Test.php
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
<?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');
|
||||
});
|
||||
186
tests/Feature/ProductPhase0Test.php
Normal file
186
tests/Feature/ProductPhase0Test.php
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Attribute;
|
||||
use App\Models\AttributeType;
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductAttribute;
|
||||
use App\Models\ProductIngredient;
|
||||
use App\Repositories\ProductRepository;
|
||||
use App\Services\HTMLHelper;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
test('product_ingredients relation uses product_id foreign key', function () {
|
||||
$product = Product::create([
|
||||
'name' => 'Relation Test',
|
||||
'title' => 'Relation Test',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$ingredient = Ingredient::create([
|
||||
'name' => 'Test INCI',
|
||||
'trans_name' => '',
|
||||
'inci' => '',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
ProductIngredient::create([
|
||||
'product_id' => $product->id,
|
||||
'ingredient_id' => $ingredient->id,
|
||||
]);
|
||||
|
||||
$product->refresh();
|
||||
|
||||
expect($product->product_ingredients)->toHaveCount(1);
|
||||
expect($product->product_ingredients->first()->ingredient_id)->toBe($ingredient->id);
|
||||
});
|
||||
|
||||
test('ingredient product_ingredients relation uses ingredient_id foreign key', function () {
|
||||
$product = Product::create([
|
||||
'name' => 'Relation Test 2',
|
||||
'title' => 'Relation Test 2',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$ingredient = Ingredient::create([
|
||||
'name' => 'Test INCI 2',
|
||||
'trans_name' => '',
|
||||
'inci' => '',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
ProductIngredient::create([
|
||||
'product_id' => $product->id,
|
||||
'ingredient_id' => $ingredient->id,
|
||||
]);
|
||||
|
||||
$ingredient->refresh();
|
||||
|
||||
expect($ingredient->product_ingredients)->toHaveCount(1);
|
||||
expect($ingredient->product_ingredients->first()->product_id)->toBe($product->id);
|
||||
});
|
||||
|
||||
test('updateIngredients removes deselected ingredients', function () {
|
||||
$product = Product::create([
|
||||
'name' => 'Sync Test',
|
||||
'title' => 'Sync Test',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$a = Ingredient::create([
|
||||
'name' => 'Alpha',
|
||||
'trans_name' => '',
|
||||
'inci' => '',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
$b = Ingredient::create([
|
||||
'name' => 'Beta',
|
||||
'trans_name' => '',
|
||||
'inci' => '',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
$repo = new ProductRepository($product);
|
||||
$repo->updateIngredients(['product_ingredients' => [(string) $a->id, (string) $b->id]]);
|
||||
|
||||
expect($product->fresh()->p_ingredients)->toHaveCount(2);
|
||||
|
||||
$repo->updateIngredients(['product_ingredients' => [(string) $a->id]]);
|
||||
|
||||
expect($product->fresh()->p_ingredients)->toHaveCount(1);
|
||||
expect($product->fresh()->p_ingredients->first()->id)->toBe($a->id);
|
||||
});
|
||||
|
||||
test('getProductIngredientsOptions lists ingredients alphabetically by name', function () {
|
||||
Ingredient::create([
|
||||
'name' => 'Zebra',
|
||||
'trans_name' => '',
|
||||
'inci' => '',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
Ingredient::create([
|
||||
'name' => 'Alpha',
|
||||
'trans_name' => '',
|
||||
'inci' => '',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
$html = HTMLHelper::getProductIngredientsOptions([], false);
|
||||
|
||||
expect(strpos($html, 'Alpha'))->toBeLessThan(strpos($html, 'Zebra'));
|
||||
});
|
||||
|
||||
test('copy preserves product attribute type_id', function () {
|
||||
$type = AttributeType::create([
|
||||
'name' => 'Type A',
|
||||
'slug' => 'type-a-'.uniqid(),
|
||||
'pos' => 0,
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$typeB = AttributeType::create([
|
||||
'name' => 'Type B',
|
||||
'slug' => 'type-b-'.uniqid(),
|
||||
'pos' => 1,
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$attr = Attribute::create([
|
||||
'attribute_type_id' => $type->id,
|
||||
'name' => 'Attr',
|
||||
'trans_name' => '',
|
||||
'pos' => 0,
|
||||
'active' => true,
|
||||
'slug' => 'attr-'.uniqid(),
|
||||
]);
|
||||
|
||||
$source = Product::create([
|
||||
'name' => 'Source',
|
||||
'title' => 'Source',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
ProductAttribute::create([
|
||||
'product_id' => $source->id,
|
||||
'type_id' => $typeB->id,
|
||||
'attribute_id' => $attr->id,
|
||||
]);
|
||||
|
||||
$repo = new ProductRepository(new Product);
|
||||
$copy = $repo->copy($source->fresh());
|
||||
|
||||
$copied = $copy->attributes()->first();
|
||||
|
||||
expect($copied)->not->toBeNull();
|
||||
expect($copied->type_id)->toBe($typeB->id);
|
||||
expect($copied->attribute_id)->toBe($attr->id);
|
||||
});
|
||||
92
tests/Feature/ProductPhase1Test.php
Normal file
92
tests/Feature/ProductPhase1Test.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductIngredient;
|
||||
use App\Repositories\ProductRepository;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
test('product saves inci pivot pos gram factor and shelf life', function () {
|
||||
$product = Product::create([
|
||||
'name' => 'Salbe',
|
||||
'title' => 'Salbe',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$a = Ingredient::create([
|
||||
'name' => 'Shea',
|
||||
'trans_name' => '',
|
||||
'inci' => 'Butyrospermum',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
'default_factor' => 1.15,
|
||||
]);
|
||||
|
||||
$repo = new ProductRepository($product);
|
||||
$repo->update([
|
||||
'id' => (string) $product->id,
|
||||
'name' => $product->name,
|
||||
'title' => $product->title,
|
||||
'active' => '1',
|
||||
'product_inci_sync_sent' => '1',
|
||||
'pi_ingredient_id' => [(string) $a->id],
|
||||
'pi_gram' => ['10,5'],
|
||||
'pi_factor' => ['1,20'],
|
||||
'shelf_life_type' => 'fixed',
|
||||
'shelf_life_months' => '12',
|
||||
]);
|
||||
|
||||
$product->refresh();
|
||||
expect($product->shelf_life_type)->toBe('fixed');
|
||||
expect($product->shelf_life_months)->toBe(12);
|
||||
|
||||
$pivot = $product->p_ingredients()->where('ingredients.id', $a->id)->first();
|
||||
expect($pivot)->not->toBeNull();
|
||||
expect((float) $pivot->pivot->gram)->toBe(10.5);
|
||||
expect((float) $pivot->pivot->factor)->toBe(1.20);
|
||||
expect((int) $pivot->pivot->pos)->toBe(0);
|
||||
});
|
||||
|
||||
test('product copy preserves inci pivot fields', function () {
|
||||
$source = Product::create([
|
||||
'name' => 'Original',
|
||||
'title' => 'Original',
|
||||
'active' => true,
|
||||
'shelf_life_type' => 'pao',
|
||||
]);
|
||||
|
||||
$ing = Ingredient::create([
|
||||
'name' => 'Oel',
|
||||
'trans_name' => '',
|
||||
'inci' => 'Oil',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
ProductIngredient::create([
|
||||
'product_id' => $source->id,
|
||||
'ingredient_id' => $ing->id,
|
||||
'pos' => 2,
|
||||
'gram' => 3.25,
|
||||
'factor' => 1.08,
|
||||
]);
|
||||
|
||||
$repo = new ProductRepository(new Product);
|
||||
$copy = $repo->copy($source->fresh());
|
||||
|
||||
$row = ProductIngredient::where('product_id', $copy->id)->where('ingredient_id', $ing->id)->first();
|
||||
expect($row)->not->toBeNull();
|
||||
expect((float) $row->gram)->toBe(3.25);
|
||||
expect((float) $row->factor)->toBe(1.08);
|
||||
expect($row->pos)->toBe(2);
|
||||
expect($copy->shelf_life_type)->toBe('pao');
|
||||
});
|
||||
88
tests/Feature/ProductPhase4Test.php
Normal file
88
tests/Feature/ProductPhase4Test.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
use App\Models\PackagingItem;
|
||||
use App\Models\PackagingMaterial;
|
||||
use App\Models\Product;
|
||||
use App\Models\Supplier;
|
||||
use App\Repositories\ProductRepository;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
test('product saves packaging bom quantities and order', function () {
|
||||
$product = Product::create([
|
||||
'name' => 'Creme',
|
||||
'title' => 'Creme',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$material = PackagingMaterial::factory()->create(['name' => 'Glas-P4']);
|
||||
$supplier = Supplier::factory()->create();
|
||||
$a = PackagingItem::factory()->create([
|
||||
'packaging_material_id' => $material->id,
|
||||
'supplier_id' => $supplier->id,
|
||||
'name' => 'Flasche 30 ml',
|
||||
'category' => 'packaging',
|
||||
'weight_grams' => 40,
|
||||
]);
|
||||
$b = PackagingItem::factory()->create([
|
||||
'packaging_material_id' => $material->id,
|
||||
'supplier_id' => $supplier->id,
|
||||
'name' => 'Etikett',
|
||||
'category' => 'label',
|
||||
'weight_grams' => 1.5,
|
||||
]);
|
||||
|
||||
$repo = new ProductRepository($product);
|
||||
$repo->update([
|
||||
'id' => (string) $product->id,
|
||||
'name' => $product->name,
|
||||
'title' => $product->title,
|
||||
'active' => '1',
|
||||
'product_inci_sync_sent' => '1',
|
||||
'pp_packaging_item_id' => [(string) $b->id, (string) $a->id],
|
||||
'pp_quantity' => ['2', '1,5'],
|
||||
]);
|
||||
|
||||
$product->refresh();
|
||||
$rows = $product->packagings()->orderByPivot('pos')->get();
|
||||
expect($rows)->toHaveCount(2);
|
||||
expect($rows[0]->id)->toBe($b->id);
|
||||
expect((float) $rows[0]->pivot->quantity)->toBe(2.0);
|
||||
expect((int) $rows[0]->pivot->pos)->toBe(0);
|
||||
expect($rows[1]->id)->toBe($a->id);
|
||||
expect((float) $rows[1]->pivot->quantity)->toBe(1.5);
|
||||
expect((int) $rows[1]->pivot->pos)->toBe(1);
|
||||
});
|
||||
|
||||
test('product copy preserves packaging bom', function () {
|
||||
$product = Product::create([
|
||||
'name' => 'Serum',
|
||||
'title' => 'Serum',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$material = PackagingMaterial::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
$pk = PackagingItem::factory()->create([
|
||||
'packaging_material_id' => $material->id,
|
||||
'supplier_id' => $supplier->id,
|
||||
'name' => 'Pumpflasche',
|
||||
'category' => 'packaging',
|
||||
]);
|
||||
|
||||
$product->packagings()->sync([
|
||||
$pk->id => ['quantity' => 3,
|
||||
'pos' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$repo = new ProductRepository(new Product);
|
||||
$copy = $repo->copy($product->fresh());
|
||||
|
||||
$copy->load('packagings');
|
||||
expect($copy->packagings)->toHaveCount(1);
|
||||
expect($copy->packagings->first()->id)->toBe($pk->id);
|
||||
expect((float) $copy->packagings->first()->pivot->quantity)->toBe(3.0);
|
||||
});
|
||||
345
tests/Feature/ProductionPhase5Test.php
Normal file
345
tests/Feature/ProductionPhase5Test.php
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\Location;
|
||||
use App\Models\PackagingItem;
|
||||
use App\Models\PackagingMaterial;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductIngredient;
|
||||
use App\Models\StockEntry;
|
||||
use App\Models\Supplier;
|
||||
use App\Services\ProductionService;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function phase5MakeUser(int $adminLevel = 1): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('p5_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
function phase5EnsureCountry(): Country
|
||||
{
|
||||
return Country::query()->firstOrCreate(
|
||||
['code' => 'DE'],
|
||||
[
|
||||
'phone' => '00',
|
||||
'en' => 'Germany',
|
||||
'de' => 'Deutschland',
|
||||
'es' => 'Germany',
|
||||
'fr' => 'Germany',
|
||||
'it' => 'Germany',
|
||||
'ru' => 'Germany',
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
test('production service stores ingredients and packaging snapshot', function () {
|
||||
phase5EnsureCountry();
|
||||
$user = phase5MakeUser(7);
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$ing = Ingredient::query()->create([
|
||||
'name' => 'P5-Rohstoff',
|
||||
'trans_name' => '',
|
||||
'inci' => 'INCI',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
$product = Product::query()->create([
|
||||
'name' => 'P5-Produkt',
|
||||
'title' => 'P5-Produkt',
|
||||
'active' => true,
|
||||
'shelf_life_type' => null,
|
||||
]);
|
||||
|
||||
ProductIngredient::query()->create([
|
||||
'product_id' => $product->id,
|
||||
'ingredient_id' => $ing->id,
|
||||
'pos' => 0,
|
||||
'gram' => 10,
|
||||
'factor' => 1.0,
|
||||
]);
|
||||
|
||||
$material = PackagingMaterial::factory()->create();
|
||||
$pk = PackagingItem::factory()->create([
|
||||
'packaging_material_id' => $material->id,
|
||||
'supplier_id' => $supplier->id,
|
||||
'name' => 'P5-Flasche',
|
||||
'category' => 'packaging',
|
||||
]);
|
||||
$product->packagings()->sync([
|
||||
$pk->id => ['quantity' => 2, 'pos' => 0],
|
||||
]);
|
||||
|
||||
$stock = StockEntry::query()->create([
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => $ing->id,
|
||||
'packaging_item_id' => null,
|
||||
'supplier_id' => $supplier->id,
|
||||
'location_id' => $location->id,
|
||||
'unit' => 'gram',
|
||||
'ordered_by' => $user->id,
|
||||
'ordered_at' => '2026-01-01',
|
||||
'ordered_quantity' => 10000,
|
||||
'price_per_kg' => 1,
|
||||
'status' => 'received',
|
||||
'received_by' => $user->id,
|
||||
'received_at' => '2026-01-15',
|
||||
'received_quantity' => 10000,
|
||||
'batch_number' => 'B-P5',
|
||||
'best_before' => '2030-12-31',
|
||||
'quality_id' => null,
|
||||
]);
|
||||
|
||||
$service = app(ProductionService::class);
|
||||
$production = $service->store(
|
||||
[
|
||||
'product_id' => $product->id,
|
||||
'location_id' => $location->id,
|
||||
'produced_at' => '2026-03-01',
|
||||
'quantity' => 5,
|
||||
'notes' => 'test',
|
||||
],
|
||||
[
|
||||
[
|
||||
'ingredient_id' => $ing->id,
|
||||
'stock_entry_id' => $stock->id,
|
||||
'quantity_used' => '50',
|
||||
],
|
||||
],
|
||||
$user->id
|
||||
);
|
||||
|
||||
expect($production->productionIngredients)->toHaveCount(1);
|
||||
expect((float) $production->productionIngredients->first()->quantity_used)->toBe(50.0);
|
||||
expect($production->productionPackagings)->toHaveCount(1);
|
||||
expect($production->productionPackagings->first()->quantity_used)->toBe(10);
|
||||
});
|
||||
|
||||
test('production service rejects wrong gram sum', function () {
|
||||
phase5EnsureCountry();
|
||||
$user = phase5MakeUser(7);
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$ing = Ingredient::query()->create([
|
||||
'name' => 'P5-R2',
|
||||
'trans_name' => '',
|
||||
'inci' => 'X',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
$product = Product::query()->create([
|
||||
'name' => 'P5-P2',
|
||||
'title' => 'P5-P2',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
ProductIngredient::query()->create([
|
||||
'product_id' => $product->id,
|
||||
'ingredient_id' => $ing->id,
|
||||
'pos' => 0,
|
||||
'gram' => 10,
|
||||
'factor' => 1.0,
|
||||
]);
|
||||
|
||||
$stock = StockEntry::query()->create([
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => $ing->id,
|
||||
'packaging_item_id' => null,
|
||||
'supplier_id' => $supplier->id,
|
||||
'location_id' => $location->id,
|
||||
'unit' => 'gram',
|
||||
'ordered_by' => $user->id,
|
||||
'ordered_at' => '2026-01-01',
|
||||
'ordered_quantity' => 10000,
|
||||
'price_per_kg' => 1,
|
||||
'status' => 'received',
|
||||
'received_by' => $user->id,
|
||||
'received_at' => '2026-01-15',
|
||||
'received_quantity' => 10000,
|
||||
'batch_number' => 'B2',
|
||||
'best_before' => '2030-12-31',
|
||||
'quality_id' => null,
|
||||
]);
|
||||
|
||||
$service = app(ProductionService::class);
|
||||
|
||||
expect(fn () => $service->store(
|
||||
[
|
||||
'product_id' => $product->id,
|
||||
'location_id' => $location->id,
|
||||
'produced_at' => '2026-03-01',
|
||||
'quantity' => 5,
|
||||
'notes' => null,
|
||||
],
|
||||
[
|
||||
[
|
||||
'ingredient_id' => $ing->id,
|
||||
'stock_entry_id' => $stock->id,
|
||||
'quantity_used' => '100',
|
||||
],
|
||||
],
|
||||
$user->id
|
||||
))->toThrow(ValidationException::class);
|
||||
});
|
||||
|
||||
test('production api recipe json returns ingredients and packagings', function () {
|
||||
phase5EnsureCountry();
|
||||
$user = phase5MakeUser(1);
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$ing = Ingredient::query()->create([
|
||||
'name' => 'P5-API',
|
||||
'trans_name' => '',
|
||||
'inci' => 'Y',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
$product = Product::query()->create([
|
||||
'name' => 'P5-API-P',
|
||||
'title' => 'P5-API-P',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
ProductIngredient::query()->create([
|
||||
'product_id' => $product->id,
|
||||
'ingredient_id' => $ing->id,
|
||||
'pos' => 0,
|
||||
'gram' => 2,
|
||||
'factor' => 1.5,
|
||||
]);
|
||||
|
||||
StockEntry::query()->create([
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => $ing->id,
|
||||
'packaging_item_id' => null,
|
||||
'supplier_id' => $supplier->id,
|
||||
'location_id' => $location->id,
|
||||
'unit' => 'gram',
|
||||
'ordered_by' => $user->id,
|
||||
'ordered_at' => '2026-01-01',
|
||||
'ordered_quantity' => 5000,
|
||||
'price_per_kg' => 1,
|
||||
'status' => 'received',
|
||||
'received_by' => $user->id,
|
||||
'received_at' => '2026-02-01',
|
||||
'received_quantity' => 5000,
|
||||
'batch_number' => 'API-1',
|
||||
'best_before' => '2028-06-01',
|
||||
'quality_id' => null,
|
||||
]);
|
||||
|
||||
$this->actingAs($user, 'user');
|
||||
$response = $this->getJson(route('admin.inventory.api.products.recipe', ['product' => $product->id]).'?location_id='.$location->id.'&quantity=10');
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonPath('product.id', $product->id);
|
||||
expect((float) $response->json('ingredients.0.required_grams_total'))->toBe(30.0);
|
||||
});
|
||||
|
||||
test('production mhd warning when stock expires before product end', function () {
|
||||
phase5EnsureCountry();
|
||||
$user = phase5MakeUser(7);
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$ing = Ingredient::query()->create([
|
||||
'name' => 'P5-MHD',
|
||||
'trans_name' => '',
|
||||
'inci' => 'Z',
|
||||
'trans_inci' => '',
|
||||
'effect' => '',
|
||||
'trans_effect' => '',
|
||||
'active' => true,
|
||||
'pos' => 0,
|
||||
]);
|
||||
|
||||
$product = Product::query()->create([
|
||||
'name' => 'P5-MHD-P',
|
||||
'title' => 'P5-MHD-P',
|
||||
'active' => true,
|
||||
'shelf_life_type' => 'fixed',
|
||||
'shelf_life_months' => 24,
|
||||
]);
|
||||
|
||||
ProductIngredient::query()->create([
|
||||
'product_id' => $product->id,
|
||||
'ingredient_id' => $ing->id,
|
||||
'pos' => 0,
|
||||
'gram' => 1,
|
||||
'factor' => 1.0,
|
||||
]);
|
||||
|
||||
$stock = StockEntry::query()->create([
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => $ing->id,
|
||||
'packaging_item_id' => null,
|
||||
'supplier_id' => $supplier->id,
|
||||
'location_id' => $location->id,
|
||||
'unit' => 'gram',
|
||||
'ordered_by' => $user->id,
|
||||
'ordered_at' => '2026-01-01',
|
||||
'ordered_quantity' => 5000,
|
||||
'price_per_kg' => 1,
|
||||
'status' => 'received',
|
||||
'received_by' => $user->id,
|
||||
'received_at' => '2026-02-01',
|
||||
'received_quantity' => 5000,
|
||||
'batch_number' => 'MHD-B',
|
||||
'best_before' => '2026-06-01',
|
||||
'quality_id' => null,
|
||||
]);
|
||||
|
||||
$service = app(ProductionService::class);
|
||||
$production = $service->store(
|
||||
[
|
||||
'product_id' => $product->id,
|
||||
'location_id' => $location->id,
|
||||
'produced_at' => '2026-03-01',
|
||||
'quantity' => 1,
|
||||
'notes' => null,
|
||||
],
|
||||
[
|
||||
[
|
||||
'ingredient_id' => $ing->id,
|
||||
'stock_entry_id' => $stock->id,
|
||||
'quantity_used' => '1',
|
||||
],
|
||||
],
|
||||
$user->id
|
||||
);
|
||||
|
||||
expect($production->mhd_warning)->toBeTrue();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue