From 3267a121dec2ed3311ced5d10112aa7146e55e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sat, 19 Sep 2026 08:51:17 +0200 Subject: [PATCH] Erweitere Marktmanagement-MCP fuer Durchfuehrungen --- .../market_management_hermes_mcp_api.md | 216 ++++++++++++++++++ modules/market_management/mcp.php | 9 + modules/market_management/service.php | 63 +++++ 3 files changed, 288 insertions(+) create mode 100644 docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md 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 new file mode 100644 index 0000000..b973739 --- /dev/null +++ b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md @@ -0,0 +1,216 @@ +# Hermes: Market-Management-MCP-API + +Stand: 2026-09-19 +Status: Aktive technische Schnittstellendokumentation + +## Verbindung + +Hermes verwendet den MCP-Endpunkt der DEV-App: + +```text +https://erpnaurua.imhochrain.ch/api/market-management-mcp.php +``` + +Jeder Request ist ein JSON-RPC-2.0-Request über HTTPS. Der Request benötigt: + +```http +Authorization: Bearer +Content-Type: application/json +``` + +Der Token wird nicht in dieser Dokumentation oder im Repository gespeichert. + +## Verbindungsablauf + +### 1. Initialisieren + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26" + } +} +``` + +### 2. Werkzeuge laden + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} +} +``` + +### 3. Werkzeug aufrufen + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "market_management.list_categories", + "arguments": {} + } +} +``` + +Das Ergebnis steht bei `tools/call` als JSON im Textinhalt des ersten +`content`-Elements. + +## Verfügbare Werkzeuge + +### Marktdefinitionen + +- `market_management.search_markets` + - optional: `search`, `include_archived`, `limit` +- `market_management.get_market` + - erforderlich: `market_id` + - optional: `include_archived` +- `market_management.create_market` + - erforderlich: `name`, `category_id` +- `market_management.update_market` + - erforderlich: `market_id` + - änderbar: `name`, `category_id` +- `market_management.archive_market` + - erforderlich: `market_id` +- `market_management.reactivate_market` + - erforderlich: `market_id` + +### Kategorien + +- `market_management.list_categories` + +Hermes darf Kategorien lesen und bei einem Markt referenzieren, aber keine +Kategorien anlegen oder ändern. + +### Durchführungen + +- `market_management.create_occurrence` + - erforderlich: `market_definition_id` + - initialer Status: `open` + - weitere Durchführungsfelder sind optional und werden typisiert validiert +- `market_management.get_occurrence` + - erforderlich: `occurrence_id` +- `market_management.update_occurrence` + - erforderlich: `occurrence_id` +- `market_management.archive_occurrence` + - erforderlich: `occurrence_id` +- `market_management.reactivate_occurrence` + - erforderlich: `occurrence_id` + +Unterstützte Statuswerte: + +```text +open +waitlist +registered +confirmation_pending +confirmed +cancelled +completed +``` + +Unterstützte Zahlungsstatuswerte: + +```text +unpaid +paid +``` + +### Durchführungstage + +- `market_management.create_day` + - erforderlich: `occurrence_id`, `date` +- `market_management.update_day` + - erforderlich: `day_id` +- `market_management.delete_day` + - erforderlich: `day_id` + +Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`, +`setup_start_time`, `setup_end_time`, `delivery_start_time`, +`delivery_end_time` und `observations`. + +### Veranstalter und Kosten + +- `market_management.set_organizer` + - erforderlich: `occurrence_id`, `organization_contact_id`, + `contact_person_id` +- `market_management.create_cost_item` + - erforderlich: `occurrence_id`, `cost_type`, `gross_amount`, `tax_status` + - `tax_status`: `tax_free` oder `taxable` +- `market_management.delete_cost_item` + - erforderlich: `cost_item_id` + +## Beispiel: Markt anlegen + +```json +{ + "jsonrpc": "2.0", + "id": 10, + "method": "tools/call", + "params": { + "name": "market_management.create_market", + "arguments": { + "name": "Beispielmarkt Hermes Test", + "category_id": 1 + } + } +} +``` + +## Beispiel: Durchführung mit Tag anlegen + +```json +{ + "jsonrpc": "2.0", + "id": 11, + "method": "tools/call", + "params": { + "name": "market_management.create_occurrence", + "arguments": { + "market_definition_id": 1, + "status": "open", + "place": "Beispielort", + "registration_deadline": "2026-10-01" + } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 12, + "method": "tools/call", + "params": { + "name": "market_management.create_day", + "arguments": { + "occurrence_id": 1, + "date": "2026-11-21", + "stand_start_time": "09:00", + "stand_end_time": "18:00" + } + } +} +``` + +## Fachliche Grenzen + +- Hermes erhält keinen direkten Datenbankzugriff. +- Hermes darf keine Kategorien anlegen. +- Umsatz und Artikelmengen werden nicht über MCP geschrieben; sie werden aus + den OTC-Verkäufen abgeleitet. +- Markt und Durchführung werden unabhängig archiviert. +- Alle Schreibvorgänge werden serverseitig validiert. + +## Fehler + +Bei fehlender oder ungültiger Authentifizierung antwortet der Endpunkt mit +HTTP 401. Ungültige JSON-RPC-Requests oder fachlich ungültige Eingaben werden +als JSON-RPC-Fehlerantwort zurückgegeben. diff --git a/modules/market_management/mcp.php b/modules/market_management/mcp.php index f98cfd2..7962d52 100644 --- a/modules/market_management/mcp.php +++ b/modules/market_management/mcp.php @@ -46,6 +46,9 @@ function market_management_mcp_tools(): array ['name' => 'market_management.create_day', 'description' => 'Durchführungstag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date']]], ['name' => 'market_management.update_day', 'description' => 'Durchführungstag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]], ['name' => 'market_management.delete_day', 'description' => 'Durchführungstag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]], + ['name' => 'market_management.set_organizer', 'description' => 'Veranstalter einer Durchführung setzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'organization_contact_id', 'contact_person_id']]], + ['name' => 'market_management.create_cost_item', 'description' => 'Kostenposition anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'cost_type', 'gross_amount', 'tax_status']]], + ['name' => 'market_management.delete_cost_item', 'description' => 'Kostenposition entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id']]], ]; } @@ -106,6 +109,12 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a $pdo, market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id') ), + 'market_management.set_organizer' => market_management_set_organizer($pdo, $arguments), + 'market_management.create_cost_item' => market_management_create_cost_item($pdo, $arguments), + 'market_management.delete_cost_item' => market_management_delete_cost_item( + $pdo, + market_management_require_positive_id($arguments['cost_item_id'] ?? null, 'cost_item_id') + ), default => throw new InvalidArgumentException('Unbekanntes MCP-Tool: ' . $tool), }; } diff --git a/modules/market_management/service.php b/modules/market_management/service.php index 63fc9d2..15a458d 100644 --- a/modules/market_management/service.php +++ b/modules/market_management/service.php @@ -442,3 +442,66 @@ function market_management_delete_day(PDO $pdo, int $dayId): array } return $day; } + +function market_management_set_organizer(PDO $pdo, array $input): array +{ + $occurrenceId = market_management_require_positive_id($input['occurrence_id'] ?? null, 'occurrence_id'); + $organizationId = market_management_require_positive_id($input['organization_contact_id'] ?? null, 'organization_contact_id'); + $contactPersonId = market_management_require_positive_id($input['contact_person_id'] ?? null, 'contact_person_id'); + $stmt = $pdo->prepare( + 'INSERT INTO market_occurrence_organizer (occurrence_id, organization_contact_id, contact_person_id) + VALUES (:occurrence_id, :organization_contact_id, :contact_person_id) + ON CONFLICT (occurrence_id) DO UPDATE + SET organization_contact_id = EXCLUDED.organization_contact_id, + contact_person_id = EXCLUDED.contact_person_id, + updated_at = NOW() + RETURNING *' + ); + $stmt->execute([ + ':occurrence_id' => $occurrenceId, + ':organization_contact_id' => $organizationId, + ':contact_person_id' => $contactPersonId, + ]); + return (array) $stmt->fetch(); +} + +function market_management_create_cost_item(PDO $pdo, array $input): array +{ + $occurrenceId = market_management_require_positive_id($input['occurrence_id'] ?? null, 'occurrence_id'); + $costType = trim((string) ($input['cost_type'] ?? '')); + $grossAmount = market_management_optional_number($input, 'gross_amount'); + $taxStatus = trim((string) ($input['tax_status'] ?? '')); + $taxRate = market_management_optional_number($input, 'tax_rate') ?? '0.00'; + if ($costType === '' || $grossAmount === null || !in_array($taxStatus, ['tax_free', 'taxable'], true)) { + throw new InvalidArgumentException('cost_type, gross_amount und tax_status sind erforderlich.'); + } + if (($taxStatus === 'tax_free' && $taxRate !== '0.00') || ($taxStatus === 'taxable' && (float) $taxRate <= 0)) { + throw new InvalidArgumentException('tax_rate passt nicht zu tax_status.'); + } + $stmt = $pdo->prepare( + 'INSERT INTO market_occurrence_cost_item + (occurrence_id, cost_type, description, gross_amount, tax_status, tax_rate) + VALUES (:occurrence_id, :cost_type, :description, :gross_amount, :tax_status, :tax_rate) + RETURNING *' + ); + $stmt->execute([ + ':occurrence_id' => $occurrenceId, + ':cost_type' => $costType, + ':description' => market_management_optional_string($input, 'description'), + ':gross_amount' => $grossAmount, + ':tax_status' => $taxStatus, + ':tax_rate' => $taxRate, + ]); + return (array) $stmt->fetch(); +} + +function market_management_delete_cost_item(PDO $pdo, int $costItemId): array +{ + $stmt = $pdo->prepare('DELETE FROM market_occurrence_cost_item WHERE id = :id RETURNING id, occurrence_id'); + $stmt->execute([':id' => $costItemId]); + $item = $stmt->fetch(); + if (!is_array($item)) { + throw new RuntimeException('Kostenposition nicht gefunden.'); + } + return $item; +}