Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .squad/decisions/inbox/sam-result-handlers-complete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Result<T> Handler Propagation Complete

**Date**: 2025-01-02
**Author**: Sam (Backend Developer)
**Issues**: #81, #83, #85, #87
**Branch**: `squad/81-result-t-handlers`
**Commit**: `9885078`

## Decision

All API handlers have been updated to propagate `Result<T>` from repositories to endpoints, completing the Result pattern implementation across all four domains (Issues, Categories, Statuses, Comments).

## Implementation Details

### Handler Changes

**Return Type Changes**:
- Get handlers: `Task<TDto?>` → `Task<Result<TDto>>`
- Update handlers: `Task<TDto>` → `Task<Result<TDto>>`
- Delete handlers: `Task<bool>` → `Task<Result<bool>>`

**Error Handling Pattern**:
```csharp
// Validation errors
if (!validationResult.IsValid)
return Result.Fail<TDto>("Validation failed", ResultErrorCode.Validation);

// Not found errors
if (getResult.Failure || getResult.Value is null)
return Result.Fail<TDto>($"Entity with ID '{id}' was not found.", ResultErrorCode.NotFound);

// Conflict errors (archived entities)
if (entity.Archived)
return Result.Fail<TDto>($"Entity is archived.", ResultErrorCode.Conflict);

// Propagate repository results
return await _repository.UpdateAsync(entity, cancellationToken);
```

**Delete Handler Pattern** (special case):
```csharp
// Repository.ArchiveAsync returns Result, not Result<bool>
var archiveResult = await _repository.ArchiveAsync(id, cancellationToken);
return archiveResult.Success
? Result.Ok(true)
: Result.Fail<bool>(archiveResult.Error!, archiveResult.ErrorCode);
```

### Endpoint Changes

**HTTP Response Mapping**:
```csharp
// Get endpoints
var result = await handler.Handle(query);
return result.Success ? Results.Ok(result.Value) : Results.NotFound();

// Update endpoints
var result = await handler.Handle(command);
if (!result.Success)
return result.ErrorCode == ResultErrorCode.NotFound ? Results.NotFound()
: result.ErrorCode == ResultErrorCode.Conflict ? Results.Conflict(result.Error)
: Results.BadRequest(result.Error);
return Results.Ok(result.Value);

// Delete endpoints
var result = await handler.Handle(command);
return result.Success ? Results.NoContent() : Results.NotFound();
```

## Files Changed

### Issues (#81)
- `src/Api/Handlers/Issues/GetIssueHandler.cs`
- `src/Api/Handlers/Issues/DeleteIssueHandler.cs`
- `src/Api/Handlers/Issues/UpdateIssueHandler.cs`
- `src/Api/Handlers/Issues/UpdateIssueStatusHandler.cs`
- `src/Api/Handlers/Issues/IssueEndpoints.cs`

### Categories (#83)
- `src/Api/Handlers/Categories/GetCategoryHandler.cs`
- `src/Api/Handlers/Categories/DeleteCategoryHandler.cs`
- `src/Api/Handlers/Categories/UpdateCategoryHandler.cs`
- `src/Api/Handlers/Categories/CategoryEndpoints.cs`

### Statuses (#85)
- `src/Api/Handlers/Statuses/GetStatusHandler.cs`
- `src/Api/Handlers/Statuses/DeleteStatusHandler.cs`
- `src/Api/Handlers/Statuses/UpdateStatusHandler.cs`
- `src/Api/Handlers/Statuses/StatusEndpoints.cs`

### Comments (#87)
- `src/Api/Handlers/Comments/GetCommentHandler.cs`
- `src/Api/Handlers/Comments/DeleteCommentHandler.cs`
- `src/Api/Handlers/Comments/UpdateCommentHandler.cs`
- `src/Api/Handlers/Comments/CommentEndpoints.cs`

## Testing Status

- ✅ **src/ compiles successfully** — No compilation errors in production code
- ❌ **tests/ have compilation errors** — Expected, waiting for Gimli to update test assertions

## Blocked Work

**Gimli** must update all handler tests to:
1. Expect `Result<T>` return types instead of direct DTOs
2. Assert on `result.Success` and `result.Failure`
3. Access values via `result.Value`
4. Check error codes via `result.ErrorCode`
5. Update FluentAssertions patterns (e.g., `result.Success.Should().BeTrue()`)

## PR Status

⚠️ **DO NOT MERGE** until test suite is updated by Gimli.

Branch: `squad/81-result-t-handlers`
PR: https://github.com/mpaulosky/IssueManager/pull/new/squad/81-result-t-handlers

## Related Decisions

- `.squad/decisions/inbox/aragorn-objectid-result-pattern.md` — Original Result<T> pattern decision
- `.squad/decisions/inbox/sam-objectid-endpoint-parsing.md` — ObjectId parsing at endpoints (sprint #80)
2 changes: 2 additions & 0 deletions IssueManager.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JetBrains settings XML namespace appears misspelled ("shemas" vs "schemas"), which may prevent Rider/ReSharper from loading this .DotSettings file. Fix the namespace value to the correct "urn:schemas-jetbrains-com:settings-storage-xaml".

Copilot uses AI. Check for mistakes.
<s:Boolean x:Key="/Default/UserDictionary/Words/=contentinfo/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
10 changes: 6 additions & 4 deletions src/Api/Handlers/Categories/CategoryEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public static IEndpointRouteBuilder MapCategoryEndpoints(this IEndpointRouteBuil
if (!ObjectId.TryParse(id, out var objectId))
return Results.BadRequest("Invalid ID format");
var query = new GetCategoryQuery(objectId);
var category = await handler.Handle(query);
return category is not null ? Results.Ok(category) : Results.NotFound();
var result = await handler.Handle(query);
return result.Success ? Results.Ok(result.Value) : Results.NotFound();
})
.WithName("GetCategory")
.WithSummary("Get a category by ID")
Expand All @@ -50,7 +50,9 @@ public static IEndpointRouteBuilder MapCategoryEndpoints(this IEndpointRouteBuil
return Results.BadRequest("Invalid ID format");
var commandWithId = command with { Id = objectId };
var result = await handler.Handle(commandWithId);
return result is not null ? Results.Ok(result) : Results.NotFound();
if (!result.Success)
return result.ErrorCode == ResultErrorCode.NotFound ? Results.NotFound() : Results.BadRequest(result.Error);
return Results.Ok(result.Value);
})
.WithName("UpdateCategory")
.WithSummary("Update an existing category")
Expand All @@ -65,7 +67,7 @@ public static IEndpointRouteBuilder MapCategoryEndpoints(this IEndpointRouteBuil
return Results.BadRequest("Invalid ID format");
var command = new DeleteCategoryCommand { Id = objectId };
var result = await handler.Handle(command);
return result ? Results.NoContent() : Results.NotFound();
return result.Success ? Results.NoContent() : Results.NotFound();
Comment on lines 68 to +70
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint maps any delete failure to 404. DeleteCategoryHandler can return ResultErrorCode.Validation (e.g., ObjectId.Empty passes TryParse but fails NotEmpty), which should be 400. Map ResultErrorCode.Validation to BadRequest (and keep NotFound → 404).

Copilot uses AI. Check for mistakes.
})
.WithName("DeleteCategory")
.WithSummary("Delete (archive) a category")
Expand Down
12 changes: 6 additions & 6 deletions src/Api/Handlers/Categories/DeleteCategoryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,23 @@ public DeleteCategoryHandler(ICategoryRepository repository, DeleteCategoryValid
/// </summary>
/// <param name="command">The command containing the category ID to delete.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns><see langword="true"/> if the category was successfully archived; otherwise, <see langword="false"/>.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains a result indicating success or failure.</returns>
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the category is not found.</exception>
Comment on lines 44 to 45
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs still advertise throwing ValidationException/NotFoundException, but this handler now returns Result for those failure cases. Update/remove the tags so the documentation matches the implementation.

Suggested change
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the category is not found.</exception>

Copilot uses AI. Check for mistakes.
public async Task<bool> Handle(DeleteCategoryCommand command, CancellationToken cancellationToken = default)
public async Task<Result<bool>> Handle(DeleteCategoryCommand command, CancellationToken cancellationToken = default)
{
var validationResult = await _validator.ValidateAsync(command, cancellationToken);
if (!validationResult.IsValid)
throw new ValidationException(validationResult.Errors);
return Result.Fail<bool>("Validation failed", ResultErrorCode.Validation);

var getResult = await _repository.GetByIdAsync(command.Id, cancellationToken);
if (getResult.Failure || getResult.Value is null)
throw new NotFoundException($"Category with ID '{command.Id}' was not found.");
return Result.Fail<bool>($"Category with ID '{command.Id}' was not found.", ResultErrorCode.NotFound);

if (getResult.Value.Archived)
return true;
return Result.Ok(true);

var archiveResult = await _repository.ArchiveAsync(command.Id, cancellationToken);
return archiveResult.Success;
return archiveResult.Success ? Result.Ok(true) : Result.Fail<bool>(archiveResult.Error!, archiveResult.ErrorCode);
}
}
7 changes: 3 additions & 4 deletions src/Api/Handlers/Categories/GetCategoryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,10 @@ public GetCategoryHandler(ICategoryRepository repository)
/// </summary>
/// <param name="query">The query containing the category ID to retrieve.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the category as a <see cref="CategoryDto"/>, or <see langword="null"/> if not found.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains the result with category as a <see cref="CategoryDto"/>, or an error if not found.</returns>
/// <exception cref="ArgumentException">Thrown when the category ID is null or empty.</exception>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs indicate an ArgumentException can be thrown for null/empty IDs, but GetCategoryQuery uses a non-nullable ObjectId and this method just forwards to the repository. Update/remove this tag to avoid misleading consumers.

Suggested change
/// <exception cref="ArgumentException">Thrown when the category ID is null or empty.</exception>

Copilot uses AI. Check for mistakes.
public async Task<CategoryDto?> Handle(GetCategoryQuery query, CancellationToken cancellationToken = default)
public async Task<Result<CategoryDto>> Handle(GetCategoryQuery query, CancellationToken cancellationToken = default)
{
var result = await _repository.GetByIdAsync(query.CategoryId, cancellationToken);
return result.Success ? result.Value : null;
return await _repository.GetByIdAsync(query.CategoryId, cancellationToken);
}
}
14 changes: 5 additions & 9 deletions src/Api/Handlers/Categories/UpdateCategoryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,25 @@ public UpdateCategoryHandler(ICategoryRepository repository, UpdateCategoryValid
/// </summary>
/// <param name="command">The command containing the updated category information.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated category as a <see cref="CategoryDto"/>.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains the result with updated category as a <see cref="CategoryDto"/>.</returns>
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the category is not found or cannot be updated.</exception>
Comment on lines 44 to 45
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs still advertise throwing ValidationException/NotFoundException, but this handler now returns Result for those failure cases. Update/remove the tags so the documentation matches the implementation.

Suggested change
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the category is not found or cannot be updated.</exception>

Copilot uses AI. Check for mistakes.
public async Task<CategoryDto> Handle(UpdateCategoryCommand command, CancellationToken cancellationToken = default)
public async Task<Result<CategoryDto>> Handle(UpdateCategoryCommand command, CancellationToken cancellationToken = default)
{
var validationResult = await _validator.ValidateAsync(command, cancellationToken);
if (!validationResult.IsValid)
throw new ValidationException(validationResult.Errors);
return Result.Fail<CategoryDto>("Validation failed", ResultErrorCode.Validation);

var getResult = await _repository.GetByIdAsync(command.Id, cancellationToken);
if (!getResult.Success || getResult.Value is null)
throw new NotFoundException($"Category with ID '{command.Id}' was not found.");
return Result.Fail<CategoryDto>($"Category with ID '{command.Id}' was not found.", ResultErrorCode.NotFound);

var updatedCategory = getResult.Value with
{
CategoryName = command.CategoryName,
CategoryDescription = command.CategoryDescription ?? string.Empty
};

var updateResult = await _repository.UpdateAsync(updatedCategory, cancellationToken);
if (!updateResult.Success)
throw new NotFoundException($"Category with ID '{command.Id}' could not be updated.");

return updateResult.Value!;
return await _repository.UpdateAsync(updatedCategory, cancellationToken);
}
}
10 changes: 6 additions & 4 deletions src/Api/Handlers/Comments/CommentEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public static IEndpointRouteBuilder MapCommentEndpoints(this IEndpointRouteBuild
if (!ObjectId.TryParse(id, out var objectId))
return Results.BadRequest("Invalid ID format");
var query = new GetCommentQuery(objectId);
var comment = await handler.Handle(query);
return comment is not null ? Results.Ok(comment) : Results.NotFound();
var result = await handler.Handle(query);
return result.Success ? Results.Ok(result.Value) : Results.NotFound();
})
.WithName("GetComment")
.WithSummary("Get a comment by ID")
Expand All @@ -50,7 +50,9 @@ public static IEndpointRouteBuilder MapCommentEndpoints(this IEndpointRouteBuild
return Results.BadRequest("Invalid ID format");
var commandWithId = command with { Id = objectId };
var result = await handler.Handle(commandWithId);
return result is not null ? Results.Ok(result) : Results.NotFound();
if (!result.Success)
return result.ErrorCode == ResultErrorCode.NotFound ? Results.NotFound() : Results.BadRequest(result.Error);
return Results.Ok(result.Value);
})
.WithName("UpdateComment")
.WithSummary("Update an existing comment")
Expand All @@ -65,7 +67,7 @@ public static IEndpointRouteBuilder MapCommentEndpoints(this IEndpointRouteBuild
return Results.BadRequest("Invalid ID format");
var command = new DeleteCommentCommand { Id = objectId };
var result = await handler.Handle(command);
return result ? Results.NoContent() : Results.NotFound();
return result.Success ? Results.NoContent() : Results.NotFound();
Comment on lines 68 to +70
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint maps any delete failure to 404. DeleteCommentHandler can return ResultErrorCode.Validation (e.g., ObjectId.Empty passes TryParse but fails NotEmpty), which should be 400. Map ResultErrorCode.Validation to BadRequest (and keep NotFound → 404).

Copilot uses AI. Check for mistakes.
})
.WithName("DeleteComment")
.WithSummary("Delete (archive) a comment")
Expand Down
12 changes: 6 additions & 6 deletions src/Api/Handlers/Comments/DeleteCommentHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,23 @@ public DeleteCommentHandler(ICommentRepository repository, DeleteCommentValidato
/// </summary>
/// <param name="command">The command containing the comment ID to delete.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns><see langword="true"/> if the comment was successfully archived; otherwise, <see langword="false"/>.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains a result indicating success or failure.</returns>
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the comment is not found.</exception>
Comment on lines 44 to 45
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs still advertise throwing ValidationException/NotFoundException, but this handler now returns Result for those failure cases. Update/remove the tags so the documentation matches the implementation.

Suggested change
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the comment is not found.</exception>

Copilot uses AI. Check for mistakes.
public async Task<bool> Handle(DeleteCommentCommand command, CancellationToken cancellationToken = default)
public async Task<Result<bool>> Handle(DeleteCommentCommand command, CancellationToken cancellationToken = default)
{
var validationResult = await _validator.ValidateAsync(command, cancellationToken);
if (!validationResult.IsValid)
throw new ValidationException(validationResult.Errors);
return Result.Fail<bool>("Validation failed", ResultErrorCode.Validation);

var getResult = await _repository.GetByIdAsync(command.Id, cancellationToken);
if (getResult.Failure || getResult.Value is null)
throw new NotFoundException($"Comment with ID '{command.Id}' was not found.");
return Result.Fail<bool>($"Comment with ID '{command.Id}' was not found.", ResultErrorCode.NotFound);

if (getResult.Value.Archived)
return true;
return Result.Ok(true);

var archiveResult = await _repository.ArchiveAsync(command.Id, cancellationToken);
return archiveResult.Success;
return archiveResult.Success ? Result.Ok(true) : Result.Fail<bool>(archiveResult.Error!, archiveResult.ErrorCode);
}
}
7 changes: 3 additions & 4 deletions src/Api/Handlers/Comments/GetCommentHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,10 @@ public GetCommentHandler(ICommentRepository repository)
/// </summary>
/// <param name="query">The query containing the comment ID to retrieve.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the comment as a <see cref="CommentDto"/>, or <see langword="null"/> if not found.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains the result with comment as a <see cref="CommentDto"/>, or an error if not found.</returns>
/// <exception cref="ArgumentException">Thrown when the comment ID is null or empty.</exception>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs indicate an ArgumentException can be thrown for null/empty IDs, but GetCommentQuery uses a non-nullable ObjectId and this method just forwards to the repository. Update/remove this tag to avoid misleading consumers.

Suggested change
/// <exception cref="ArgumentException">Thrown when the comment ID is null or empty.</exception>

Copilot uses AI. Check for mistakes.
public async Task<CommentDto?> Handle(GetCommentQuery query, CancellationToken cancellationToken = default)
public async Task<Result<CommentDto>> Handle(GetCommentQuery query, CancellationToken cancellationToken = default)
{
var result = await _repository.GetByIdAsync(query.CommentId, cancellationToken);
return result.Success ? result.Value : null;
return await _repository.GetByIdAsync(query.CommentId, cancellationToken);
}
}
14 changes: 5 additions & 9 deletions src/Api/Handlers/Comments/UpdateCommentHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,28 +40,24 @@ public UpdateCommentHandler(ICommentRepository repository, UpdateCommentValidato
/// </summary>
/// <param name="command">The command containing the updated comment information.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated comment as a <see cref="CommentDto"/>.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains the result with updated comment as a <see cref="CommentDto"/>.</returns>
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the comment is not found or cannot be updated.</exception>
Comment on lines 44 to 45
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs still advertise throwing ValidationException/NotFoundException, but this handler now returns Result for those failure cases. Update/remove the tags so the documentation matches the implementation.

Suggested change
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the comment is not found or cannot be updated.</exception>

Copilot uses AI. Check for mistakes.
public async Task<CommentDto> Handle(UpdateCommentCommand command, CancellationToken cancellationToken = default)
public async Task<Result<CommentDto>> Handle(UpdateCommentCommand command, CancellationToken cancellationToken = default)
{
var validationResult = await _validator.ValidateAsync(command, cancellationToken);
if (!validationResult.IsValid)
throw new ValidationException(validationResult.Errors);
return Result.Fail<CommentDto>("Validation failed", ResultErrorCode.Validation);

var getResult = await _repository.GetByIdAsync(command.Id, cancellationToken);
if (getResult.Failure || getResult.Value is null)
throw new NotFoundException($"Comment with ID '{command.Id}' was not found.");
return Result.Fail<CommentDto>($"Comment with ID '{command.Id}' was not found.", ResultErrorCode.NotFound);

var updatedComment = getResult.Value with
{
Title = command.Title
};

var updateResult = await _repository.UpdateAsync(updatedComment, cancellationToken);
if (updateResult.Failure)
throw new NotFoundException($"Comment with ID '{command.Id}' could not be updated.");

return updateResult.Value!;
return await _repository.UpdateAsync(updatedComment, cancellationToken);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning _repository.UpdateAsync(...) directly can yield ErrorCode=None on not-found/update-failed (CommentRepository.UpdateAsync doesn’t set ResultErrorCode.NotFound). That will make CommentEndpoints return 400 instead of 404 for missing comments. Consider translating failed updates into a NotFound-coded Result here (or update the repository to set codes).

Suggested change
return await _repository.UpdateAsync(updatedComment, cancellationToken);
var updateResult = await _repository.UpdateAsync(updatedComment, cancellationToken);
if (updateResult.Failure || updateResult.Value is null)
return Result.Fail<CommentDto>($"Comment with ID '{command.Id}' was not found or could not be updated.", ResultErrorCode.NotFound);
return updateResult;

Copilot uses AI. Check for mistakes.
}
}
10 changes: 5 additions & 5 deletions src/Api/Handlers/Issues/DeleteIssueHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@ public DeleteIssueHandler(IIssueRepository repository, DeleteIssueValidator vali
/// <summary>
/// Handles the soft-deletion (archiving) of an issue.
/// </summary>
public async Task<bool> Handle(DeleteIssueCommand command, CancellationToken cancellationToken = default)
public async Task<Result<bool>> Handle(DeleteIssueCommand command, CancellationToken cancellationToken = default)
{
var validationResult = await _validator.ValidateAsync(command, cancellationToken);
if (!validationResult.IsValid)
throw new ValidationException(validationResult.Errors);
return Result.Fail<bool>("Validation failed", ResultErrorCode.Validation);

var getResult = await _repository.GetByIdAsync(command.Id, cancellationToken);
if (getResult.Failure || getResult.Value is null)
throw new NotFoundException($"Issue with ID '{command.Id}' was not found.");
return Result.Fail<bool>($"Issue with ID '{command.Id}' was not found.", ResultErrorCode.NotFound);

if (getResult.Value.Archived)
return true;
return Result.Ok(true);

var archiveResult = await _repository.ArchiveAsync(command.Id, cancellationToken);
return archiveResult.Success;
return archiveResult.Success ? Result.Ok(true) : Result.Fail<bool>(archiveResult.Error!, archiveResult.ErrorCode);
}
}
5 changes: 2 additions & 3 deletions src/Api/Handlers/Issues/GetIssueHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ public GetIssueHandler(IIssueRepository repository)
/// <summary>
/// Handles the retrieval of a single issue.
/// </summary>
public async Task<IssueDto?> Handle(GetIssueQuery query, CancellationToken cancellationToken = default)
public async Task<Result<IssueDto>> Handle(GetIssueQuery query, CancellationToken cancellationToken = default)
{
var result = await _repository.GetByIdAsync(query.IssueId, cancellationToken);
return result.Success ? result.Value : null;
return await _repository.GetByIdAsync(query.IssueId, cancellationToken);
}

/// <summary>
Expand Down
Loading
Loading