create([ 'email' => uniqid('sep_', true).'@test.example', 'password' => bcrypt('password'), ]); $user->forceFill([ 'admin' => 7, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false, ])->save(); return $user->fresh(); } function stockPriceIngredient(): Ingredient { return Ingredient::query()->create(['name' => 'INCI-'.uniqid('', true), 'active' => true, 'pos' => 0]); } test('net price derives gross from tax rate on store', function () { $admin = stockPriceAdmin(); $ingredient = stockPriceIngredient(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $quality = MaterialQuality::factory()->create(); $tax = TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]); $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, 'quality_id' => (string) $quality->id, 'tax_rate_id' => (string) $tax->id, 'ordered_at' => '2026-06-01', 'ordered_quantity' => '1000', 'price_per_kg' => '10,00', ])->assertRedirect(route('admin.inventory.stock-entries.index')); $entry = StockEntry::query()->latest('id')->firstOrFail(); expect((float) $entry->price_per_kg)->toBe(10.0) ->and((float) $entry->price_per_kg_gross)->toBe(11.9) ->and((float) $entry->tax_rate_percent)->toBe(19.0); }); test('gross price derives net from tax rate on store', function () { $admin = stockPriceAdmin(); $ingredient = stockPriceIngredient(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $quality = MaterialQuality::factory()->create(); $tax = TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]); $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, 'quality_id' => (string) $quality->id, 'tax_rate_id' => (string) $tax->id, 'ordered_at' => '2026-06-01', 'ordered_quantity' => '1000', 'price_per_kg_gross' => '11,90', ])->assertRedirect(route('admin.inventory.stock-entries.index')); $entry = StockEntry::query()->latest('id')->firstOrFail(); expect((float) $entry->price_per_kg)->toBe(10.0) ->and((float) $entry->price_per_kg_gross)->toBe(11.9); }); test('without tax rate net equals gross', function () { $admin = stockPriceAdmin(); $ingredient = stockPriceIngredient(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $quality = MaterialQuality::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, 'quality_id' => (string) $quality->id, 'ordered_at' => '2026-06-01', 'ordered_quantity' => '500', 'price_per_kg' => '8,00', ])->assertRedirect(route('admin.inventory.stock-entries.index')); $entry = StockEntry::query()->latest('id')->firstOrFail(); expect((float) $entry->price_per_kg)->toBe(8.0) ->and((float) $entry->price_per_kg_gross)->toBe(8.0) ->and($entry->tax_rate_percent)->toBeNull(); }); test('ingredient requires net or gross price', function () { $admin = stockPriceAdmin(); $ingredient = stockPriceIngredient(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $quality = MaterialQuality::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, 'quality_id' => (string) $quality->id, 'ordered_at' => '2026-06-01', 'ordered_quantity' => '500', ])->assertSessionHasErrors('price_per_kg'); }); test('copy creates pending duplicate without receiving data', function () { $admin = stockPriceAdmin(); $ingredient = stockPriceIngredient(); $location = Location::factory()->create(); $supplier = Supplier::factory()->create(); $quality = MaterialQuality::factory()->create(); $tax = TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]); $source = StockEntry::query()->create([ 'entry_type' => 'ingredient', 'ingredient_id' => $ingredient->id, 'supplier_id' => $supplier->id, 'location_id' => $location->id, 'unit' => 'gram', 'ordered_by' => $admin->id, 'ordered_at' => '2026-01-01', 'ordered_quantity' => 1000, 'price_per_kg' => 10, 'price_per_kg_gross' => 11.9, 'tax_rate_id' => $tax->id, 'tax_rate_percent' => 19, 'quality_id' => $quality->id, 'received_at' => '2026-01-10', 'received_quantity' => 1000, 'batch_number' => 'CHARGE-1', 'best_before' => '2027-01-01', 'status' => 'received', ]); $this->actingAs($admin, 'user'); $this->get(route('admin.inventory.stock-entries.copy', $source)) ->assertRedirect(); $copy = StockEntry::query()->where('status', 'pending')->latest('id')->firstOrFail(); expect($copy->id)->not->toBe($source->id) ->and($copy->ingredient_id)->toBe($ingredient->id) ->and($copy->supplier_id)->toBe($supplier->id) ->and((float) $copy->price_per_kg)->toBe(10.0) ->and((float) $copy->price_per_kg_gross)->toBe(11.9) ->and($copy->quality_id)->toBe($quality->id) ->and($copy->batch_number)->toBeNull() ->and($copy->best_before)->toBeNull() ->and($copy->received_quantity)->toBeNull() ->and($copy->ordered_by)->toBe($admin->id); }); test('copy is blocked for non admin', function () { $admin = stockPriceAdmin(); $source = StockEntry::factory()->create(['ordered_by' => $admin->id]); $copyreader = User::query()->create([ 'email' => uniqid('cr_', true).'@test.example', 'password' => bcrypt('password'), ]); $copyreader->forceFill(['admin' => 1, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save(); $this->actingAs($copyreader->fresh(), 'user'); $this->get(route('admin.inventory.stock-entries.copy', $source)) ->assertRedirect(route('home')); });