From b8ba2db7a5668f650dee36c07aa58b4136886700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Mon, 12 Oct 2026 21:47:12 +0200 Subject: [PATCH] Fix occurrence MCP schema and expose product sales --- .../market_management_hermes_mcp_api.md | 9 +++-- modules/market_management/mcp.php | 2 +- modules/market_management/service.php | 39 +++++++++++++++++++ .../market_management_mcp_tools_list_test.php | 5 +++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md index eca0b1d..25cbcf9 100644 --- a/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md +++ b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md @@ -173,10 +173,11 @@ Kategorien anlegen oder ändern. - erforderlich: `market_definition_id`, `days` mit mindestens einem Markttag - initialer Status: `open` - weitere Marktdurchführungsfelder sind optional und werden typisiert validiert + - legt automatisch eine Standard-Checkliste mit den vier festen Kapiteln an - `market_management.get_occurrence` - erforderlich: `occurrence_id` - liefert zusätzlich den Markt, Markttage, Kosten und - berechnete Tages- und Gesamterlöse + berechnete Tages- und Gesamterlöse sowie `sales_items` je Markttag - `market_management.get_occurrence_checklist` - erforderlich: `occurrence_id` - liefert die vier festen Kapitel, die Punkte mit `chapter_key` und den @@ -323,12 +324,14 @@ Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`, - Hermes darf keine Kategorien anlegen. - Umsatz und Artikelmengen werden nicht über MCP geschrieben; sie werden aus den OTC-Verkäufen abgeleitet. +- `get_occurrence` liefert `sales_items` mit `day_id`, `date`, `product_id`, + `sku`, `product_name`, `quantity`, `gross_revenue` und `unit_price`. - Markt und Marktdurchführung werden unabhängig archiviert. - Alle Schreibvorgänge werden serverseitig validiert. - Jeder schreibende MCP-Aufruf benötigt `idempotency_key`; optional kann `client_id` mitgegeben werden. Wiederholungen liefern dasselbe Ergebnis. -- Umsatz- und Artikelmengen aus OTC-Verkäufen sind erst verfügbar, sobald die - separate OTC-Schnittstelle die Zuordnung zum Markttag bereitstellt. +- Die gelieferten Verkaufspositionen stammen ausschließlich aus OTC-Verkäufen, + die einem konkreten Markttag zugeordnet sind. ## Fehler diff --git a/modules/market_management/mcp.php b/modules/market_management/mcp.php index 1f78720..b22b408 100644 --- a/modules/market_management/mcp.php +++ b/modules/market_management/mcp.php @@ -85,7 +85,7 @@ function market_management_mcp_tools(): array ['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]], ['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string']]]], ['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], - ['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], 'properties' => ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string'], 'days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]] + $occurrenceProperties]]], + ['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], 'properties' => ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string']] + $occurrenceProperties + ['days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]]]]], ['name' => 'market_management.get_occurrence', 'description' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]], ['name' => 'market_management.get_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]], ['name' => 'market_management.update_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung vollständig ersetzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'items'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'comment' => ['type' => ['string', 'null']], 'items' => ['type' => 'array', 'items' => ['type' => 'object', 'required' => ['chapter_key', 'label', 'is_completed'], 'properties' => ['chapter_key' => ['type' => 'string', 'enum' => ['products_sales', 'decoration', 'stand_equipment', 'technology']], 'label' => ['type' => 'string'], 'is_completed' => ['type' => 'boolean']]]]]]], diff --git a/modules/market_management/service.php b/modules/market_management/service.php index 5d7dd8f..23ff2d5 100644 --- a/modules/market_management/service.php +++ b/modules/market_management/service.php @@ -1040,6 +1040,45 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc $occurrenceSalesCount += (int) $salesRow['sales_count']; } unset($day); + $salesItemsStmt = $pdo->prepare( + 'SELECT so.market_occurrence_day_id AS day_id, + d.date, + p.id AS product_id, + p.sku, + p.name AS product_name, + SUM(GREATEST(sol.qty - sol.qty_cancelled, 0) * sic.qty_per_item) AS quantity, + SUM(CASE WHEN sol.line_status = \'cancelled\' OR sol.line_total IS NULL THEN 0 + ELSE sol.line_total * GREATEST(sol.qty - sol.qty_cancelled, 0) / NULLIF(sol.qty, 0) END) AS gross_revenue + FROM sales_order so + INNER JOIN sales_order_line sol ON sol.sales_order_id = so.id + INNER JOIN sellable_item_component sic ON sic.sellable_item_id = sol.sellable_item_id + INNER JOIN product p ON p.id = sic.product_id + INNER JOIN market_occurrence_day d ON d.id = so.market_occurrence_day_id + WHERE so.market_occurrence_id = :occurrence_id + AND so.market_occurrence_day_id IS NOT NULL + AND so.order_source = \'direct\' + AND so.order_status <> \'cancelled\' + GROUP BY so.market_occurrence_day_id, d.date, p.id, p.sku, p.name + ORDER BY d.date ASC, p.sku ASC, p.id ASC' + ); + $salesItemsStmt->execute([':occurrence_id' => $occurrenceId]); + $occurrence['sales_items'] = array_map( + static function (array $item): array { + $quantity = (float) $item['quantity']; + $grossRevenue = (float) $item['gross_revenue']; + return [ + 'day_id' => (int) $item['day_id'], + 'date' => (string) $item['date'], + 'product_id' => (int) $item['product_id'], + 'sku' => (string) $item['sku'], + 'product_name' => (string) $item['product_name'], + 'quantity' => $quantity, + 'gross_revenue' => $grossRevenue, + 'unit_price' => $quantity > 0 ? $grossRevenue / $quantity : null, + ]; + }, + $salesItemsStmt->fetchAll() + ); $occurrence['sales_summary'] = $occurrenceSalesCount === 0 ? [ 'sales_count' => 0, diff --git a/scripts/tests/market_management_mcp_tools_list_test.php b/scripts/tests/market_management_mcp_tools_list_test.php index 6f653ef..8b74e5a 100644 --- a/scripts/tests/market_management_mcp_tools_list_test.php +++ b/scripts/tests/market_management_mcp_tools_list_test.php @@ -74,6 +74,11 @@ assert_test(count($tools) === 23, 'Expected 23 MCP tools, got ' . count($tools)) $occurrenceTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === 'market_management.update_occurrence'))[0] ?? null; assert_test(is_array($occurrenceTool), 'Update occurrence MCP tool must exist'); assert_test(isset($occurrenceTool['inputSchema']['properties']['registration_note']), 'MCP occurrence schema must expose registration_note'); +$createOccurrenceTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === 'market_management.create_occurrence'))[0] ?? null; +assert_test(is_array($createOccurrenceTool), 'Create occurrence MCP tool must exist'); +$createProperties = $createOccurrenceTool['inputSchema']['properties']; +assert_test(isset($createProperties['place'], $createProperties['days']['items']['properties']['date']), 'Create occurrence schema must expose both levels'); +assert_test(!isset($createProperties['days']['items']['properties']['place']), 'Occurrence fields must not be nested under days'); $actual = []; foreach ($tools as $tool) {