mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:09:35 +01:00
feat(inventory): Inventur durchführen – Ist-Mengen erfassen (US-6.2)
Implementiert startCounting() und updateCountItem() auf dem InventoryCount-
Aggregate, zwei neue Use Cases (StartInventoryCount, RecordCountItem) mit
zugehörigen Controller-Endpoints (PATCH /{id}/start, PATCH /{id}/items/{itemId}).
Inkl. Domain-, Application-, Integrations- und Gatling-Lasttests.
This commit is contained in:
parent
206921d2a6
commit
252f48d52b
17 changed files with 1451 additions and 17 deletions
|
|
@ -44,6 +44,7 @@ public final class LoadTestDataSeeder {
|
|||
private final List<String> stockIds = new ArrayList<>();
|
||||
private final List<String> stockBatchIds = new ArrayList<>();
|
||||
private final List<String> stockMovementIds = new ArrayList<>();
|
||||
private final List<String> inventoryCountIds = new ArrayList<>();
|
||||
|
||||
// Statische Felder für Zugriff aus Szenarien
|
||||
private static List<String> seededCategoryIds;
|
||||
|
|
@ -57,6 +58,7 @@ public final class LoadTestDataSeeder {
|
|||
private static List<String> seededStockIds;
|
||||
private static List<String> seededStockBatchIds;
|
||||
private static List<String> seededStockMovementIds;
|
||||
private static List<String> seededInventoryCountIds;
|
||||
|
||||
public LoadTestDataSeeder(ConfigurableApplicationContext appContext) {
|
||||
int port = appContext.getEnvironment()
|
||||
|
|
@ -80,6 +82,7 @@ public final class LoadTestDataSeeder {
|
|||
seedProductionOrders();
|
||||
seedStocksAndBatches();
|
||||
seedStockMovements();
|
||||
seedInventoryCounts();
|
||||
|
||||
// Statische Referenzen setzen
|
||||
seededCategoryIds = List.copyOf(categoryIds);
|
||||
|
|
@ -93,16 +96,17 @@ public final class LoadTestDataSeeder {
|
|||
seededStockIds = List.copyOf(stockIds);
|
||||
seededStockBatchIds = List.copyOf(stockBatchIds);
|
||||
seededStockMovementIds = List.copyOf(stockMovementIds);
|
||||
seededInventoryCountIds = List.copyOf(inventoryCountIds);
|
||||
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
System.out.printf(
|
||||
"Seeded in %dms: %d Kategorien, %d Artikel, %d Lieferanten, %d Kunden, "
|
||||
+ "%d Lagerorte, %d Rezepte, %d Chargen, %d Produktionsaufträge, "
|
||||
+ "%d Bestände, %d Bestandsbewegungen%n",
|
||||
+ "%d Bestände, %d Bestandsbewegungen, %d Inventuren%n",
|
||||
duration, categoryIds.size(), articleIds.size(), supplierIds.size(),
|
||||
customerIds.size(), storageLocationIds.size(), recipeIds.size(),
|
||||
batchIds.size(), productionOrderIds.size(),
|
||||
stockIds.size(), stockMovementIds.size());
|
||||
stockIds.size(), stockMovementIds.size(), inventoryCountIds.size());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Testdaten-Seeding fehlgeschlagen", e);
|
||||
}
|
||||
|
|
@ -649,6 +653,33 @@ public final class LoadTestDataSeeder {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- Inventuren (3 pro Lagerort mit Stocks, davon 2 gestartet) ----
|
||||
|
||||
private void seedInventoryCounts() throws Exception {
|
||||
if (storageLocationIds.isEmpty() || stockIds.isEmpty()) return;
|
||||
|
||||
// Nur Lagerorte nutzen, die auch Stocks haben (maximal 3 Lagerorte)
|
||||
int countLimit = Math.min(3, storageLocationIds.size());
|
||||
for (int i = 0; i < countLimit; i++) {
|
||||
String locationId = storageLocationIds.get(i);
|
||||
|
||||
String body = """
|
||||
{"storageLocationId":"%s","countDate":"%s"}"""
|
||||
.formatted(locationId, LocalDate.now());
|
||||
|
||||
try {
|
||||
var json = mapper.readTree(post("/api/inventory/inventory-counts", body, adminToken));
|
||||
String countId = json.get("id").asText();
|
||||
inventoryCountIds.add(countId);
|
||||
|
||||
// 2 von 3 Inventuren starten → Status COUNTING
|
||||
if (i < 2) {
|
||||
tryPatch("/api/inventory/inventory-counts/" + countId + "/start", adminToken);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- HTTP Helper ----
|
||||
|
||||
private String post(String path, String jsonBody, String token) throws Exception {
|
||||
|
|
@ -676,6 +707,31 @@ public final class LoadTestDataSeeder {
|
|||
}
|
||||
}
|
||||
|
||||
/** Patch ohne Exception bei Fehler – für optionale Status-Übergänge. */
|
||||
private void tryPatch(String path, String token) {
|
||||
try {
|
||||
patch(path, token);
|
||||
} catch (Exception ignored) {
|
||||
// Status-Übergang fehlgeschlagen, z.B. wegen Vorbedingungen
|
||||
}
|
||||
}
|
||||
|
||||
private String patch(String path, String token) throws Exception {
|
||||
var builder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + path))
|
||||
.header("Content-Type", "application/json")
|
||||
.method("PATCH", HttpRequest.BodyPublishers.ofString("{}"));
|
||||
if (token != null) {
|
||||
builder.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
var response = http.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() >= 400) {
|
||||
throw new RuntimeException("HTTP %d auf %s: %s".formatted(
|
||||
response.statusCode(), path, response.body()));
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
// ---- Statische Getter für Szenarien ----
|
||||
|
||||
public static List<String> categoryIds() { return seededCategoryIds; }
|
||||
|
|
@ -689,4 +745,5 @@ public final class LoadTestDataSeeder {
|
|||
public static List<String> stockIds() { return seededStockIds; }
|
||||
public static List<String> stockBatchIds() { return seededStockBatchIds; }
|
||||
public static List<String> stockMovementIds() { return seededStockMovementIds; }
|
||||
public static List<String> inventoryCountIds() { return seededInventoryCountIds; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,84 @@ public final class InventoryScenario {
|
|||
);
|
||||
}
|
||||
|
||||
public static ChainBuilder listInventoryCounts() {
|
||||
return exec(
|
||||
http("Inventuren auflisten")
|
||||
.get("/api/inventory/inventory-counts")
|
||||
.header("Authorization", "Bearer #{accessToken}")
|
||||
.check(status().is(200))
|
||||
);
|
||||
}
|
||||
|
||||
public static ChainBuilder getRandomInventoryCount() {
|
||||
return exec(session -> {
|
||||
var ids = LoadTestDataSeeder.inventoryCountIds();
|
||||
if (ids == null || ids.isEmpty()) return session;
|
||||
String id = ids.get(ThreadLocalRandom.current().nextInt(ids.size()));
|
||||
return session.set("inventoryCountId", id);
|
||||
}).doIf(session -> session.contains("inventoryCountId")).then(
|
||||
exec(
|
||||
http("Inventur laden")
|
||||
.get("/api/inventory/inventory-counts/#{inventoryCountId}")
|
||||
.header("Authorization", "Bearer #{accessToken}")
|
||||
.check(status().in(200, 404))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static ChainBuilder createAndStartInventoryCount() {
|
||||
return exec(session -> {
|
||||
var ids = LoadTestDataSeeder.storageLocationIds();
|
||||
String id = ids.get(ThreadLocalRandom.current().nextInt(ids.size()));
|
||||
return session.set("icStorageLocationId", id);
|
||||
}).exec(
|
||||
http("Inventur anlegen")
|
||||
.post("/api/inventory/inventory-counts")
|
||||
.header("Authorization", "Bearer #{accessToken}")
|
||||
.body(StringBody("""
|
||||
{"storageLocationId":"#{icStorageLocationId}","countDate":"%s"}"""
|
||||
.formatted(java.time.LocalDate.now())))
|
||||
.check(status().in(201, 409))
|
||||
.check(jsonPath("$.id").optional().saveAs("newInventoryCountId"))
|
||||
).doIf(session -> session.contains("newInventoryCountId")).then(
|
||||
exec(
|
||||
http("Inventur starten")
|
||||
.patch("/api/inventory/inventory-counts/#{newInventoryCountId}/start")
|
||||
.header("Authorization", "Bearer #{accessToken}")
|
||||
.check(status().in(200, 400))
|
||||
).exec(session -> session.remove("newInventoryCountId"))
|
||||
);
|
||||
}
|
||||
|
||||
public static ChainBuilder recordCountItem() {
|
||||
return exec(session -> {
|
||||
var ids = LoadTestDataSeeder.inventoryCountIds();
|
||||
if (ids == null || ids.isEmpty()) return session;
|
||||
String id = ids.get(ThreadLocalRandom.current().nextInt(ids.size()));
|
||||
return session.set("recordCountId", id);
|
||||
}).doIf(session -> session.contains("recordCountId")).then(
|
||||
exec(
|
||||
http("Inventur laden für Zählung")
|
||||
.get("/api/inventory/inventory-counts/#{recordCountId}")
|
||||
.header("Authorization", "Bearer #{accessToken}")
|
||||
.check(status().in(200, 404))
|
||||
.check(jsonPath("$.countItems[0].id").optional().saveAs("recordItemId"))
|
||||
).doIf(session -> session.contains("recordItemId")).then(
|
||||
exec(session -> {
|
||||
int qty = ThreadLocalRandom.current().nextInt(0, 50);
|
||||
return session.set("recordQty", "%d.0".formatted(qty));
|
||||
}).exec(
|
||||
http("Ist-Menge erfassen")
|
||||
.patch("/api/inventory/inventory-counts/#{recordCountId}/items/#{recordItemId}")
|
||||
.header("Authorization", "Bearer #{accessToken}")
|
||||
.body(StringBody("""
|
||||
{"actualQuantityAmount":"#{recordQty}","actualQuantityUnit":"KILOGRAM"}"""))
|
||||
.check(status().in(200, 400))
|
||||
).exec(session -> session.remove("recordItemId"))
|
||||
).exec(session -> session.remove("recordCountId"))
|
||||
);
|
||||
}
|
||||
|
||||
public static ChainBuilder recordStockMovement() {
|
||||
return exec(session -> {
|
||||
var rnd = ThreadLocalRandom.current();
|
||||
|
|
@ -189,17 +267,21 @@ public final class InventoryScenario {
|
|||
.exec(AuthenticationScenario.login("admin", "admin123"))
|
||||
.repeat(15).on(
|
||||
randomSwitch().on(
|
||||
percent(18.0).then(listStocks()),
|
||||
percent(12.0).then(listStorageLocations()),
|
||||
percent(12.0).then(getRandomStorageLocation()),
|
||||
percent(12.0).then(listStocksByLocation()),
|
||||
percent(8.0).then(listStocksBelowMinimum()),
|
||||
percent(8.0).then(listStockMovements()),
|
||||
percent(5.0).then(listStockMovementsByStock()),
|
||||
percent(5.0).then(listStockMovementsByBatch()),
|
||||
percent(5.0).then(listStockMovementsByDateRange()),
|
||||
percent(8.0).then(reserveAndConfirmStock()),
|
||||
percent(7.0).then(recordStockMovement())
|
||||
percent(15.0).then(listStocks()),
|
||||
percent(10.0).then(listStorageLocations()),
|
||||
percent(10.0).then(getRandomStorageLocation()),
|
||||
percent(10.0).then(listStocksByLocation()),
|
||||
percent(6.0).then(listStocksBelowMinimum()),
|
||||
percent(6.0).then(listStockMovements()),
|
||||
percent(4.0).then(listStockMovementsByStock()),
|
||||
percent(4.0).then(listStockMovementsByBatch()),
|
||||
percent(4.0).then(listStockMovementsByDateRange()),
|
||||
percent(7.0).then(reserveAndConfirmStock()),
|
||||
percent(6.0).then(recordStockMovement()),
|
||||
percent(5.0).then(listInventoryCounts()),
|
||||
percent(5.0).then(getRandomInventoryCount()),
|
||||
percent(4.0).then(createAndStartInventoryCount()),
|
||||
percent(4.0).then(recordCountItem())
|
||||
).pause(1, 3)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ public class FullWorkloadSimulation extends Simulation {
|
|||
details("Bestandsbewegungen nach Bestand").responseTime().mean().lt(35),
|
||||
details("Bestandsbewegungen nach Charge").responseTime().mean().lt(35),
|
||||
details("Bestandsbewegungen nach Zeitraum").responseTime().mean().lt(35),
|
||||
details("Inventuren auflisten").responseTime().mean().lt(35),
|
||||
details("Inventur laden").responseTime().mean().lt(20),
|
||||
|
||||
// Listen mit viel Daten (50-300 Einträge): mean < 75ms
|
||||
details("Chargen auflisten").responseTime().mean().lt(75),
|
||||
|
|
@ -121,6 +123,9 @@ public class FullWorkloadSimulation extends Simulation {
|
|||
details("Produktionsauftrag stornieren").responseTime().mean().lt(50),
|
||||
details("Produktionsauftrag umterminieren").responseTime().mean().lt(50),
|
||||
details("Bestandsbewegung erfassen").responseTime().mean().lt(50),
|
||||
details("Inventur anlegen").responseTime().mean().lt(50),
|
||||
details("Inventur starten").responseTime().mean().lt(50),
|
||||
details("Ist-Menge erfassen").responseTime().mean().lt(50),
|
||||
|
||||
// Produktionsaufträge-Listen: mean < 35ms
|
||||
details("Produktionsaufträge auflisten").responseTime().mean().lt(35),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue