// ----------------------------------------------------------------------------
// UpdateOrderStatusEndpoint
//
// Demonstrates:
// - PATCH operation for partial updates
// - Path parameters with request body
// - Write authorization claims
// - State machine validation
// ----------------------------------------------------------------------------
using Examples.OrderService.Models;
using Microsoft.Extensions.Logging;
using StellaOps.Microservice;
namespace Examples.OrderService.Endpoints;
///
/// Request to update order status.
///
public sealed record UpdateOrderStatusRequest
{
public required string OrderId { get; init; }
public required OrderStatus NewStatus { get; init; }
public string? Reason { get; init; }
public string? TrackingNumber { get; init; }
}
///
/// Response after status update.
///
public sealed record UpdateOrderStatusResponse
{
public required string OrderId { get; init; }
public required OrderStatus PreviousStatus { get; init; }
public required OrderStatus CurrentStatus { get; init; }
public required DateTimeOffset UpdatedAt { get; init; }
public string? Message { get; init; }
}
///
/// Endpoint for updating order status with state machine validation.
///
[StellaEndpoint("PATCH", "/orders/{id}/status", TimeoutSeconds = 10, RequiredClaims = ["orders:write"])]
[ValidateSchema(
ValidateRequest = true,
ValidateResponse = true,
Summary = "Update order status",
Description = "Updates the status of an order. Status transitions are validated against allowed state machine rules.",
Tags = ["orders", "write", "status"])]
public sealed class UpdateOrderStatusEndpoint : IStellaEndpoint
{
private readonly ILogger _logger;
// Valid state transitions
private static readonly Dictionary ValidTransitions = new()
{
[OrderStatus.Pending] = [OrderStatus.Confirmed, OrderStatus.Cancelled],
[OrderStatus.Confirmed] = [OrderStatus.Processing, OrderStatus.Cancelled],
[OrderStatus.Processing] = [OrderStatus.Shipped, OrderStatus.Cancelled],
[OrderStatus.Shipped] = [OrderStatus.Delivered],
[OrderStatus.Delivered] = [OrderStatus.Refunded],
[OrderStatus.Cancelled] = [],
[OrderStatus.Refunded] = []
};
public UpdateOrderStatusEndpoint(ILogger logger)
{
_logger = logger;
}
public Task HandleAsync(
UpdateOrderStatusRequest request,
CancellationToken cancellationToken)
{
// Simulate current status lookup
var currentStatus = OrderStatus.Processing;
_logger.LogInformation(
"Updating order {OrderId} status: {CurrentStatus} -> {NewStatus}",
request.OrderId, currentStatus, request.NewStatus);
// Validate state transition
if (!IsValidTransition(currentStatus, request.NewStatus))
{
_logger.LogWarning(
"Invalid status transition for order {OrderId}: {Current} -> {New}",
request.OrderId, currentStatus, request.NewStatus);
// In real implementation, throw an exception that maps to 422
return Task.FromResult(new UpdateOrderStatusResponse
{
OrderId = request.OrderId,
PreviousStatus = currentStatus,
CurrentStatus = currentStatus, // Unchanged
UpdatedAt = DateTimeOffset.UtcNow,
Message = $"Invalid transition: {currentStatus} cannot transition to {request.NewStatus}"
});
}
return Task.FromResult(new UpdateOrderStatusResponse
{
OrderId = request.OrderId,
PreviousStatus = currentStatus,
CurrentStatus = request.NewStatus,
UpdatedAt = DateTimeOffset.UtcNow,
Message = request.NewStatus == OrderStatus.Shipped
? $"Shipped with tracking: {request.TrackingNumber}"
: null
});
}
private static bool IsValidTransition(OrderStatus current, OrderStatus target)
{
return ValidTransitions.TryGetValue(current, out var allowed) && allowed.Contains(target);
}
}