Issue Resolution Flow
Once an issue is filed it follows a structured lifecycle designed to create accountability and prevent premature closure.
Status lifecycleβ
open βββΊ in_progress βββΊ resolved βββΊ closed (terminal)
β
ββββ reopen request approved βββΊ open
All status transitions are guarded on the server. Attempting an invalid transition (e.g. resolving an already-resolved issue) returns 409 Conflict.
Roles and permissionsβ
| Action | Who can do it |
|---|---|
| Mark resolved | Reporter of the issue or admin/superadmin |
| Rate a resolution | Any authenticated user except the reporter |
| Request reopen | Any non-viewer authenticated user |
| Review reopen request | Reporter or admin/superadmin |
| Close issue | Reporter or admin/superadmin |
POST /issues/:id/resolveβ
Mark an open or in-progress issue as resolved. Requires a description of the fix and at least one proof media item already uploaded to the issue.
Auth required: Yes β reporter or admin
Request bodyβ
{
"description": "Pothole filled with bitumen by BBMP Roads crew on 28 Feb. Area cordoned off for 24 hrs to cure.",
"mediaIds": ["media-abc123", "media-def456"]
}
| Field | Type | Rules |
|---|---|---|
description | string | 10β5000 characters |
mediaIds | string[] | At least 1 item; each ID must be a media item already attached to this issue |
Response 201 Createdβ
{
"success": true,
"data": {
"issueId": "ISS-001",
"status": "resolved",
"resolution": {
"description": "Pothole filled with bitumenβ¦",
"mediaIds": ["media-abc123", "media-def456"],
"resolvedBy": { "id": "u1", "name": "Rahul Sharma", "role": "admin" },
"resolvedAt": "2026-03-01T09:45:00Z",
"ratings": [],
"reopenRequests": []
}
}
}
Errorsβ
| Status | Cause |
|---|---|
| 403 | Caller is not the reporter or an admin |
| 409 | Issue is already resolved or closed |
| 422 | mediaIds is empty or description too short |
POST /issues/:id/rate-resolutionβ
Rate the quality of a resolution thumbs-up or thumbs-down. A user can change their vote by calling this endpoint again with a different value.
Auth required: Yes β any user except the original reporter
Request bodyβ
{ "vote": "up" }
| Field | Type | Values |
|---|---|---|
vote | string | up | down |
Response 200 OKβ
{
"success": true,
"data": {
"issueId": "ISS-001",
"resolution": {
"ratings": [
{ "userId": "u2", "vote": "up", "at": "2026-03-02T11:00:00Z" }
]
}
}
}
Errorsβ
| Status | Cause |
|---|---|
| 403 | Reporter trying to rate their own issue |
| 409 | Issue is not in resolved status |
POST /issues/:id/reopen-requestβ
Submit a request to reopen a resolved issue (e.g. the fix didn't hold, or new evidence has emerged). Only admins can approve/deny reopen requests.
Auth required: Yes β any non-viewer user
Request bodyβ
{
"reason": "The pothole has reappeared after the first rain. The fix was superficial and did not address the root cause."
}
| Field | Type | Rules |
|---|---|---|
reason | string | 5β1000 characters |
Response 201 Createdβ
{
"success": true,
"data": {
"requestId": "req-7f3a",
"issue": {
"issueId": "ISS-001",
"resolution": {
"reopenRequests": [
{
"id": "req-7f3a",
"userId": "u2",
"userName": "Priya Nair",
"reason": "The pothole has reappearedβ¦",
"at": "2026-03-02T12:00:00Z",
"status": "pending"
}
]
}
}
}
}
Errorsβ
| Status | Cause |
|---|---|
| 403 | User has viewer role |
| 409 | Issue is closed (terminal β cannot be reopened) |
PATCH /issues/:id/reopen-request/:reqIdβ
Approve or deny a pending reopen request.
- Approve β reverts the issue to
open, removes the resolution object entirely, and marks the request asapproved. - Deny β sets the request's
statustodenied; issue remainsresolved.
Auth required: Yes β reporter or admin
Request bodyβ
{ "action": "approve" }
| Field | Type | Values |
|---|---|---|
action | string | approve | deny |
Response 200 OK (approve)β
{
"success": true,
"data": {
"issueId": "ISS-001",
"status": "open",
"resolution": null
}
}
Errorsβ
| Status | Cause |
|---|---|
| 403 | Not the reporter or an admin |
| 404 | Request ID not found on this issue |
| 409 | Request has already been reviewed (not pending) |
PATCH /issues/:id/closeβ
Permanently close a resolved issue. This is a terminal state β closed issues cannot be reopened or modified.
Typically triggered by the reporter once they are satisfied, or by an admin after the 7-day auto-close window.
Auth required: Yes β reporter or admin
Request bodyβ
{ "source": "owner" }
| Field | Type | Values | Meaning |
|---|---|---|---|
source | string | owner | Reporter manually closed it |
admin | Admin closed it | ||
auto | System closed it after 7-day window |
Response 200 OKβ
{
"success": true,
"data": {
"issueId": "ISS-001",
"status": "closed",
"closedAt": "2026-03-08T09:45:00Z",
"closedBy": "owner"
}
}
Errorsβ
| Status | Cause |
|---|---|
| 403 | Not the reporter or an admin |
| 409 | Issue is not in resolved status (must be resolved before closing) |
Auto-closeβ
The frontend fires a close request automatically if a resolved issue's resolution.resolvedAt is more than 7 days old. The source for these requests is "auto".
In production, this would be a scheduled Lambda/cron function that scans for resolved issues older than 7 days and calls PATCH /issues/:id/close with { "source": "auto" }.