feat: update area management to implement hard delete and adjust related references

This commit is contained in:
gabdevele
2026-06-15 15:50:12 +02:00 Unverified
parent bf85168914
commit 4f80d7f995
9 changed files with 30 additions and 24 deletions
+2 -1
View File
@@ -46,7 +46,7 @@ Authorization: Bearer <token>
## Public Reference / Public Access ## Public Reference / Public Access
- `GET /areas` - `GET /areas`
active municipal areas municipal areas
- `GET /interventions/{token}` - `GET /interventions/{token}`
public external-team summary link public external-team summary link
- `GET /open-data/reports` - `GET /open-data/reports`
@@ -115,6 +115,7 @@ Authorization: Bearer <token>
- `POST /admin/areas` - `POST /admin/areas`
- `PATCH /admin/areas/{id}` - `PATCH /admin/areas/{id}`
- `DELETE /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 area polygon must stay inside allowed Bari municipal boundary
- `POST /admin/teams` - `POST /admin/teams`
- `PATCH /admin/teams/{id}` - `PATCH /admin/teams/{id}`
+4 -2
View File
@@ -28,7 +28,7 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB
- `report_categories` - `report_categories`
citizen-facing report categories, `active` citizen-facing report categories, `active`
- `areas` - `areas`
named municipal areas, GeoJSON polygon in `boundary_geojson`, `active` named municipal areas, GeoJSON polygon in `boundary_geojson`
- `team_categories` - `team_categories`
category of external intervention team category of external intervention team
- `teams` - `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 - `reward_events(report_id, reason)` unique
- `api_tokens.token_hash` unique - `api_tokens.token_hash` unique
- `intervention_access_links.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 ## Current Query Model
- No generated ORM models - No generated ORM models
+1 -1
View File
@@ -41,7 +41,7 @@ public revocable link generated by operator/admin.
1. Register/login. 1. Register/login.
2. `POST /reports` with category, coordinates, address, description, optional media array. 2. `POST /reports` with category, coordinates, address, description, optional media array.
3. Backend first rejects coordinates outside Bari municipal boundary. 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. 5. Report starts `OPEN`, status history row inserted.
6. Citizen can: 6. Citizen can:
- list own reports - list own reports
+2 -2
View File
@@ -82,14 +82,14 @@ moderate abuse, and inspect operational stats.
- soft-delete categories - soft-delete categories
- create/update areas - create/update areas
area polygons must stay inside Bari municipal boundary area polygons must stay inside Bari municipal boundary
- soft-delete areas - delete areas
- create/update teams - create/update teams
- soft-delete teams - soft-delete teams
- list users, update roles, ban/unban - list users, update roles, ban/unban
- global stats including area/category/team resolution aggregates - global stats including area/category/team resolution aggregates
### Public Data ### Public Data
- active area list - area list
- anonymized resolved/closed open-data export - anonymized resolved/closed open-data export
- public intervention token endpoint - public intervention token endpoint
- open-data output filtered to Bari municipal boundary - open-data output filtered to Bari municipal boundary
+2 -5
View File
@@ -85,9 +85,6 @@
"Area": { "Area": {
"additionalProperties": true, "additionalProperties": true,
"properties": { "properties": {
"active": {
"type": "boolean"
},
"boundary_geojson": { "boundary_geojson": {
"$ref": "#/components/schemas/GeoJsonObject" "$ref": "#/components/schemas/GeoJsonObject"
}, },
@@ -1713,7 +1710,7 @@
}, },
"/api/v1/admin/areas/{id}": { "/api/v1/admin/areas/{id}": {
"delete": { "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", "operationId": "AdminController.deleteArea",
"parameters": [ "parameters": [
{ {
@@ -2268,7 +2265,7 @@
}, },
"/api/v1/areas": { "/api/v1/areas": {
"get": { "get": {
"description": "Lists active municipal areas.", "description": "Lists municipal areas.",
"operationId": "PublicController.listAreas", "operationId": "PublicController.listAreas",
"parameters": [ ], "parameters": [ ],
"responses": { "responses": {
+2 -3
View File
@@ -82,7 +82,6 @@ CREATE TABLE IF NOT EXISTS areas (
name VARCHAR(120) NOT NULL, name VARCHAR(120) NOT NULL,
description TEXT NULL, description TEXT NULL,
boundary_geojson JSON NOT NULL, boundary_geojson JSON NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE KEY areas_name_unique (name) UNIQUE KEY areas_name_unique (name)
); );
@@ -102,7 +101,7 @@ CREATE TABLE IF NOT EXISTS teams (
active BOOLEAN NOT NULL DEFAULT TRUE, active BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE KEY teams_name_unique (name), UNIQUE KEY teams_name_unique (name),
CONSTRAINT teams_category_fk FOREIGN KEY (team_category_id) REFERENCES team_categories(id), 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 ( 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, 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_user_fk FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT reports_category_fk FOREIGN KEY (category_id) REFERENCES report_categories(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_status_idx (status),
KEY reports_category_idx (category_id), KEY reports_category_idx (category_id),
KEY reports_area_idx (area_id) KEY reports_area_idx (area_id)
@@ -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;
+3 -7
View File
@@ -444,7 +444,9 @@ Json::Value AdminService::deleteArea(const auth::UserContext &user,
{ {
return failure("area_not_found"); 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(); return success();
} }
@@ -474,12 +476,6 @@ Json::Value AdminService::updateArea(const auth::UserContext &user,
body["description"].asString(), body["description"].asString(),
areaId); areaId);
} }
if (isActiveField(body))
{
db->execSqlSync("UPDATE areas SET active = ? WHERE id = ?",
body["active"].asBool(),
areaId);
}
if (body["boundary_geojson"].isObject()) if (body["boundary_geojson"].isObject())
{ {
if (!isAreaInsideAllowedBoundary(body["boundary_geojson"])) if (!isAreaInsideAllowedBoundary(body["boundary_geojson"]))
+2 -3
View File
@@ -107,7 +107,7 @@ std::optional<long long> findArea(double latitude, double longitude)
{ {
const auto result = drogon::app().getDbClient()->execSqlSync( const auto result = drogon::app().getDbClient()->execSqlSync(
"SELECT id, JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson " "SELECT id, JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
"FROM areas WHERE active = TRUE"); "FROM areas");
Json::CharReaderBuilder builder; Json::CharReaderBuilder builder;
for (const auto &row : result) for (const auto &row : result)
@@ -647,8 +647,7 @@ Json::Value ReportService::listAreas()
{ {
auto response = success(); auto response = success();
response["areas"] = repositories::resultToJson( response["areas"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync( drogon::app().getDbClient()->execSqlSync("SELECT * FROM areas ORDER BY name"));
"SELECT * FROM areas WHERE active = TRUE ORDER BY name"));
return response; return response;
} }