gruene-seele/tests/Feature/IngredientDensityTest.php
Kevin Adametz fa852340ee Warenwirtschaft: AP-42 — Rohstoff-Dichte (g/ml) für ml-Erfassung im Wareneingang
Kundenentscheid 26.08.: Rohstoffe werden in Gramm geführt, dürfen aber in ml
erfasst werden; über die Dichte ergeben sich die Gramm (100 ml Propolistinktur
× 0,79 = 79 g).

- Neues Feld ingredients.density (g/ml, optional) inkl. Feld im INCI-Formular.
- Wareneingang: Einheiten-Umschalter g/ml am Mengenfeld (nur wenn Dichte
  hinterlegt; Live-Hinweis mit Gramm-Umrechnung, Dichte kommt aus der
  Rohstoff-Suche mit). Erfassung in ml wird über die Dichte in Gramm
  umgerechnet und kanonisch als Gramm gespeichert — InventoryService und alle
  Aggregationen bleiben unverändert. purchase_unit + density_used (Snapshot)
  protokollieren die ml-Erfassung für Anzeige/Bearbeitung; Detailseite zeigt
  „790 g (= 1.000 ml)". Wareneingang-Buchung rechnet gelieferte ml ebenfalls um.
- Serverseitige Validierung: ml nur mit hinterlegter Dichte.

Migrationen add_density_* auf Dev-/Live-DB. 4 Tests, Suite 318 grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-28 09:05:07 +00:00

128 lines
4.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Models\Ingredient;
use App\Models\Location;
use App\Models\MaterialQuality;
use App\Models\StockEntry;
use App\Models\Supplier;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
/**
* AP-42: Rohstoffe werden in Gramm geführt, dürfen aber in ml erfasst werden.
* Über die Dichte (g/ml) ergeben sich die Gramm (100 ml Propolis × 0,79 = 79 g).
*/
function densAdmin(): User
{
$user = User::query()->create(['email' => uniqid('dens_', true).'@test.example', 'password' => bcrypt('password')]);
$user->forceFill(['admin' => 7, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
return $user->fresh();
}
/**
* @return array{0: User, 1: Ingredient, 2: Location, 3: Supplier, 4: MaterialQuality}
*/
function densSetup(?float $density): array
{
$admin = densAdmin();
$ingredient = Ingredient::query()->create(['name' => 'INCI-'.uniqid('', true), 'active' => true, 'pos' => 0, 'density' => $density]);
return [$admin, $ingredient, Location::factory()->create(), Supplier::factory()->create(), MaterialQuality::factory()->create()];
}
test('AP-42: ml-erfassung wird über die dichte in gramm gespeichert', function () {
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(0.79);
$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-08-01',
'ordered_quantity' => '100',
'ordered_unit' => 'ml',
'price_per_kg' => '10,00',
])->assertRedirect(route('admin.inventory.stock-entries.index'));
$entry = StockEntry::query()->latest('id')->firstOrFail();
expect((float) $entry->ordered_quantity)->toBe(79.0)
->and($entry->purchase_unit)->toBe('ml')
->and((float) $entry->density_used)->toBe(0.79);
});
test('AP-42: gramm-erfassung bleibt unverändert', function () {
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(0.79);
$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-08-01',
'ordered_quantity' => '100',
'ordered_unit' => 'gram',
'price_per_kg' => '10,00',
])->assertRedirect();
$entry = StockEntry::query()->latest('id')->firstOrFail();
expect((float) $entry->ordered_quantity)->toBe(100.0)
->and($entry->purchase_unit)->toBe('gram')
->and($entry->density_used)->toBeNull();
});
test('AP-42: ml-erfassung ohne hinterlegte dichte wird abgelehnt', function () {
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(null);
$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-08-01',
'ordered_quantity' => '100',
'ordered_unit' => 'ml',
'price_per_kg' => '10,00',
])->assertSessionHasErrors('ordered_quantity');
expect(StockEntry::query()->count())->toBe(0);
});
test('AP-42: wareneingang in ml wird über die dichte in gramm gebucht', function () {
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(0.80);
$this->actingAs($admin, 'user');
// Bestellung in ml (250 ml × 0,80 = 200 g)
$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-08-01',
'ordered_quantity' => '250',
'ordered_unit' => 'ml',
'price_per_kg' => '10,00',
])->assertRedirect();
$entry = StockEntry::query()->latest('id')->firstOrFail();
// Wareneingang: 250 ml geliefert => 200 g gebucht
$this->put(route('admin.inventory.stock-entries.receive', $entry), [
'received_at' => '2026-08-05',
'received_quantity' => '250',
'batch_number' => 'DENS-1',
'best_before' => '2030-12-31',
])->assertRedirect();
expect((float) $entry->fresh()->received_quantity)->toBe(200.0);
});