-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmicro-boxes.json
More file actions
332 lines (332 loc) · 21.1 KB
/
micro-boxes.json
File metadata and controls
332 lines (332 loc) · 21.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
{
"version": "1.0",
"basePackage": "com.agency.api",
"workpack": "cf923b52-5e1e-4660-a290-53f8131a18be",
"boxes": [
{
"id": "P1",
"title": "Create Project",
"series": "PROJECT",
"depends": [],
"outputFiles": [
"src/main/java/com/agency/api/project/dto/ProjectRequest.java",
"src/main/java/com/agency/api/project/dto/ProjectResponse.java",
"src/main/java/com/agency/api/project/ProjectService.java",
"src/main/java/com/agency/api/project/ProjectResource.java"
],
"purpose": "Implement the POST endpoint that creates a new project scoped to the current tenant.",
"inputContext": [
"Project entity at com.agency.api.project.model.Project with fields: id (UUID), tenantId (UUID), name (String), description (String), createdAt (Instant), updatedAt (Instant)",
"TenantContext bean (CDI, @RequestScoped) exposes getTenantId(): UUID",
"AuthorizationService.requireRole(String role) throws WebApplicationException(403) if caller lacks role",
"Quarkus 3 + Hibernate ORM + Panache + Jakarta REST + Jackson",
"Base package: com.agency.api"
],
"instructions": [
"Create record ProjectRequest(String name, String description) in com.agency.api.project.dto. Add @NotBlank on name.",
"Create record ProjectResponse(UUID id, String name, String description, Instant createdAt) in the same package. Add a static of(Project p) factory method.",
"In ProjectService (@ApplicationScoped), add method ProjectResponse create(ProjectRequest req): build a Project, set tenantId from TenantContext, persist with entity.persist(), return ProjectResponse.of(entity).",
"In ProjectResource (@Path(\"/api/projects\"), @Produces(APPLICATION_JSON), @Consumes(APPLICATION_JSON)), add @POST method: call authorizationService.requireRole(\"ADMIN\"), delegate to projectService.create(req), return Response.status(201).entity(dto).build().",
"Add entry to requests.http: POST {{baseUrl}}/api/projects with X-Tenant-Id header and JSON body {\"name\":\"Acme\",\"description\":\"Demo project\"}."
],
"acceptanceCriteria": [
"POST /api/projects with valid body returns 201 and JSON with id and createdAt.",
"POST /api/projects without name returns 400.",
"Calling without ADMIN role returns 403."
]
},
{
"id": "P2",
"title": "List Projects",
"series": "PROJECT",
"depends": ["P1"],
"outputFiles": [
"src/main/java/com/agency/api/project/ProjectRepository.java",
"src/main/java/com/agency/api/project/ProjectService.java",
"src/main/java/com/agency/api/project/ProjectResource.java"
],
"purpose": "Implement GET endpoint that returns all projects belonging to the current tenant, ordered newest-first.",
"inputContext": [
"Project entity and ProjectResponse DTO already exist from P1",
"TenantContext provides getTenantId(): UUID",
"ProjectRepository extends PanacheRepository<Project>",
"Panache list() method signature: list(String query, Object... params)"
],
"instructions": [
"In ProjectRepository (@ApplicationScoped, extends PanacheRepository<Project>), add method: List<Project> findByTenant(UUID tenantId) { return list(\"tenantId = ?1 ORDER BY createdAt DESC\", tenantId); }",
"In ProjectService, add List<ProjectResponse> list(): call projectRepository.findByTenant(tenantContext.getTenantId()), map each with ProjectResponse.of(p), return the list.",
"In ProjectResource, add @GET method: call projectService.list(), return Response.ok(dtos).build().",
"Add entry to requests.http: GET {{baseUrl}}/api/projects with X-Tenant-Id header."
],
"acceptanceCriteria": [
"GET /api/projects returns 200 and a JSON array (may be empty).",
"Projects from other tenants never appear in the response.",
"Order is newest-first (createdAt DESC)."
]
},
{
"id": "P3",
"title": "Get Project by ID",
"series": "PROJECT",
"depends": ["P2"],
"outputFiles": [
"src/main/java/com/agency/api/project/ProjectRepository.java",
"src/main/java/com/agency/api/project/ProjectService.java",
"src/main/java/com/agency/api/project/ProjectResource.java"
],
"purpose": "Implement GET endpoint that retrieves a single project by ID, scoped to the current tenant.",
"inputContext": [
"ProjectRepository already exists from P2",
"ProjectResponse DTO already exists from P1",
"Return 404 if project not found or belongs to a different tenant",
"Jakarta NotFoundException maps to HTTP 404 automatically in Quarkus"
],
"instructions": [
"In ProjectRepository, add: Optional<Project> findByIdAndTenant(UUID id, UUID tenantId) { return find(\"id = ?1 and tenantId = ?2\", id, tenantId).firstResultOptional(); }",
"In ProjectService, add ProjectResponse getById(UUID id): call projectRepository.findByIdAndTenant(id, tenantContext.getTenantId()), throw new NotFoundException() if empty, else return ProjectResponse.of(entity).",
"In ProjectResource, add @GET @Path(\"/{id}\") method with @PathParam(\"id\") UUID id: call projectService.getById(id), return Response.ok(dto).build().",
"Add entry to requests.http: GET {{baseUrl}}/api/projects/{{projectId}}."
],
"acceptanceCriteria": [
"GET /api/projects/{id} returns 200 with the project JSON when it belongs to the current tenant.",
"Returns 404 when the ID does not exist or belongs to another tenant."
]
},
{
"id": "P4",
"title": "Update Project",
"series": "PROJECT",
"depends": ["P3"],
"outputFiles": [
"src/main/java/com/agency/api/project/dto/ProjectUpdateRequest.java",
"src/main/java/com/agency/api/project/ProjectService.java",
"src/main/java/com/agency/api/project/ProjectResource.java"
],
"purpose": "Implement PATCH endpoint to update name and/or description of an existing project.",
"inputContext": [
"ProjectService.getById(UUID id) already exists from P3 — but for mutation, re-fetch the managed entity directly from ProjectRepository",
"Only ADMIN may update",
"Null fields in the request mean no change (partial update)"
],
"instructions": [
"Create record ProjectUpdateRequest(String name, String description) in com.agency.api.project.dto. Both fields are nullable — no @NotBlank.",
"In ProjectService, add ProjectResponse update(UUID id, ProjectUpdateRequest req): fetch the managed Project with projectRepository.findByIdAndTenant(id, tenantContext.getTenantId()).orElseThrow(NotFoundException::new), apply non-null fields (if req.name() != null set entity.setName(req.name()), same for description), return ProjectResponse.of(entity). Panache merges automatically within the transaction.",
"In ProjectResource, add @PATCH @Path(\"/{id}\") @Transactional method: call authorizationService.requireRole(\"ADMIN\"), delegate to projectService.update(id, req), return Response.ok(dto).build().",
"Add entry to requests.http: PATCH {{baseUrl}}/api/projects/{{projectId}} with partial JSON body {\"name\":\"New Name\"}."
],
"acceptanceCriteria": [
"PATCH /api/projects/{id} with {\"name\":\"New Name\"} returns 200 and updated JSON; description unchanged.",
"Returns 404 for unknown project or cross-tenant ID.",
"Returns 403 without ADMIN role."
]
},
{
"id": "P5",
"title": "Delete Project",
"series": "PROJECT",
"depends": ["P3"],
"outputFiles": [
"src/main/java/com/agency/api/project/ProjectRepository.java",
"src/main/java/com/agency/api/project/ProjectService.java",
"src/main/java/com/agency/api/project/ProjectResource.java"
],
"purpose": "Implement DELETE endpoint to permanently remove a project owned by the current tenant.",
"inputContext": [
"ProjectRepository already exists from P2",
"Only ADMIN may delete",
"Return 204 No Content on success",
"Return 404 if project not found or cross-tenant"
],
"instructions": [
"In ProjectRepository, add: boolean deleteByIdAndTenant(UUID id, UUID tenantId) { long count = delete(\"id = ?1 and tenantId = ?2\", id, tenantId); return count > 0; }",
"In ProjectService, add void delete(UUID id): call projectRepository.deleteByIdAndTenant(id, tenantContext.getTenantId()); if returns false, throw new NotFoundException().",
"In ProjectResource, add @DELETE @Path(\"/{id}\") @Transactional method: call authorizationService.requireRole(\"ADMIN\"), call projectService.delete(id), return Response.noContent().build().",
"Add entry to requests.http: DELETE {{baseUrl}}/api/projects/{{projectId}}."
],
"acceptanceCriteria": [
"DELETE /api/projects/{id} returns 204 and the project no longer appears in GET list.",
"Returns 404 for unknown or cross-tenant ID.",
"Returns 403 without ADMIN role."
]
},
{
"id": "T1",
"title": "Create Task",
"series": "TASK",
"depends": ["P3"],
"outputFiles": [
"src/main/java/com/agency/api/task/dto/TaskCreateRequest.java",
"src/main/java/com/agency/api/task/dto/TaskResponse.java",
"src/main/java/com/agency/api/task/TaskService.java",
"src/main/java/com/agency/api/task/TaskResource.java"
],
"purpose": "Implement POST endpoint to create a task within a project, scoped to tenant.",
"inputContext": [
"Task entity at com.agency.api.task.model.Task with fields: id (UUID), tenantId (UUID), projectId (UUID), title (String), description (String), status (TaskStatus enum: TODO/IN_PROGRESS/DONE), priority (TaskPriority enum: LOW/MEDIUM/HIGH), assigneeId (UUID nullable), createdAt (Instant), updatedAt (Instant)",
"TaskStatus enum at com.agency.api.task.model.TaskStatus",
"TaskPriority enum at com.agency.api.task.model.TaskPriority",
"TenantContext and AuthorizationService same as Project series",
"ProjectRepository.findByIdAndTenant(UUID id, UUID tenantId) already exists from P3 — use it to validate the project exists",
"Base path for this resource: /api/projects/{projectId}/tasks",
"New tasks always start with status = TaskStatus.TODO"
],
"instructions": [
"Create record TaskCreateRequest(@NotBlank String title, String description, TaskPriority priority, UUID assigneeId) in com.agency.api.task.dto.",
"Create record TaskResponse(UUID id, UUID projectId, String title, String description, TaskStatus status, TaskPriority priority, UUID assigneeId, Instant createdAt) in same package. Add static of(Task t) factory.",
"In TaskService (@ApplicationScoped), add TaskResponse create(UUID projectId, TaskCreateRequest req): verify project with projectRepository.findByIdAndTenant(projectId, tenantContext.getTenantId()).orElseThrow(NotFoundException::new), build Task setting tenantId, projectId, status=TODO, persist, return TaskResponse.of(entity).",
"In TaskResource (@Path(\"/api/projects/{projectId}/tasks\")), inject @PathParam UUID projectId as field, add @POST method: call authorizationService.requireRole(\"MEMBER\"), delegate to taskService.create(projectId, req), return Response.status(201).entity(dto).build().",
"Add requests.http entry: POST {{baseUrl}}/api/projects/{{projectId}}/tasks with body {\"title\":\"Fix login bug\",\"priority\":\"HIGH\"}."
],
"acceptanceCriteria": [
"POST /api/projects/{projectId}/tasks returns 201 with status: \"TODO\".",
"Returns 404 if projectId does not exist or belongs to another tenant.",
"Returns 400 if title is blank."
]
},
{
"id": "T2",
"title": "List Tasks",
"series": "TASK",
"depends": ["T1"],
"outputFiles": [
"src/main/java/com/agency/api/task/TaskRepository.java",
"src/main/java/com/agency/api/task/TaskService.java",
"src/main/java/com/agency/api/task/TaskResource.java"
],
"purpose": "Implement GET endpoint to list all tasks in a project with optional status filter.",
"inputContext": [
"TaskResponse DTO already exists from T1",
"TaskRepository extends PanacheRepository<Task>",
"Optional query param ?status=TODO|IN_PROGRESS|DONE — if absent, return all",
"Results ordered by createdAt DESC"
],
"instructions": [
"In TaskRepository (@ApplicationScoped, extends PanacheRepository<Task>), add: List<Task> findByProject(UUID projectId, UUID tenantId) { return list(\"projectId = ?1 and tenantId = ?2 ORDER BY createdAt DESC\", projectId, tenantId); }",
"Add: List<Task> findByProjectAndStatus(UUID projectId, UUID tenantId, TaskStatus status) { return list(\"projectId = ?1 and tenantId = ?2 and status = ?3 ORDER BY createdAt DESC\", projectId, tenantId, status); }",
"In TaskService, add List<TaskResponse> list(UUID projectId, TaskStatus statusFilter): if statusFilter == null call findByProject, else call findByProjectAndStatus; map to TaskResponse and return.",
"In TaskResource, add @GET method with @QueryParam(\"status\") TaskStatus status: call taskService.list(projectId, status), return Response.ok(dtos).build().",
"Add requests.http entry: GET {{baseUrl}}/api/projects/{{projectId}}/tasks?status=TODO."
],
"acceptanceCriteria": [
"GET /api/projects/{projectId}/tasks returns 200 with all tasks for that project.",
"?status=DONE filters correctly; other statuses excluded.",
"Returns [] (not 404) when no tasks exist; tasks from other tenants never appear."
]
},
{
"id": "T3",
"title": "Get Task by ID",
"series": "TASK",
"depends": ["T2"],
"outputFiles": [
"src/main/java/com/agency/api/task/TaskRepository.java",
"src/main/java/com/agency/api/task/TaskService.java",
"src/main/java/com/agency/api/task/TaskResource.java"
],
"purpose": "Implement GET endpoint to retrieve a single task by ID within a project.",
"inputContext": [
"TaskRepository already exists from T2",
"TaskResponse DTO already exists from T1",
"Must verify taskId, projectId, and tenantId all match — returning 404 on any mismatch"
],
"instructions": [
"In TaskRepository, add: Optional<Task> findByIdAndProject(UUID taskId, UUID projectId, UUID tenantId) { return find(\"id = ?1 and projectId = ?2 and tenantId = ?3\", taskId, projectId, tenantId).firstResultOptional(); }",
"In TaskService, add TaskResponse getById(UUID projectId, UUID taskId): call taskRepository.findByIdAndProject(taskId, projectId, tenantContext.getTenantId()).orElseThrow(NotFoundException::new), return TaskResponse.of(entity).",
"In TaskResource, add @GET @Path(\"/{taskId}\") method with @PathParam UUID taskId: call taskService.getById(projectId, taskId), return Response.ok(dto).build().",
"Add requests.http entry: GET {{baseUrl}}/api/projects/{{projectId}}/tasks/{{taskId}}."
],
"acceptanceCriteria": [
"Returns 200 with full task JSON when task belongs to the project and tenant.",
"Returns 404 if task ID is unknown, belongs to another project, or another tenant."
]
},
{
"id": "T4",
"title": "Update Task",
"series": "TASK",
"depends": ["T3"],
"outputFiles": [
"src/main/java/com/agency/api/task/dto/TaskUpdateRequest.java",
"src/main/java/com/agency/api/task/TaskService.java",
"src/main/java/com/agency/api/task/TaskResource.java"
],
"purpose": "Implement PATCH endpoint to update title, description, priority, or assignee of a task. Status changes are NOT allowed here.",
"inputContext": [
"TaskService.getById already exists from T3 but returns DTO; for mutation, re-fetch the managed Task entity directly from TaskRepository",
"All fields in the request are nullable — null means no change",
"Do NOT allow changing status through this endpoint — status has its own endpoint (T6)"
],
"instructions": [
"Create record TaskUpdateRequest(String title, String description, TaskPriority priority, UUID assigneeId) in com.agency.api.task.dto. All fields nullable.",
"In TaskService, add TaskResponse update(UUID projectId, UUID taskId, TaskUpdateRequest req): fetch managed entity with taskRepository.findByIdAndProject(taskId, projectId, tenantContext.getTenantId()).orElseThrow(NotFoundException::new), apply non-null fields (if req.title() != null entity.setTitle(req.title()), same for description, priority, assigneeId — never touch status), return TaskResponse.of(entity).",
"In TaskResource, add @PATCH @Path(\"/{taskId}\") @Transactional method: call authorizationService.requireRole(\"MEMBER\"), delegate to taskService.update(projectId, taskId, req), return Response.ok(dto).build().",
"Add requests.http entry: PATCH {{baseUrl}}/api/projects/{{projectId}}/tasks/{{taskId}} with body {\"priority\":\"HIGH\"}."
],
"acceptanceCriteria": [
"PATCH with {\"priority\":\"HIGH\"} returns 200; only priority changes, status and title are unchanged.",
"A status field in the request body has no effect on the stored status.",
"Returns 404 for unknown task."
]
},
{
"id": "T5",
"title": "Delete Task",
"series": "TASK",
"depends": ["T3"],
"outputFiles": [
"src/main/java/com/agency/api/task/TaskRepository.java",
"src/main/java/com/agency/api/task/TaskService.java",
"src/main/java/com/agency/api/task/TaskResource.java"
],
"purpose": "Implement DELETE endpoint to permanently remove a task from a project.",
"inputContext": [
"TaskRepository already exists from T2",
"Only ADMIN may delete tasks",
"Return 204 No Content on success",
"Return 404 if task not found, wrong project, or cross-tenant"
],
"instructions": [
"In TaskRepository, add: boolean deleteByIdAndProject(UUID taskId, UUID projectId, UUID tenantId) { long count = delete(\"id = ?1 and projectId = ?2 and tenantId = ?3\", taskId, projectId, tenantId); return count > 0; }",
"In TaskService, add void delete(UUID projectId, UUID taskId): call taskRepository.deleteByIdAndProject(taskId, projectId, tenantContext.getTenantId()); if returns false, throw new NotFoundException().",
"In TaskResource, add @DELETE @Path(\"/{taskId}\") @Transactional method: call authorizationService.requireRole(\"ADMIN\"), call taskService.delete(projectId, taskId), return Response.noContent().build().",
"Add requests.http entry: DELETE {{baseUrl}}/api/projects/{{projectId}}/tasks/{{taskId}}."
],
"acceptanceCriteria": [
"DELETE returns 204 and task no longer appears in GET list.",
"Returns 404 for unknown task or cross-tenant.",
"Returns 403 without ADMIN role."
]
},
{
"id": "T6",
"title": "Task Status Transition",
"series": "TASK",
"depends": ["T3"],
"outputFiles": [
"src/main/java/com/agency/api/task/dto/TaskStatusRequest.java",
"src/main/java/com/agency/api/task/TaskService.java",
"src/main/java/com/agency/api/task/TaskResource.java"
],
"purpose": "Implement PATCH /status endpoint to transition a task through its lifecycle. Valid: TODO→IN_PROGRESS, IN_PROGRESS→DONE. All other transitions return 422.",
"inputContext": [
"TaskStatus enum: TODO, IN_PROGRESS, DONE",
"Valid forward transitions only: TODO→IN_PROGRESS, IN_PROGRESS→DONE",
"Invalid transitions (skipping a state, or going backwards) return HTTP 422 Unprocessable Entity",
"WebApplicationException(422) is the correct way to return 422 in Quarkus/Jakarta REST",
"Any MEMBER or ADMIN may trigger status transitions"
],
"instructions": [
"Create record TaskStatusRequest(@NotNull TaskStatus status) in com.agency.api.task.dto.",
"Add private method boolean isValidTransition(TaskStatus from, TaskStatus to) in TaskService: return (from == TODO && to == IN_PROGRESS) || (from == IN_PROGRESS && to == DONE). All other combinations return false.",
"In TaskService, add TaskResponse transition(UUID projectId, UUID taskId, TaskStatus newStatus): fetch managed Task entity (same as T4), call isValidTransition — if false throw new WebApplicationException(422), else set entity.setStatus(newStatus), return TaskResponse.of(entity).",
"In TaskResource, add @PATCH @Path(\"/{taskId}/status\") @Transactional method: call authorizationService.requireRole(\"MEMBER\"), delegate to taskService.transition(projectId, taskId, req.status()), return Response.ok(dto).build().",
"Add two requests.http entries: PATCH .../tasks/{{taskId}}/status with {\"status\":\"IN_PROGRESS\"} and {\"status\":\"DONE\"}."
],
"acceptanceCriteria": [
"TODO→IN_PROGRESS returns 200 with updated status.",
"IN_PROGRESS→DONE returns 200 with updated status.",
"TODO→DONE and DONE→TODO both return 422."
]
}
]
}