feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using Microsoft.AspNetCore.Http.HttpResults;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using WorkClub.Api.Services;
|
|
|
|
|
using WorkClub.Application.Tasks.DTOs;
|
|
|
|
|
|
|
|
|
|
namespace WorkClub.Api.Endpoints.Tasks;
|
|
|
|
|
|
|
|
|
|
public static class TaskEndpoints
|
|
|
|
|
{
|
|
|
|
|
public static void MapTaskEndpoints(this IEndpointRouteBuilder app)
|
|
|
|
|
{
|
|
|
|
|
var group = app.MapGroup("/api/tasks");
|
|
|
|
|
|
|
|
|
|
group.MapGet("", GetTasks)
|
|
|
|
|
.RequireAuthorization("RequireMember")
|
|
|
|
|
.WithName("GetTasks");
|
|
|
|
|
|
|
|
|
|
group.MapGet("{id:guid}", GetTask)
|
|
|
|
|
.RequireAuthorization("RequireMember")
|
|
|
|
|
.WithName("GetTask");
|
|
|
|
|
|
|
|
|
|
group.MapPost("", CreateTask)
|
|
|
|
|
.RequireAuthorization("RequireManager")
|
|
|
|
|
.WithName("CreateTask");
|
|
|
|
|
|
|
|
|
|
group.MapPatch("{id:guid}", UpdateTask)
|
|
|
|
|
.RequireAuthorization("RequireMember")
|
|
|
|
|
.WithName("UpdateTask");
|
|
|
|
|
|
|
|
|
|
group.MapDelete("{id:guid}", DeleteTask)
|
|
|
|
|
.RequireAuthorization("RequireAdmin")
|
|
|
|
|
.WithName("DeleteTask");
|
2026-03-09 15:47:57 +01:00
|
|
|
|
|
|
|
|
group.MapPost("{id:guid}/assign", AssignTaskToMe)
|
|
|
|
|
.RequireAuthorization("RequireMember")
|
|
|
|
|
.WithName("AssignTaskToMe");
|
|
|
|
|
|
|
|
|
|
group.MapDelete("{id:guid}/assign", UnassignTaskFromMe)
|
|
|
|
|
.RequireAuthorization("RequireMember")
|
|
|
|
|
.WithName("UnassignTaskFromMe");
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<Ok<TaskListDto>> GetTasks(
|
|
|
|
|
TaskService taskService,
|
2026-03-09 15:47:57 +01:00
|
|
|
HttpContext httpContext,
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
[FromQuery] string? status = null,
|
|
|
|
|
[FromQuery] int page = 1,
|
|
|
|
|
[FromQuery] int pageSize = 20)
|
|
|
|
|
{
|
2026-03-09 15:47:57 +01:00
|
|
|
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
|
|
|
|
var result = await taskService.GetTasksAsync(status, page, pageSize, externalUserId);
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
return TypedResults.Ok(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<Results<Ok<TaskDetailDto>, NotFound>> GetTask(
|
|
|
|
|
Guid id,
|
2026-03-09 15:47:57 +01:00
|
|
|
TaskService taskService,
|
|
|
|
|
HttpContext httpContext)
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
{
|
2026-03-09 15:47:57 +01:00
|
|
|
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
|
|
|
|
var result = await taskService.GetTaskByIdAsync(id, externalUserId);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
if (result == null)
|
|
|
|
|
return TypedResults.NotFound();
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<Results<Created<TaskDetailDto>, BadRequest<string>>> CreateTask(
|
|
|
|
|
CreateTaskRequest request,
|
|
|
|
|
TaskService taskService,
|
|
|
|
|
HttpContext httpContext)
|
|
|
|
|
{
|
|
|
|
|
var userIdClaim = httpContext.User.FindFirst("sub")?.Value;
|
|
|
|
|
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var createdById))
|
|
|
|
|
{
|
|
|
|
|
return TypedResults.BadRequest("Invalid user ID");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (task, error) = await taskService.CreateTaskAsync(request, createdById);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
if (error != null || task == null)
|
|
|
|
|
return TypedResults.BadRequest(error ?? "Failed to create task");
|
|
|
|
|
|
|
|
|
|
return TypedResults.Created($"/api/tasks/{task.Id}", task);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<Results<Ok<TaskDetailDto>, NotFound, UnprocessableEntity<string>, Conflict<string>>> UpdateTask(
|
|
|
|
|
Guid id,
|
|
|
|
|
UpdateTaskRequest request,
|
2026-03-09 15:47:57 +01:00
|
|
|
TaskService taskService,
|
|
|
|
|
HttpContext httpContext)
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
{
|
2026-03-09 15:47:57 +01:00
|
|
|
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
|
|
|
|
var (task, error, isConflict) = await taskService.UpdateTaskAsync(id, request, externalUserId);
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
|
|
|
|
|
if (error != null)
|
|
|
|
|
{
|
|
|
|
|
if (error == "Task not found")
|
|
|
|
|
return TypedResults.NotFound();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
if (isConflict)
|
|
|
|
|
return TypedResults.Conflict(error);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
return TypedResults.UnprocessableEntity(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok(task!);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<Results<NoContent, NotFound>> DeleteTask(
|
|
|
|
|
Guid id,
|
|
|
|
|
TaskService taskService)
|
|
|
|
|
{
|
|
|
|
|
var deleted = await taskService.DeleteTaskAsync(id);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
if (!deleted)
|
|
|
|
|
return TypedResults.NotFound();
|
|
|
|
|
|
|
|
|
|
return TypedResults.NoContent();
|
|
|
|
|
}
|
2026-03-09 15:47:57 +01:00
|
|
|
|
|
|
|
|
private static async Task<Results<Ok, BadRequest<string>, NotFound>> AssignTaskToMe(
|
|
|
|
|
Guid id,
|
|
|
|
|
TaskService taskService,
|
|
|
|
|
HttpContext httpContext)
|
|
|
|
|
{
|
|
|
|
|
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
|
|
|
|
if (externalUserId == null) return TypedResults.BadRequest("Invalid user");
|
|
|
|
|
|
|
|
|
|
var (success, error) = await taskService.AssignToMeAsync(id, externalUserId);
|
|
|
|
|
|
|
|
|
|
if (!success)
|
|
|
|
|
{
|
|
|
|
|
if (error == "Task not found") return TypedResults.NotFound();
|
|
|
|
|
return TypedResults.BadRequest(error ?? "Failed to assign task");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<Results<Ok, BadRequest<string>, NotFound>> UnassignTaskFromMe(
|
|
|
|
|
Guid id,
|
|
|
|
|
TaskService taskService,
|
|
|
|
|
HttpContext httpContext)
|
|
|
|
|
{
|
|
|
|
|
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
|
|
|
|
if (externalUserId == null) return TypedResults.BadRequest("Invalid user");
|
|
|
|
|
|
|
|
|
|
var (success, error) = await taskService.UnassignFromMeAsync(id, externalUserId);
|
|
|
|
|
|
|
|
|
|
if (!success)
|
|
|
|
|
{
|
|
|
|
|
if (error == "Task not found") return TypedResults.NotFound();
|
|
|
|
|
return TypedResults.BadRequest(error ?? "Failed to unassign task");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok();
|
|
|
|
|
}
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
}
|