doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements
This commit is contained in:
@@ -99,9 +99,188 @@ public static class GreyQueueEndpoints
|
||||
.WithSummary("Get grey queue summary statistics")
|
||||
.WithDescription("Returns summary counts by status, reason, and performance metrics.");
|
||||
|
||||
// Sprint: SPRINT_20260118_018 (UQ-005) - New state transitions
|
||||
group.MapPost("/{id:guid}/assign", AssignForReview)
|
||||
.WithName("AssignGreyQueueEntry")
|
||||
.WithSummary("Assign entry for review")
|
||||
.WithDescription("Assigns an entry to a reviewer, transitioning to UnderReview state.");
|
||||
|
||||
group.MapPost("/{id:guid}/escalate", EscalateEntry)
|
||||
.WithName("EscalateGreyQueueEntry")
|
||||
.WithSummary("Escalate entry to security team")
|
||||
.WithDescription("Escalates an entry to the security team, transitioning to Escalated state.");
|
||||
|
||||
group.MapPost("/{id:guid}/reject", RejectEntry)
|
||||
.WithName("RejectGreyQueueEntry")
|
||||
.WithSummary("Reject a grey queue entry")
|
||||
.WithDescription("Marks an entry as rejected (invalid or not actionable).");
|
||||
|
||||
group.MapPost("/{id:guid}/reopen", ReopenEntry)
|
||||
.WithName("ReopenGreyQueueEntry")
|
||||
.WithSummary("Reopen a closed entry")
|
||||
.WithDescription("Reopens a rejected, failed, or dismissed entry back to pending.");
|
||||
|
||||
group.MapGet("/{id:guid}/transitions", GetValidTransitions)
|
||||
.WithName("GetValidTransitions")
|
||||
.WithSummary("Get valid state transitions")
|
||||
.WithDescription("Returns the valid next states for an entry based on current state.");
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260118_018 (UQ-005) - Assign for review
|
||||
private static async Task<Results<Ok<GreyQueueEntryDto>, NotFound, BadRequest<string>>> AssignForReview(
|
||||
Guid id,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromBody] AssignForReviewRequest request,
|
||||
IGreyQueueRepository repository = null!,
|
||||
INotificationPublisher? notificationPublisher = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entry = await repository.GetByIdAsync(tenantId, id, ct);
|
||||
if (entry is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GreyQueueStateMachine.ValidateUnderReviewTransition(entry.Status, request.Assignee);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return TypedResults.BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
var updated = await repository.TransitionToUnderReviewAsync(
|
||||
tenantId, id, request.Assignee, ct);
|
||||
|
||||
return TypedResults.Ok(MapToDto(updated));
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260118_018 (UQ-005) - Escalate to security team
|
||||
private static async Task<Results<Ok<GreyQueueEntryDto>, NotFound, BadRequest<string>>> EscalateEntry(
|
||||
Guid id,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromBody] EscalateRequest request,
|
||||
IGreyQueueRepository repository = null!,
|
||||
INotificationPublisher? notificationPublisher = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entry = await repository.GetByIdAsync(tenantId, id, ct);
|
||||
if (entry is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GreyQueueStateMachine.ValidateTransition(entry.Status, GreyQueueStatus.Escalated);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return TypedResults.BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
var updated = await repository.TransitionToEscalatedAsync(
|
||||
tenantId, id, request.Reason, ct);
|
||||
|
||||
// Notify security team
|
||||
if (notificationPublisher != null)
|
||||
{
|
||||
await notificationPublisher.PublishAsync(new EscalationNotification
|
||||
{
|
||||
EntryId = id,
|
||||
BomRef = entry.BomRef,
|
||||
Reason = request.Reason,
|
||||
EscalatedAt = DateTimeOffset.UtcNow
|
||||
}, ct);
|
||||
}
|
||||
|
||||
return TypedResults.Ok(MapToDto(updated));
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260118_018 (UQ-005) - Reject entry
|
||||
private static async Task<Results<Ok<GreyQueueEntryDto>, NotFound, BadRequest<string>>> RejectEntry(
|
||||
Guid id,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromBody] RejectRequest request,
|
||||
IGreyQueueRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entry = await repository.GetByIdAsync(tenantId, id, ct);
|
||||
if (entry is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GreyQueueStateMachine.ValidateTransition(entry.Status, GreyQueueStatus.Rejected);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return TypedResults.BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
var updated = await repository.TransitionToRejectedAsync(
|
||||
tenantId, id, request.Reason, request.RejectedBy, ct);
|
||||
|
||||
return TypedResults.Ok(MapToDto(updated));
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260118_018 (UQ-005) - Reopen entry
|
||||
private static async Task<Results<Ok<GreyQueueEntryDto>, NotFound, BadRequest<string>>> ReopenEntry(
|
||||
Guid id,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
[FromBody] ReopenRequest request,
|
||||
IGreyQueueRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entry = await repository.GetByIdAsync(tenantId, id, ct);
|
||||
if (entry is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GreyQueueStateMachine.ValidateTransition(entry.Status, GreyQueueStatus.Pending);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return TypedResults.BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
var updated = await repository.ReopenAsync(tenantId, id, request.Reason, ct);
|
||||
|
||||
return TypedResults.Ok(MapToDto(updated));
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260118_018 (UQ-005) - Get valid transitions
|
||||
private static async Task<Results<Ok<ValidTransitionsResponse>, NotFound>> GetValidTransitions(
|
||||
Guid id,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
IGreyQueueRepository repository = null!,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entry = await repository.GetByIdAsync(tenantId, id, ct);
|
||||
if (entry is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var validStates = GreyQueueStateMachine.GetValidNextStates(entry.Status);
|
||||
|
||||
var response = new ValidTransitionsResponse
|
||||
{
|
||||
CurrentState = entry.Status.ToString(),
|
||||
ValidNextStates = validStates.Select(s => s.ToString()).ToList()
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// List entries with pagination
|
||||
private static async Task<Ok<GreyQueueListResponse>> ListEntries(
|
||||
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
|
||||
@@ -580,3 +759,57 @@ public sealed record ExpireResultResponse
|
||||
{
|
||||
public required int ExpiredCount { get; init; }
|
||||
}
|
||||
// Sprint: SPRINT_20260118_018 (UQ-005) - New DTOs for state transitions
|
||||
|
||||
public sealed record AssignForReviewRequest
|
||||
{
|
||||
/// <summary>Required: The assignee for review.</summary>
|
||||
public required string Assignee { get; init; }
|
||||
|
||||
/// <summary>Optional notes for the reviewer.</summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
|
||||
public sealed record EscalateRequest
|
||||
{
|
||||
/// <summary>Reason for escalation.</summary>
|
||||
public required string Reason { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RejectRequest
|
||||
{
|
||||
/// <summary>Reason for rejection.</summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>Who rejected the entry.</summary>
|
||||
public required string RejectedBy { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ReopenRequest
|
||||
{
|
||||
/// <summary>Reason for reopening.</summary>
|
||||
public required string Reason { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ValidTransitionsResponse
|
||||
{
|
||||
/// <summary>Current state of the entry.</summary>
|
||||
public required string CurrentState { get; init; }
|
||||
|
||||
/// <summary>Valid next states from current state.</summary>
|
||||
public required List<string> ValidNextStates { get; init; }
|
||||
}
|
||||
|
||||
public sealed record EscalationNotification
|
||||
{
|
||||
public required Guid EntryId { get; init; }
|
||||
public required string BomRef { get; init; }
|
||||
public required string Reason { get; init; }
|
||||
public DateTimeOffset EscalatedAt { get; init; }
|
||||
}
|
||||
|
||||
// Interface for notification publishing
|
||||
public interface INotificationPublisher
|
||||
{
|
||||
Task PublishAsync<T>(T notification, CancellationToken ct = default);
|
||||
}
|
||||
Reference in New Issue
Block a user