diff --git a/docs/API_SPEC.md b/docs/API_SPEC.md index daa02de..b50b98d 100644 --- a/docs/API_SPEC.md +++ b/docs/API_SPEC.md @@ -46,7 +46,7 @@ Authorization: Bearer ## Public Reference / Public Access - `GET /areas` - active municipal areas + municipal areas - `GET /interventions/{token}` public external-team summary link - `GET /open-data/reports` @@ -115,6 +115,7 @@ Authorization: Bearer - `POST /admin/areas` - `PATCH /admin/areas/{id}` - `DELETE /admin/areas/{id}` + deletes area and clears linked `reports.area_id` and `teams.area_id` area polygon must stay inside allowed Bari municipal boundary - `POST /admin/teams` - `PATCH /admin/teams/{id}` diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 0ab5a32..03c061a 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -28,7 +28,7 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB - `report_categories` citizen-facing report categories, `active` - `areas` - named municipal areas, GeoJSON polygon in `boundary_geojson`, `active` + named municipal areas, GeoJSON polygon in `boundary_geojson` - `team_categories` category of external intervention team - `teams` @@ -92,7 +92,9 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB - `reward_events(report_id, reason)` unique - `api_tokens.token_hash` unique - `intervention_access_links.token_hash` unique -- category/team/area delete behavior is soft delete via `active = FALSE` +- category/team delete behavior is soft delete via `active = FALSE` +- area delete behavior is hard delete; linked `reports.area_id` and `teams.area_id` + become `NULL` ## Current Query Model - No generated ORM models diff --git a/docs/LLM_CONTEXT.md b/docs/LLM_CONTEXT.md index 80411b4..ae612e8 100644 --- a/docs/LLM_CONTEXT.md +++ b/docs/LLM_CONTEXT.md @@ -41,7 +41,7 @@ public revocable link generated by operator/admin. 1. Register/login. 2. `POST /reports` with category, coordinates, address, description, optional media array. 3. Backend first rejects coordinates outside Bari municipal boundary. -4. Backend infers `area_id` by GeoJSON polygon match when possible. +4. Backend infers `area_id` by GeoJSON polygon match when possible from existing areas. 5. Report starts `OPEN`, status history row inserted. 6. Citizen can: - list own reports diff --git a/docs/PROJECT_SPEC.md b/docs/PROJECT_SPEC.md index 2d87084..f00ad1d 100644 --- a/docs/PROJECT_SPEC.md +++ b/docs/PROJECT_SPEC.md @@ -82,14 +82,14 @@ moderate abuse, and inspect operational stats. - soft-delete categories - create/update areas area polygons must stay inside Bari municipal boundary -- soft-delete areas +- delete areas - create/update teams - soft-delete teams - list users, update roles, ban/unban - global stats including area/category/team resolution aggregates ### Public Data -- active area list +- area list - anonymized resolved/closed open-data export - public intervention token endpoint - open-data output filtered to Bari municipal boundary diff --git a/docs/openapi/visione.openapi.yaml b/docs/openapi/visione.openapi.yaml index 2f9fe76..79bdad4 100644 --- a/docs/openapi/visione.openapi.yaml +++ b/docs/openapi/visione.openapi.yaml @@ -85,9 +85,6 @@ "Area": { "additionalProperties": true, "properties": { - "active": { - "type": "boolean" - }, "boundary_geojson": { "$ref": "#/components/schemas/GeoJsonObject" }, @@ -1713,7 +1710,7 @@ }, "/api/v1/admin/areas/{id}": { "delete": { - "description": "Soft-deletes an existing municipal area by setting active to false.", + "description": "Deletes an existing municipal area and clears linked report/team area references.", "operationId": "AdminController.deleteArea", "parameters": [ { @@ -2268,7 +2265,7 @@ }, "/api/v1/areas": { "get": { - "description": "Lists active municipal areas.", + "description": "Lists municipal areas.", "operationId": "PublicController.listAreas", "parameters": [ ], "responses": { diff --git a/migrations/001_initial_schema.sql b/migrations/001_initial_schema.sql index e1ea717..5d6fcb3 100644 --- a/migrations/001_initial_schema.sql +++ b/migrations/001_initial_schema.sql @@ -82,7 +82,6 @@ CREATE TABLE IF NOT EXISTS areas ( name VARCHAR(120) NOT NULL, description TEXT NULL, boundary_geojson JSON NOT NULL, - active BOOLEAN NOT NULL DEFAULT TRUE, UNIQUE KEY areas_name_unique (name) ); @@ -102,7 +101,7 @@ CREATE TABLE IF NOT EXISTS teams ( active BOOLEAN NOT NULL DEFAULT TRUE, UNIQUE KEY teams_name_unique (name), CONSTRAINT teams_category_fk FOREIGN KEY (team_category_id) REFERENCES team_categories(id), - CONSTRAINT teams_area_fk FOREIGN KEY (area_id) REFERENCES areas(id) + CONSTRAINT teams_area_fk FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS reports ( @@ -119,7 +118,7 @@ CREATE TABLE IF NOT EXISTS reports ( updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT reports_user_fk FOREIGN KEY (user_id) REFERENCES users(id), CONSTRAINT reports_category_fk FOREIGN KEY (category_id) REFERENCES report_categories(id), - CONSTRAINT reports_area_fk FOREIGN KEY (area_id) REFERENCES areas(id), + CONSTRAINT reports_area_fk FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE SET NULL, KEY reports_status_idx (status), KEY reports_category_idx (category_id), KEY reports_area_idx (area_id) diff --git a/migrations/009_drop_area_active_soft_delete.sql b/migrations/009_drop_area_active_soft_delete.sql new file mode 100644 index 0000000..921e767 --- /dev/null +++ b/migrations/009_drop_area_active_soft_delete.sql @@ -0,0 +1,12 @@ +ALTER TABLE teams DROP FOREIGN KEY teams_area_fk; +ALTER TABLE reports DROP FOREIGN KEY reports_area_fk; + +ALTER TABLE areas DROP COLUMN active; + +ALTER TABLE teams + ADD CONSTRAINT teams_area_fk + FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE SET NULL; + +ALTER TABLE reports + ADD CONSTRAINT reports_area_fk + FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE SET NULL; diff --git a/services/AdminService.cc b/services/AdminService.cc index 413a31e..ba72bff 100644 --- a/services/AdminService.cc +++ b/services/AdminService.cc @@ -444,7 +444,9 @@ Json::Value AdminService::deleteArea(const auth::UserContext &user, { return failure("area_not_found"); } - db->execSqlSync("UPDATE areas SET active = FALSE WHERE id = ?", areaId); + db->execSqlSync("UPDATE teams SET area_id = NULL WHERE area_id = ?", areaId); + db->execSqlSync("UPDATE reports SET area_id = NULL WHERE area_id = ?", areaId); + db->execSqlSync("DELETE FROM areas WHERE id = ?", areaId); return success(); } @@ -474,12 +476,6 @@ Json::Value AdminService::updateArea(const auth::UserContext &user, body["description"].asString(), areaId); } - if (isActiveField(body)) - { - db->execSqlSync("UPDATE areas SET active = ? WHERE id = ?", - body["active"].asBool(), - areaId); - } if (body["boundary_geojson"].isObject()) { if (!isAreaInsideAllowedBoundary(body["boundary_geojson"])) diff --git a/services/ReportService.cc b/services/ReportService.cc index a2c2966..689397d 100644 --- a/services/ReportService.cc +++ b/services/ReportService.cc @@ -107,7 +107,7 @@ std::optional findArea(double latitude, double longitude) { const auto result = drogon::app().getDbClient()->execSqlSync( "SELECT id, JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson " - "FROM areas WHERE active = TRUE"); + "FROM areas"); Json::CharReaderBuilder builder; for (const auto &row : result) @@ -647,8 +647,7 @@ Json::Value ReportService::listAreas() { auto response = success(); response["areas"] = repositories::resultToJson( - drogon::app().getDbClient()->execSqlSync( - "SELECT * FROM areas WHERE active = TRUE ORDER BY name")); + drogon::app().getDbClient()->execSqlSync("SELECT * FROM areas ORDER BY name")); return response; }