Vervollstaendige Marktmanagement-MCP-Abdeckung
This commit is contained in:
1 parent
3267a121de
commit
9bde337506
4 files changed
+348
-18
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE market_mcp_idempotency
|
||||||
|
ADD COLUMN result_payload JSONB;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -32,13 +32,16 @@ function market_management_mcp_tools(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object']],
|
['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object']],
|
||||||
|
['name' => 'market_management.search_occurrences', 'description' => 'Durchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object']],
|
||||||
['name' => 'market_management.get_market', 'description' => 'Markt mit Durchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
['name' => 'market_management.get_market', 'description' => 'Markt mit Durchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
||||||
['name' => 'market_management.create_market', 'description' => 'Markt mit bestehender Kategorie anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id']]],
|
['name' => 'market_management.create_market', 'description' => 'Markt mit bestehender Kategorie anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id']]],
|
||||||
['name' => 'market_management.update_market', 'description' => 'Markt ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
['name' => 'market_management.update_market', 'description' => 'Markt ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
||||||
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
||||||
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
||||||
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object']],
|
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object']],
|
||||||
['name' => 'market_management.create_occurrence', 'description' => 'Durchführung anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id']]],
|
['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object']],
|
||||||
|
['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object']],
|
||||||
|
['name' => 'market_management.create_occurrence', 'description' => 'Durchführung mit mindestens einem Tag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days']]],
|
||||||
['name' => 'market_management.get_occurrence', 'description' => 'Durchführung mit Tagen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
['name' => 'market_management.get_occurrence', 'description' => 'Durchführung mit Tagen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
||||||
['name' => 'market_management.update_occurrence', 'description' => 'Durchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
['name' => 'market_management.update_occurrence', 'description' => 'Durchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
||||||
['name' => 'market_management.archive_occurrence', 'description' => 'Durchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
['name' => 'market_management.archive_occurrence', 'description' => 'Durchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
||||||
@@ -47,15 +50,107 @@ function market_management_mcp_tools(): array
|
|||||||
['name' => 'market_management.update_day', 'description' => 'Durchführungstag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]],
|
['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.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.set_organizer', 'description' => 'Veranstalter einer Durchführung setzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'organization_contact_id', 'contact_person_id']]],
|
||||||
|
['name' => 'market_management.delete_organizer', 'description' => 'Veranstalter einer Durchführung entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_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.create_cost_item', 'description' => 'Kostenposition anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'cost_type', 'gross_amount', 'tax_status']]],
|
||||||
|
['name' => 'market_management.update_cost_item', 'description' => 'Kostenposition ändern', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id']]],
|
||||||
['name' => 'market_management.delete_cost_item', 'description' => 'Kostenposition entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id']]],
|
['name' => 'market_management.delete_cost_item', 'description' => 'Kostenposition entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id']]],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function market_management_mcp_is_write_tool(string $tool): bool
|
||||||
|
{
|
||||||
|
return in_array($tool, [
|
||||||
|
'market_management.create_market', 'market_management.update_market',
|
||||||
|
'market_management.archive_market', 'market_management.reactivate_market',
|
||||||
|
'market_management.create_occurrence', 'market_management.update_occurrence',
|
||||||
|
'market_management.archive_occurrence', 'market_management.reactivate_occurrence',
|
||||||
|
'market_management.create_day', 'market_management.update_day', 'market_management.delete_day',
|
||||||
|
'market_management.set_organizer', 'market_management.delete_organizer',
|
||||||
|
'market_management.create_cost_item', 'market_management.update_cost_item',
|
||||||
|
'market_management.delete_cost_item',
|
||||||
|
], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function market_management_mcp_call_idempotent(PDO $pdo, string $userSubject, string $tool, array $arguments): array
|
||||||
|
{
|
||||||
|
if (!market_management_mcp_is_write_tool($tool)) {
|
||||||
|
return market_management_mcp_call($pdo, $tool, $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
$idempotencyKey = trim((string) ($arguments['idempotency_key'] ?? ''));
|
||||||
|
if ($idempotencyKey === '') {
|
||||||
|
throw new InvalidArgumentException('Schreibende MCP-Aufrufe benötigen idempotency_key.');
|
||||||
|
}
|
||||||
|
$clientId = trim((string) ($arguments['client_id'] ?? 'hermes'));
|
||||||
|
if ($clientId === '') {
|
||||||
|
throw new InvalidArgumentException('client_id darf nicht leer sein.');
|
||||||
|
}
|
||||||
|
$businessArguments = $arguments;
|
||||||
|
unset($businessArguments['idempotency_key'], $businessArguments['client_id']);
|
||||||
|
$requestHash = hash('sha256', json_encode([
|
||||||
|
'tool' => $tool,
|
||||||
|
'arguments' => $businessArguments,
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$existingStmt = $pdo->prepare(
|
||||||
|
'SELECT user_subject, request_hash, result_status, result_payload
|
||||||
|
FROM market_mcp_idempotency
|
||||||
|
WHERE client_id = :client_id AND idempotency_key = :idempotency_key
|
||||||
|
FOR UPDATE'
|
||||||
|
);
|
||||||
|
$existingStmt->execute([':client_id' => $clientId, ':idempotency_key' => $idempotencyKey]);
|
||||||
|
$existing = $existingStmt->fetch();
|
||||||
|
if (is_array($existing)) {
|
||||||
|
if ((string) $existing['user_subject'] !== $userSubject || (string) $existing['request_hash'] !== $requestHash) {
|
||||||
|
throw new InvalidArgumentException('Idempotency-Schlüssel wurde bereits für eine andere Anfrage verwendet.');
|
||||||
|
}
|
||||||
|
if ((string) $existing['result_status'] === 'succeeded' && $existing['result_payload'] !== null) {
|
||||||
|
$pdo->commit();
|
||||||
|
return (array) json_decode((string) $existing['result_payload'], true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
throw new RuntimeException('Idempotency-Anfrage wird bereits verarbeitet oder ist fehlgeschlagen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$insert = $pdo->prepare(
|
||||||
|
'INSERT INTO market_mcp_idempotency
|
||||||
|
(user_subject, client_id, idempotency_key, tool_name, request_hash, result_status, expires_at)
|
||||||
|
VALUES (:user_subject, :client_id, :idempotency_key, :tool_name, :request_hash, \'processing\', NOW() + INTERVAL \'24 hours\')'
|
||||||
|
);
|
||||||
|
$insert->execute([
|
||||||
|
':user_subject' => $userSubject,
|
||||||
|
':client_id' => $clientId,
|
||||||
|
':idempotency_key' => $idempotencyKey,
|
||||||
|
':tool_name' => $tool,
|
||||||
|
':request_hash' => $requestHash,
|
||||||
|
]);
|
||||||
|
$result = market_management_mcp_call($pdo, $tool, $businessArguments);
|
||||||
|
$update = $pdo->prepare(
|
||||||
|
'UPDATE market_mcp_idempotency
|
||||||
|
SET result_status = \'succeeded\', result_payload = CAST(:result_payload AS JSONB)
|
||||||
|
WHERE client_id = :client_id AND idempotency_key = :idempotency_key'
|
||||||
|
);
|
||||||
|
$update->execute([
|
||||||
|
':result_payload' => json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||||
|
':client_id' => $clientId,
|
||||||
|
':idempotency_key' => $idempotencyKey,
|
||||||
|
]);
|
||||||
|
$pdo->commit();
|
||||||
|
return $result;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): array
|
function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): array
|
||||||
{
|
{
|
||||||
return match ($tool) {
|
return match ($tool) {
|
||||||
'market_management.search_markets' => market_management_search_markets($pdo, $arguments),
|
'market_management.search_markets' => market_management_search_markets($pdo, $arguments),
|
||||||
|
'market_management.search_occurrences' => market_management_search_occurrences($pdo, $arguments),
|
||||||
'market_management.get_market' => market_management_get_market(
|
'market_management.get_market' => market_management_get_market(
|
||||||
$pdo,
|
$pdo,
|
||||||
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
|
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
|
||||||
@@ -78,6 +173,11 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a
|
|||||||
false
|
false
|
||||||
),
|
),
|
||||||
'market_management.list_categories' => market_management_list_categories($pdo),
|
'market_management.list_categories' => market_management_list_categories($pdo),
|
||||||
|
'market_management.list_reference_data' => market_management_list_reference_data($pdo, $arguments),
|
||||||
|
'market_management.get_overview' => [
|
||||||
|
'markets' => market_management_search_markets($pdo, $arguments),
|
||||||
|
'occurrences' => market_management_search_occurrences($pdo, $arguments),
|
||||||
|
],
|
||||||
'market_management.create_occurrence' => market_management_create_occurrence($pdo, $arguments),
|
'market_management.create_occurrence' => market_management_create_occurrence($pdo, $arguments),
|
||||||
'market_management.get_occurrence' => market_management_get_occurrence(
|
'market_management.get_occurrence' => market_management_get_occurrence(
|
||||||
$pdo,
|
$pdo,
|
||||||
@@ -110,7 +210,16 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a
|
|||||||
market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id')
|
market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id')
|
||||||
),
|
),
|
||||||
'market_management.set_organizer' => market_management_set_organizer($pdo, $arguments),
|
'market_management.set_organizer' => market_management_set_organizer($pdo, $arguments),
|
||||||
|
'market_management.delete_organizer' => market_management_delete_organizer(
|
||||||
|
$pdo,
|
||||||
|
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id')
|
||||||
|
),
|
||||||
'market_management.create_cost_item' => market_management_create_cost_item($pdo, $arguments),
|
'market_management.create_cost_item' => market_management_create_cost_item($pdo, $arguments),
|
||||||
|
'market_management.update_cost_item' => market_management_update_cost_item(
|
||||||
|
$pdo,
|
||||||
|
market_management_require_positive_id($arguments['cost_item_id'] ?? null, 'cost_item_id'),
|
||||||
|
$arguments
|
||||||
|
),
|
||||||
'market_management.delete_cost_item' => market_management_delete_cost_item(
|
'market_management.delete_cost_item' => market_management_delete_cost_item(
|
||||||
$pdo,
|
$pdo,
|
||||||
market_management_require_positive_id($arguments['cost_item_id'] ?? null, 'cost_item_id')
|
market_management_require_positive_id($arguments['cost_item_id'] ?? null, 'cost_item_id')
|
||||||
|
|||||||
@@ -118,10 +118,28 @@ function market_management_search_markets(PDO $pdo, array $input): array
|
|||||||
{
|
{
|
||||||
$search = market_management_optional_string($input, 'search');
|
$search = market_management_optional_string($input, 'search');
|
||||||
$includeArchived = (bool) ($input['include_archived'] ?? false);
|
$includeArchived = (bool) ($input['include_archived'] ?? false);
|
||||||
|
$categoryId = array_key_exists('category_id', $input)
|
||||||
|
? market_management_require_positive_id($input['category_id'], 'category_id')
|
||||||
|
: null;
|
||||||
|
$status = array_key_exists('status', $input) ? market_management_validate_status($input['status']) : null;
|
||||||
|
$place = market_management_optional_string($input, 'place');
|
||||||
|
$municipalityId = array_key_exists('municipality_reference_id', $input)
|
||||||
|
? market_management_require_positive_id($input['municipality_reference_id'], 'municipality_reference_id')
|
||||||
|
: null;
|
||||||
|
$dateFrom = market_management_optional_string($input, 'date_from');
|
||||||
|
$dateTo = market_management_optional_string($input, 'date_to');
|
||||||
$limit = min(100, max(1, (int) ($input['limit'] ?? 50)));
|
$limit = min(100, max(1, (int) ($input['limit'] ?? 50)));
|
||||||
|
$offset = max(0, (int) ($input['offset'] ?? 0));
|
||||||
|
$sort = (string) ($input['sort'] ?? 'name');
|
||||||
|
$sortDirection = strtolower((string) ($input['sort_direction'] ?? 'asc')) === 'desc' ? 'DESC' : 'ASC';
|
||||||
|
$sortColumn = match ($sort) {
|
||||||
|
'earliest_date' => 'MIN(d.date)',
|
||||||
|
'updated_at' => 'm.updated_at',
|
||||||
|
default => 'm.name',
|
||||||
|
};
|
||||||
|
|
||||||
$where = [];
|
$where = [];
|
||||||
$params = [':limit' => $limit];
|
$params = [':limit' => $limit, ':offset' => $offset, ':include_archived' => $includeArchived];
|
||||||
if (!$includeArchived) {
|
if (!$includeArchived) {
|
||||||
$where[] = 'm.archived_at IS NULL';
|
$where[] = 'm.archived_at IS NULL';
|
||||||
}
|
}
|
||||||
@@ -129,18 +147,51 @@ function market_management_search_markets(PDO $pdo, array $input): array
|
|||||||
$where[] = 'm.name ILIKE :search';
|
$where[] = 'm.name ILIKE :search';
|
||||||
$params[':search'] = '%' . $search . '%';
|
$params[':search'] = '%' . $search . '%';
|
||||||
}
|
}
|
||||||
|
if ($categoryId !== null) {
|
||||||
|
$where[] = 'm.category_id = :category_id';
|
||||||
|
$params[':category_id'] = $categoryId;
|
||||||
|
}
|
||||||
|
if ($status !== null) {
|
||||||
|
$where[] = 'EXISTS (SELECT 1 FROM market_occurrence os WHERE os.market_definition_id = m.id AND os.status = :status AND (:include_archived OR os.archived_at IS NULL))';
|
||||||
|
$params[':status'] = $status;
|
||||||
|
}
|
||||||
|
if ($place !== null) {
|
||||||
|
$where[] = 'EXISTS (SELECT 1 FROM market_occurrence op WHERE op.market_definition_id = m.id AND op.place ILIKE :place AND (:include_archived OR op.archived_at IS NULL))';
|
||||||
|
$params[':place'] = '%' . $place . '%';
|
||||||
|
}
|
||||||
|
if ($municipalityId !== null) {
|
||||||
|
$where[] = 'EXISTS (SELECT 1 FROM market_occurrence om WHERE om.market_definition_id = m.id AND om.municipality_reference_id = :municipality_id AND (:include_archived OR om.archived_at IS NULL))';
|
||||||
|
$params[':municipality_id'] = $municipalityId;
|
||||||
|
}
|
||||||
|
if ($dateFrom !== null || $dateTo !== null) {
|
||||||
|
$dateConditions = ['od.occurrence_id = odate.id'];
|
||||||
|
if ($dateFrom !== null) {
|
||||||
|
$dateConditions[] = 'od.date >= :date_from';
|
||||||
|
$params[':date_from'] = $dateFrom;
|
||||||
|
}
|
||||||
|
if ($dateTo !== null) {
|
||||||
|
$dateConditions[] = 'od.date <= :date_to';
|
||||||
|
$params[':date_to'] = $dateTo;
|
||||||
|
}
|
||||||
|
$where[] = 'EXISTS (SELECT 1 FROM market_occurrence odate INNER JOIN market_occurrence_day od ON ' . implode(' AND ', $dateConditions) . ' WHERE odate.market_definition_id = m.id AND (:include_archived OR odate.archived_at IS NULL))';
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
'SELECT m.id, m.name, m.category_id, c.code AS category_code, c.name AS category_name,
|
'SELECT m.id, m.name, m.category_id, c.code AS category_code, c.name AS category_name,
|
||||||
m.archived_at, m.created_at, m.updated_at
|
m.archived_at, m.created_at, m.updated_at,
|
||||||
|
MIN(d.date) AS earliest_date, MAX(d.date) AS latest_date,
|
||||||
|
COUNT(DISTINCT o.id) AS occurrence_count
|
||||||
FROM market_definition m
|
FROM market_definition m
|
||||||
INNER JOIN market_category c ON c.id = m.category_id
|
INNER JOIN market_category c ON c.id = m.category_id
|
||||||
|
LEFT JOIN market_occurrence o ON o.market_definition_id = m.id AND (:include_archived OR o.archived_at IS NULL)
|
||||||
|
LEFT JOIN market_occurrence_day d ON d.occurrence_id = o.id
|
||||||
WHERE ' . ($where === [] ? 'TRUE' : implode(' AND ', $where)) . '
|
WHERE ' . ($where === [] ? 'TRUE' : implode(' AND ', $where)) . '
|
||||||
ORDER BY m.name ASC, m.id ASC
|
GROUP BY m.id, c.code, c.name
|
||||||
LIMIT :limit'
|
ORDER BY ' . $sortColumn . ' ' . $sortDirection . ', m.id ASC
|
||||||
|
LIMIT :limit OFFSET :offset'
|
||||||
);
|
);
|
||||||
foreach ($params as $key => $value) {
|
foreach ($params as $key => $value) {
|
||||||
$stmt->bindValue($key, $value, $key === ':limit' ? PDO::PARAM_INT : PDO::PARAM_STR);
|
$stmt->bindValue($key, $value, in_array($key, [':limit', ':offset', ':category_id', ':municipality_id'], true) ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||||
}
|
}
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
return $stmt->fetchAll();
|
return $stmt->fetchAll();
|
||||||
@@ -162,10 +213,7 @@ function market_management_get_market(PDO $pdo, int $marketId, bool $includeArch
|
|||||||
}
|
}
|
||||||
|
|
||||||
$occurrences = $pdo->prepare(
|
$occurrences = $pdo->prepare(
|
||||||
'SELECT id, status, place, address, municipality_reference_id, notes,
|
'SELECT *
|
||||||
registration_deadline, request_or_registration_date, confirmation_date,
|
|
||||||
payment_deadline, payment_status, payment_date, paid_amount,
|
|
||||||
contract_folder_url, archived_at, created_at, updated_at
|
|
||||||
FROM market_occurrence
|
FROM market_occurrence
|
||||||
WHERE market_definition_id = :market_id
|
WHERE market_definition_id = :market_id
|
||||||
AND (:include_archived OR archived_at IS NULL)
|
AND (:include_archived OR archived_at IS NULL)
|
||||||
@@ -173,9 +221,94 @@ function market_management_get_market(PDO $pdo, int $marketId, bool $includeArch
|
|||||||
);
|
);
|
||||||
$occurrences->execute([':market_id' => $marketId, ':include_archived' => $includeArchived]);
|
$occurrences->execute([':market_id' => $marketId, ':include_archived' => $includeArchived]);
|
||||||
$market['occurrences'] = $occurrences->fetchAll();
|
$market['occurrences'] = $occurrences->fetchAll();
|
||||||
|
foreach ($market['occurrences'] as &$occurrence) {
|
||||||
|
$occurrenceId = (int) $occurrence['id'];
|
||||||
|
$organizer = $pdo->prepare('SELECT * FROM market_occurrence_organizer WHERE occurrence_id = :occurrence_id');
|
||||||
|
$organizer->execute([':occurrence_id' => $occurrenceId]);
|
||||||
|
$occurrence['organizer'] = $organizer->fetch() ?: null;
|
||||||
|
$days = $pdo->prepare('SELECT * FROM market_occurrence_day WHERE occurrence_id = :occurrence_id ORDER BY date ASC, id ASC');
|
||||||
|
$days->execute([':occurrence_id' => $occurrenceId]);
|
||||||
|
$occurrence['days'] = $days->fetchAll();
|
||||||
|
$costs = $pdo->prepare('SELECT * FROM market_occurrence_cost_item WHERE occurrence_id = :occurrence_id ORDER BY id ASC');
|
||||||
|
$costs->execute([':occurrence_id' => $occurrenceId]);
|
||||||
|
$occurrence['cost_items'] = $costs->fetchAll();
|
||||||
|
}
|
||||||
|
unset($occurrence);
|
||||||
return $market;
|
return $market;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function market_management_search_occurrences(PDO $pdo, array $input): array
|
||||||
|
{
|
||||||
|
$where = [];
|
||||||
|
$params = [':limit' => min(100, max(1, (int) ($input['limit'] ?? 50))), ':offset' => max(0, (int) ($input['offset'] ?? 0))];
|
||||||
|
$includeArchived = (bool) ($input['include_archived'] ?? false);
|
||||||
|
$params[':include_archived'] = $includeArchived;
|
||||||
|
if (!$includeArchived) {
|
||||||
|
$where[] = 'o.archived_at IS NULL';
|
||||||
|
}
|
||||||
|
if (array_key_exists('market_definition_id', $input)) {
|
||||||
|
$where[] = 'o.market_definition_id = :market_definition_id';
|
||||||
|
$params[':market_definition_id'] = market_management_require_positive_id($input['market_definition_id'], 'market_definition_id');
|
||||||
|
}
|
||||||
|
if (array_key_exists('status', $input)) {
|
||||||
|
$where[] = 'o.status = :status';
|
||||||
|
$params[':status'] = market_management_validate_status($input['status']);
|
||||||
|
}
|
||||||
|
foreach (['place' => 'o.place', 'address' => 'o.address'] as $key => $column) {
|
||||||
|
$value = market_management_optional_string($input, $key);
|
||||||
|
if ($value !== null) {
|
||||||
|
$where[] = $column . ' ILIKE :' . $key;
|
||||||
|
$params[':' . $key] = '%' . $value . '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (array_key_exists('municipality_reference_id', $input)) {
|
||||||
|
$where[] = 'o.municipality_reference_id = :municipality_reference_id';
|
||||||
|
$params[':municipality_reference_id'] = market_management_require_positive_id($input['municipality_reference_id'], 'municipality_reference_id');
|
||||||
|
}
|
||||||
|
foreach (['date_from' => '>=', 'date_to' => '<='] as $key => $operator) {
|
||||||
|
$value = market_management_optional_string($input, $key);
|
||||||
|
if ($value !== null) {
|
||||||
|
$where[] = 'EXISTS (SELECT 1 FROM market_occurrence_day fd WHERE fd.occurrence_id = o.id AND fd.date ' . $operator . ' :' . $key . ')';
|
||||||
|
$params[':' . $key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT o.*, m.name AS market_name, m.category_id, c.code AS category_code, c.name AS category_name,
|
||||||
|
MIN(d.date) AS earliest_date, MAX(d.date) AS latest_date, COUNT(d.id) AS day_count
|
||||||
|
FROM market_occurrence o
|
||||||
|
INNER JOIN market_definition m ON m.id = o.market_definition_id
|
||||||
|
INNER JOIN market_category c ON c.id = m.category_id
|
||||||
|
LEFT JOIN market_occurrence_day d ON d.occurrence_id = o.id
|
||||||
|
WHERE ' . ($where === [] ? 'TRUE' : implode(' AND ', $where)) . '
|
||||||
|
GROUP BY o.id, m.name, c.code, c.name
|
||||||
|
ORDER BY earliest_date NULLS LAST, o.id ASC
|
||||||
|
LIMIT :limit OFFSET :offset'
|
||||||
|
);
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
$stmt->bindValue($key, $value, in_array($key, [':limit', ':offset', ':market_definition_id', ':municipality_reference_id'], true) ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function market_management_list_reference_data(PDO $pdo, array $input): array
|
||||||
|
{
|
||||||
|
$result = ['categories' => market_management_list_categories($pdo)];
|
||||||
|
$municipalities = $pdo->query(
|
||||||
|
'SELECT id, bfs_municipality_number, name, canton_code, data_version
|
||||||
|
FROM market_municipality_reference WHERE is_active = TRUE ORDER BY name ASC, id ASC'
|
||||||
|
)->fetchAll();
|
||||||
|
$result['municipalities'] = $municipalities;
|
||||||
|
$result['cantons'] = array_values(array_map(
|
||||||
|
static fn (array $row): array => ['code' => (string) $row['canton_code']],
|
||||||
|
array_values(array_reduce($municipalities, static function (array $carry, array $row): array {
|
||||||
|
$carry[(string) $row['canton_code']] = $row;
|
||||||
|
return $carry;
|
||||||
|
}, []))
|
||||||
|
));
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
function market_management_update_market(PDO $pdo, int $marketId, array $input): array
|
function market_management_update_market(PDO $pdo, int $marketId, array $input): array
|
||||||
{
|
{
|
||||||
$sets = [];
|
$sets = [];
|
||||||
@@ -274,6 +407,10 @@ function market_management_create_occurrence(PDO $pdo, array $input): array
|
|||||||
throw new InvalidArgumentException('market_definition_id verweist nicht auf einen Markt.');
|
throw new InvalidArgumentException('market_definition_id verweist nicht auf einen Markt.');
|
||||||
}
|
}
|
||||||
$status = array_key_exists('status', $input) ? market_management_validate_status($input['status']) : 'open';
|
$status = array_key_exists('status', $input) ? market_management_validate_status($input['status']) : 'open';
|
||||||
|
$days = $input['days'] ?? null;
|
||||||
|
if (!is_array($days) || count($days) < 1) {
|
||||||
|
throw new InvalidArgumentException('Mindestens ein Durchführungstag ist erforderlich.');
|
||||||
|
}
|
||||||
$definitions = [
|
$definitions = [
|
||||||
'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'id', 'notes' => 'string',
|
'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'id', 'notes' => 'string',
|
||||||
'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'confirmation_date' => 'string',
|
'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'confirmation_date' => 'string',
|
||||||
@@ -296,13 +433,35 @@ function market_management_create_occurrence(PDO $pdo, array $input): array
|
|||||||
$params[':status'] = $status;
|
$params[':status'] = $status;
|
||||||
$columns = array_map(static fn (string $field): string => explode(' = ', $field, 2)[0], $fields);
|
$columns = array_map(static fn (string $field): string => explode(' = ', $field, 2)[0], $fields);
|
||||||
$values = array_map(static fn (string $field): string => explode(' = ', $field, 2)[1], $fields);
|
$values = array_map(static fn (string $field): string => explode(' = ', $field, 2)[1], $fields);
|
||||||
$stmt = $pdo->prepare(
|
$startedTransaction = !$pdo->inTransaction();
|
||||||
'INSERT INTO market_occurrence (' . implode(', ', $columns) . ')
|
if ($startedTransaction) {
|
||||||
VALUES (' . implode(', ', $values) . ')
|
$pdo->beginTransaction();
|
||||||
RETURNING *'
|
}
|
||||||
);
|
try {
|
||||||
$stmt->execute($params);
|
$stmt = $pdo->prepare(
|
||||||
return (array) $stmt->fetch();
|
'INSERT INTO market_occurrence (' . implode(', ', $columns) . ')
|
||||||
|
VALUES (' . implode(', ', $values) . ')
|
||||||
|
RETURNING *'
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$occurrence = (array) $stmt->fetch();
|
||||||
|
foreach ($days as $day) {
|
||||||
|
if (!is_array($day)) {
|
||||||
|
throw new InvalidArgumentException('Jeder Durchführungstag muss ein Objekt sein.');
|
||||||
|
}
|
||||||
|
$day['occurrence_id'] = (int) $occurrence['id'];
|
||||||
|
market_management_create_day($pdo, $day);
|
||||||
|
}
|
||||||
|
if ($startedTransaction) {
|
||||||
|
$pdo->commit();
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($startedTransaction && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
return market_management_get_occurrence($pdo, (int) $occurrence['id'], true) ?? $occurrence;
|
||||||
}
|
}
|
||||||
|
|
||||||
function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $includeArchived = false): ?array
|
function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $includeArchived = false): ?array
|
||||||
@@ -322,6 +481,12 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
|
|||||||
);
|
);
|
||||||
$days->execute([':occurrence_id' => $occurrenceId]);
|
$days->execute([':occurrence_id' => $occurrenceId]);
|
||||||
$occurrence['days'] = $days->fetchAll();
|
$occurrence['days'] = $days->fetchAll();
|
||||||
|
$organizer = $pdo->prepare('SELECT * FROM market_occurrence_organizer WHERE occurrence_id = :occurrence_id');
|
||||||
|
$organizer->execute([':occurrence_id' => $occurrenceId]);
|
||||||
|
$occurrence['organizer'] = $organizer->fetch() ?: null;
|
||||||
|
$costs = $pdo->prepare('SELECT * FROM market_occurrence_cost_item WHERE occurrence_id = :occurrence_id ORDER BY id ASC');
|
||||||
|
$costs->execute([':occurrence_id' => $occurrenceId]);
|
||||||
|
$occurrence['cost_items'] = $costs->fetchAll();
|
||||||
return $occurrence;
|
return $occurrence;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +630,17 @@ function market_management_set_organizer(PDO $pdo, array $input): array
|
|||||||
return (array) $stmt->fetch();
|
return (array) $stmt->fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function market_management_delete_organizer(PDO $pdo, int $occurrenceId): array
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM market_occurrence_organizer WHERE occurrence_id = :occurrence_id RETURNING id, occurrence_id');
|
||||||
|
$stmt->execute([':occurrence_id' => $occurrenceId]);
|
||||||
|
$organizer = $stmt->fetch();
|
||||||
|
if (!is_array($organizer)) {
|
||||||
|
throw new RuntimeException('Veranstalter nicht gefunden.');
|
||||||
|
}
|
||||||
|
return $organizer;
|
||||||
|
}
|
||||||
|
|
||||||
function market_management_create_cost_item(PDO $pdo, array $input): array
|
function market_management_create_cost_item(PDO $pdo, array $input): array
|
||||||
{
|
{
|
||||||
$occurrenceId = market_management_require_positive_id($input['occurrence_id'] ?? null, 'occurrence_id');
|
$occurrenceId = market_management_require_positive_id($input['occurrence_id'] ?? null, 'occurrence_id');
|
||||||
@@ -505,3 +681,42 @@ function market_management_delete_cost_item(PDO $pdo, int $costItemId): array
|
|||||||
}
|
}
|
||||||
return $item;
|
return $item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function market_management_update_cost_item(PDO $pdo, int $costItemId, array $input): array
|
||||||
|
{
|
||||||
|
$sets = [];
|
||||||
|
$params = [':id' => $costItemId];
|
||||||
|
if (array_key_exists('cost_type', $input)) {
|
||||||
|
$costType = trim((string) $input['cost_type']);
|
||||||
|
if ($costType === '') {
|
||||||
|
throw new InvalidArgumentException('cost_type darf nicht leer sein.');
|
||||||
|
}
|
||||||
|
$sets[] = 'cost_type = :cost_type';
|
||||||
|
$params[':cost_type'] = $costType;
|
||||||
|
}
|
||||||
|
foreach (['description' => 'string', 'gross_amount' => 'number', 'tax_status' => 'string', 'tax_rate' => 'number'] as $field => $type) {
|
||||||
|
if (!array_key_exists($field, $input)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sets[] = $field . ' = :' . $field;
|
||||||
|
$params[':' . $field] = $type === 'number'
|
||||||
|
? market_management_optional_number($input, $field)
|
||||||
|
: market_management_optional_string($input, $field);
|
||||||
|
}
|
||||||
|
if (isset($params[':tax_status']) && !in_array($params[':tax_status'], ['tax_free', 'taxable'], true)) {
|
||||||
|
throw new InvalidArgumentException('Ungültiger Steuerstatus.');
|
||||||
|
}
|
||||||
|
if ($sets === []) {
|
||||||
|
throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.');
|
||||||
|
}
|
||||||
|
$sets[] = 'updated_at = NOW()';
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'UPDATE market_occurrence_cost_item SET ' . implode(', ', $sets) . ' WHERE id = :id RETURNING *'
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$item = $stmt->fetch();
|
||||||
|
if (!is_array($item)) {
|
||||||
|
throw new RuntimeException('Kostenposition nicht gefunden.');
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ try {
|
|||||||
$result = [
|
$result = [
|
||||||
'content' => [[
|
'content' => [[
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'text' => json_encode(market_management_mcp_call($pdo, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
'text' => json_encode(market_management_mcp_call_idempotent($pdo, $userSubject, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||||
]],
|
]],
|
||||||
'isError' => false,
|
'isError' => false,
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in new issue
Block a user