From 241ec75323dbedfd7bc9829d52391c81faa5a7e2 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Fri, 17 Apr 2026 19:32:50 +0200 Subject: [PATCH] added contributions API endpoint with CRUD and voting with prepared statements --- api/contributions.php | 318 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 api/contributions.php diff --git a/api/contributions.php b/api/contributions.php new file mode 100644 index 0000000..3dcb4b4 --- /dev/null +++ b/api/contributions.php @@ -0,0 +1,318 @@ + $municipality_id]; + + if (!empty($input['category'])) { + $sql .= " AND category = :cat"; + $params[':cat'] = $input['category']; + } + + $sql .= " ORDER BY created_at DESC"; + + try { + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + } catch (PDOException $e) { + error_response('Database Error: ' . $e->getMessage(), 500); + } + + // Build GeoJSON FeatureCollection + $features = []; + + foreach ($rows as $row) { + $geometry = json_decode($row['geojson']); + + // Remove raw Geometry Columns from Properties + unset($row['geom']); + unset($row['geojson']); + + $features[] = [ + 'type' => 'Feature', + 'geometry' => $geometry, + 'properties' => $row + ]; + } + + $featureCollection = [ + 'type' => 'FeatureCollection', + 'features' => $features + ]; + + json_response($featureCollection); +} + + +// --------------------------------------------------------------------- +// CREATE — Insert a new Contribution +// Required: municipality_id, geom, geom_type, category, title, author_name +// Optional: description +// --------------------------------------------------------------------- +function handle_create($input) { + $pdo = get_db(); + + // Validate Input + $missing = validate_required($input, [ + 'municipality_id', 'geom', 'geom_type', 'category', 'title', 'author_name' + ]); + if (!empty($missing)) { + error_response('Missing Fields: ' . implode(', ', $missing)); + } + + // Validate Geometry Type + $valid_geom_types = ['point', 'line', 'polygon']; + if (!in_array($input['geom_type'], $valid_geom_types)) { + error_response('Invalid geom_type. Must be: ' . implode(', ', $valid_geom_types)); + } + + // Validate GeoJSON + $geojson = json_decode($input['geom']); + if (!$geojson || !isset($geojson->type)) { + error_response('Invalid GeoJSON in geom Field.'); + } + + try { + $stmt = $pdo->prepare(" + INSERT INTO contributions + (municipality_id, geom, geom_type, category, title, description, author_name) + VALUES + (:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type, + :category, :title, :description, :author_name) + "); + + $stmt->execute([ + ':mid' => $input['municipality_id'], + ':geom' => $input['geom'], + ':geom_type' => $input['geom_type'], + ':category' => $input['category'], + ':title' => $input['title'], + ':description' => $input['description'] ?? '', + ':author_name' => $input['author_name'] + ]); + + json_response([ + 'message' => 'Contribution created successfully.', + 'contribution_id' => (int) $pdo->lastInsertId() + ], 201); + + } catch (PDOException $e) { + error_response('Database Error: ' . $e->getMessage(), 500); + } +} + + +// --------------------------------------------------------------------- +// UPDATE — Update an existing Contribution +// Required: contribution_id +// Optional: category, title, description, status +// Only provided Fields are updated — others remain unchanged. +// --------------------------------------------------------------------- +function handle_update($input) { + $pdo = get_db(); + + // Validate Input + $missing = validate_required($input, ['contribution_id']); + if (!empty($missing)) { + error_response('Missing Fields: ' . implode(', ', $missing)); + } + + $contribution_id = $input['contribution_id']; + + // Check if Contribution exists + $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); + $stmt->execute([':id' => $contribution_id]); + if (!$stmt->fetch()) { + error_response('Contribution not found.', 404); + } + + // Build dynamic UPDATE Query — only update Fields that were sent + $updatable_fields = ['category', 'title', 'description', 'status']; + $set_clauses = []; + $params = [':id' => $contribution_id]; + + foreach ($updatable_fields as $field) { + if (isset($input[$field]) && $input[$field] !== '') { + $set_clauses[] = "$field = :$field"; + $params[":$field"] = $input[$field]; + } + } + + if (empty($set_clauses)) { + error_response('No Fields to update. Provide at least one of: ' . implode(', ', $updatable_fields)); + } + + // Validate Status if provided + if (isset($params[':status'])) { + $valid_statuses = ['pending', 'approved', 'rejected', 'in_progress', 'done']; + if (!in_array($params[':status'], $valid_statuses)) { + error_response('Invalid Status. Must be: ' . implode(', ', $valid_statuses)); + } + } + + $sql = "UPDATE contributions SET " . implode(', ', $set_clauses) . " WHERE contribution_id = :id"; + + try { + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + json_response(['message' => 'Contribution updated successfully.']); + + } catch (PDOException $e) { + error_response('Database Error: ' . $e->getMessage(), 500); + } +} + + +// --------------------------------------------------------------------- +// DELETE — Delete a Contribution +// Required: contribution_id +// Note: Associated Votes are deleted automatically (ON DELETE CASCADE). +// --------------------------------------------------------------------- +function handle_delete($input) { + $pdo = get_db(); + + // Validate Input + $missing = validate_required($input, ['contribution_id']); + if (!empty($missing)) { + error_response('Missing Fields: ' . implode(', ', $missing)); + } + + $contribution_id = $input['contribution_id']; + + // Check if Contribution exists + $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); + $stmt->execute([':id' => $contribution_id]); + if (!$stmt->fetch()) { + error_response('Contribution not found.', 404); + } + + try { + $stmt = $pdo->prepare("DELETE FROM contributions WHERE contribution_id = :id"); + $stmt->execute([':id' => $contribution_id]); + + json_response(['message' => 'Contribution deleted successfully.']); + + } catch (PDOException $e) { + error_response('Database Error: ' . $e->getMessage(), 500); + } +} + + +// --------------------------------------------------------------------- +// VOTE — Cast a Like or Dislike on a Contribution +// Required: contribution_id, voter_name, vote_type (like|dislike) +// The Database Trigger automatically updates likes_count/dislikes_count. +// The UNIQUE Constraint prevents duplicate Votes per Voter. +// --------------------------------------------------------------------- +function handle_vote($input) { + $pdo = get_db(); + + // Validate Input + $missing = validate_required($input, ['contribution_id', 'voter_name', 'vote_type']); + if (!empty($missing)) { + error_response('Missing Fields: ' . implode(', ', $missing)); + } + + // Validate Vote Type + $valid_vote_types = ['like', 'dislike']; + if (!in_array($input['vote_type'], $valid_vote_types)) { + error_response('Invalid vote_type. Must be: ' . implode(', ', $valid_vote_types)); + } + + // Check if Contribution exists + $stmt = $pdo->prepare("SELECT contribution_id FROM contributions WHERE contribution_id = :id"); + $stmt->execute([':id' => $input['contribution_id']]); + if (!$stmt->fetch()) { + error_response('Contribution not found.', 404); + } + + try { + $stmt = $pdo->prepare(" + INSERT INTO votes (contribution_id, voter_name, vote_type) + VALUES (:cid, :voter, :vtype) + "); + + $stmt->execute([ + ':cid' => $input['contribution_id'], + ':voter' => $input['voter_name'], + ':vtype' => $input['vote_type'] + ]); + + json_response(['message' => 'Vote recorded successfully.'], 201); + + } catch (PDOException $e) { + // UNIQUE Constraint Violation — Voter already voted on this Contribution + if ($e->getCode() == '23505') { + error_response('You have already voted on this Contribution.', 409); + } + error_response('Database Error: ' . $e->getMessage(), 500); + } +} \ No newline at end of file