prepare('SELECT 1 FROM market_category WHERE id = :id AND is_active = TRUE'); $stmt->execute([':id' => $categoryId]); return $stmt->fetchColumn() !== false; } function market_management_create_market(PDO $pdo, array $input): array { $name = trim((string) ($input['name'] ?? '')); $categoryId = market_management_require_positive_id($input['category_id'] ?? null, 'category_id'); if ($name === '') { throw new InvalidArgumentException('name ist erforderlich.'); } if (!market_management_category_exists($pdo, $categoryId)) { throw new InvalidArgumentException('category_id verweist nicht auf eine aktive Kategorie.'); } $stmt = $pdo->prepare( 'INSERT INTO market_definition (name, category_id) VALUES (:name, :category_id) RETURNING id, name, category_id, archived_at, created_at, updated_at' ); $stmt->execute([':name' => $name, ':category_id' => $categoryId]); return (array) $stmt->fetch(); } function market_management_search_markets(PDO $pdo, array $input): array { $search = market_management_optional_string($input, 'search'); $includeArchived = (bool) ($input['include_archived'] ?? false); $limit = min(100, max(1, (int) ($input['limit'] ?? 50))); $where = []; $params = [':limit' => $limit]; if (!$includeArchived) { $where[] = 'm.archived_at IS NULL'; } if ($search !== null) { $where[] = 'm.name ILIKE :search'; $params[':search'] = '%' . $search . '%'; } $stmt = $pdo->prepare( '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 FROM market_definition m INNER JOIN market_category c ON c.id = m.category_id WHERE ' . ($where === [] ? 'TRUE' : implode(' AND ', $where)) . ' ORDER BY m.name ASC, m.id ASC LIMIT :limit' ); foreach ($params as $key => $value) { $stmt->bindValue($key, $value, $key === ':limit' ? PDO::PARAM_INT : PDO::PARAM_STR); } $stmt->execute(); return $stmt->fetchAll(); } function market_management_get_market(PDO $pdo, int $marketId, bool $includeArchived = false): ?array { $stmt = $pdo->prepare( '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 FROM market_definition m INNER JOIN market_category c ON c.id = m.category_id WHERE m.id = :id AND (:include_archived OR m.archived_at IS NULL)' ); $stmt->execute([':id' => $marketId, ':include_archived' => $includeArchived]); $market = $stmt->fetch(); if (!is_array($market)) { return null; } $occurrences = $pdo->prepare( 'SELECT id, status, place, address, municipality_reference_id, notes, 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 WHERE market_definition_id = :market_id AND (:include_archived OR archived_at IS NULL) ORDER BY id ASC' ); $occurrences->execute([':market_id' => $marketId, ':include_archived' => $includeArchived]); $market['occurrences'] = $occurrences->fetchAll(); return $market; } function market_management_update_market(PDO $pdo, int $marketId, array $input): array { $sets = []; $params = [':id' => $marketId]; if (array_key_exists('name', $input)) { $name = trim((string) $input['name']); if ($name === '') { throw new InvalidArgumentException('name darf nicht leer sein.'); } $sets[] = 'name = :name'; $params[':name'] = $name; } if (array_key_exists('category_id', $input)) { $categoryId = market_management_require_positive_id($input['category_id'], 'category_id'); if (!market_management_category_exists($pdo, $categoryId)) { throw new InvalidArgumentException('category_id verweist nicht auf eine aktive Kategorie.'); } $sets[] = 'category_id = :category_id'; $params[':category_id'] = $categoryId; } if ($sets === []) { throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.'); } $sets[] = 'updated_at = NOW()'; $stmt = $pdo->prepare( 'UPDATE market_definition SET ' . implode(', ', $sets) . ' WHERE id = :id RETURNING id, name, category_id, archived_at, created_at, updated_at' ); $stmt->execute($params); $market = $stmt->fetch(); if (!is_array($market)) { throw new RuntimeException('Markt nicht gefunden.'); } return $market; } function market_management_set_market_archive(PDO $pdo, int $marketId, bool $archive): array { $stmt = $pdo->prepare( 'UPDATE market_definition SET archived_at = ' . ($archive ? 'COALESCE(archived_at, NOW())' : 'NULL') . ', updated_at = NOW() WHERE id = :id RETURNING id, name, category_id, archived_at, created_at, updated_at' ); $stmt->execute([':id' => $marketId]); $market = $stmt->fetch(); if (!is_array($market)) { throw new RuntimeException('Markt nicht gefunden.'); } return $market; } function market_management_list_categories(PDO $pdo): array { return $pdo->query( 'SELECT id, code, name FROM market_category WHERE is_active = TRUE ORDER BY name ASC, id ASC' )->fetchAll(); } function market_management_validate_status(mixed $status): string { $value = trim((string) $status); if (!in_array($value, MARKET_MANAGEMENT_STATUSES, true)) { throw new InvalidArgumentException('Ungültiger Durchführungsstatus.'); } return $value; } function market_management_write_fields(array $input, array $definitions): array { $fields = []; $params = []; foreach ($definitions as $field => $type) { if (!array_key_exists($field, $input)) { continue; } $value = match ($type) { 'string' => market_management_optional_string($input, $field), 'bool' => market_management_optional_bool($input, $field), 'number' => market_management_optional_number($input, $field), 'integer' => market_management_optional_integer($input, $field), 'id' => market_management_require_positive_id($input[$field], $field), default => throw new LogicException('Unbekannter Feldtyp.'), }; $fields[] = $field . ' = :' . $field; $params[':' . $field] = $value; } return [$fields, $params]; } function market_management_create_occurrence(PDO $pdo, array $input): array { $marketId = market_management_require_positive_id($input['market_definition_id'] ?? null, 'market_definition_id'); if (market_management_get_market($pdo, $marketId, true) === null) { throw new InvalidArgumentException('market_definition_id verweist nicht auf einen Markt.'); } $status = array_key_exists('status', $input) ? market_management_validate_status($input['status']) : 'open'; $definitions = [ 'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'id', 'notes' => 'string', 'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'confirmation_date' => 'string', 'payment_deadline' => 'string', 'stand_size' => 'string', 'table_available' => 'bool', 'table_rentable' => 'bool', 'own_table_required' => 'bool', 'shelter_available' => 'bool', 'power_available' => 'bool', 'water_available' => 'bool', 'lighting_available' => 'bool', 'other_infrastructure' => 'string', 'own_stand_requirements' => 'string', 'travel_address' => 'string', 'travel_distance_km' => 'number', 'travel_duration_minutes' => 'integer', 'parking_options' => 'string', 'delivery_access' => 'string', 'direct_unload_possible' => 'bool', 'storage_options' => 'string', 'travel_notes' => 'string', 'payment_status' => 'string', 'payment_date' => 'string', 'paid_amount' => 'number', 'payment_method_code' => 'string', 'cancellation_terms' => 'string', 'revenue_share_terms' => 'string', 'other_participation_conditions' => 'string', 'contract_folder_url' => 'string', ]; [$fields, $params] = market_management_write_fields($input, $definitions); if (isset($params[':payment_status']) && !in_array($params[':payment_status'], ['paid', 'unpaid'], true)) { throw new InvalidArgumentException('Ungültiger Zahlungsstatus.'); } array_unshift($fields, 'market_definition_id = :market_definition_id', 'status = :status'); $params[':market_definition_id'] = $marketId; $params[':status'] = $status; $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); $stmt = $pdo->prepare( 'INSERT INTO market_occurrence (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ') RETURNING *' ); $stmt->execute($params); return (array) $stmt->fetch(); } function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $includeArchived = false): ?array { $stmt = $pdo->prepare( 'SELECT * FROM market_occurrence WHERE id = :id AND (:include_archived OR archived_at IS NULL)' ); $stmt->execute([':id' => $occurrenceId, ':include_archived' => $includeArchived]); $occurrence = $stmt->fetch(); if (!is_array($occurrence)) { return 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(); return $occurrence; } function market_management_update_occurrence(PDO $pdo, int $occurrenceId, array $input): array { $definitions = [ 'status' => 'string', 'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'id', 'notes' => 'string', 'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'confirmation_date' => 'string', 'payment_deadline' => 'string', 'stand_size' => 'string', 'table_available' => 'bool', 'table_rentable' => 'bool', 'own_table_required' => 'bool', 'shelter_available' => 'bool', 'power_available' => 'bool', 'water_available' => 'bool', 'lighting_available' => 'bool', 'other_infrastructure' => 'string', 'own_stand_requirements' => 'string', 'travel_address' => 'string', 'travel_distance_km' => 'number', 'travel_duration_minutes' => 'integer', 'parking_options' => 'string', 'delivery_access' => 'string', 'direct_unload_possible' => 'bool', 'storage_options' => 'string', 'travel_notes' => 'string', 'payment_status' => 'string', 'payment_date' => 'string', 'paid_amount' => 'number', 'payment_method_code' => 'string', 'cancellation_terms' => 'string', 'revenue_share_terms' => 'string', 'other_participation_conditions' => 'string', 'contract_folder_url' => 'string', ]; [$fields, $params] = market_management_write_fields($input, $definitions); if (array_key_exists('status', $input)) { $params[':status'] = market_management_validate_status($input['status']); } if (isset($params[':payment_status']) && !in_array($params[':payment_status'], ['paid', 'unpaid'], true)) { throw new InvalidArgumentException('Ungültiger Zahlungsstatus.'); } if ($fields === []) { throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.'); } $fields[] = 'updated_at = NOW()'; $params[':id'] = $occurrenceId; $stmt = $pdo->prepare( 'UPDATE market_occurrence SET ' . implode(', ', $fields) . ' WHERE id = :id RETURNING *' ); $stmt->execute($params); $occurrence = $stmt->fetch(); if (!is_array($occurrence)) { throw new RuntimeException('Durchführung nicht gefunden.'); } return $occurrence; } function market_management_create_day(PDO $pdo, array $input): array { $occurrenceId = market_management_require_positive_id($input['occurrence_id'] ?? null, 'occurrence_id'); if (market_management_get_occurrence($pdo, $occurrenceId, true) === null) { throw new InvalidArgumentException('occurrence_id verweist nicht auf eine Durchführung.'); } $date = trim((string) ($input['date'] ?? '')); if ($date === '') { throw new InvalidArgumentException('date ist erforderlich.'); } $definitions = [ 'stand_start_time' => 'string', 'stand_end_time' => 'string', 'setup_start_time' => 'string', 'setup_end_time' => 'string', 'delivery_start_time' => 'string', 'delivery_end_time' => 'string', 'observations' => 'string', ]; [$fields, $params] = market_management_write_fields($input, $definitions); array_unshift($fields, 'occurrence_id = :occurrence_id', 'date = :date'); $params[':occurrence_id'] = $occurrenceId; $params[':date'] = $date; $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); $stmt = $pdo->prepare( 'INSERT INTO market_occurrence_day (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ') RETURNING *' ); $stmt->execute($params); return (array) $stmt->fetch(); } function market_management_update_day(PDO $pdo, int $dayId, array $input): array { $definitions = [ 'date' => 'string', 'stand_start_time' => 'string', 'stand_end_time' => 'string', 'setup_start_time' => 'string', 'setup_end_time' => 'string', 'delivery_start_time' => 'string', 'delivery_end_time' => 'string', 'observations' => 'string', ]; [$fields, $params] = market_management_write_fields($input, $definitions); if ($fields === []) { throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.'); } $fields[] = 'updated_at = NOW()'; $params[':id'] = $dayId; $stmt = $pdo->prepare( 'UPDATE market_occurrence_day SET ' . implode(', ', $fields) . ' WHERE id = :id RETURNING *' ); $stmt->execute($params); $day = $stmt->fetch(); if (!is_array($day)) { throw new RuntimeException('Durchführungstag nicht gefunden.'); } return $day; } function market_management_set_occurrence_archive(PDO $pdo, int $occurrenceId, bool $archive): array { $stmt = $pdo->prepare( 'UPDATE market_occurrence SET archived_at = ' . ($archive ? 'COALESCE(archived_at, NOW())' : 'NULL') . ', updated_at = NOW() WHERE id = :id RETURNING *' ); $stmt->execute([':id' => $occurrenceId]); $occurrence = $stmt->fetch(); if (!is_array($occurrence)) { throw new RuntimeException('Durchführung nicht gefunden.'); } return $occurrence; } function market_management_delete_day(PDO $pdo, int $dayId): array { $stmt = $pdo->prepare('DELETE FROM market_occurrence_day WHERE id = :id RETURNING id, occurrence_id, date'); $stmt->execute([':id' => $dayId]); $day = $stmt->fetch(); if (!is_array($day)) { throw new RuntimeException('Durchführungstag nicht gefunden.'); } 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; }