From 1e86d2f05b3ded3925ca3e536c2cef30e6418db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Tue, 31 Mar 2026 19:51:32 -0600 Subject: [PATCH] feat(operations-service): add lifecycle-backed restaurant detail reads --- docs/api/internal-workflow-contracts.md | 10 +- .../Ports/DefaultOperationsWorkflowPort.cs | 140 +++++++++++++++--- .../Ports/IOperationsWorkflowPort.cs | 3 + .../UseCases/GetCustomerOrderDetailUseCase.cs | 13 ++ .../GetCustomerOrderHistoryUseCase.cs | 13 ++ .../GetPosTransactionDetailUseCase.cs | 13 ++ .../IGetCustomerOrderDetailUseCase.cs | 9 ++ .../IGetCustomerOrderHistoryUseCase.cs | 9 ++ .../IGetPosTransactionDetailUseCase.cs | 9 ++ .../Requests/GetCustomerOrderDetailRequest.cs | 5 + .../GetCustomerOrderHistoryRequest.cs | 3 + .../GetPosTransactionDetailRequest.cs | 5 + .../GetCustomerOrderDetailResponse.cs | 9 ++ .../GetCustomerOrderHistoryResponse.cs | 9 ++ .../GetPosTransactionDetailResponse.cs | 10 ++ src/Operations.Service.Grpc/Program.cs | 29 ++++ .../OperationsWorkflowUseCasesTests.cs | 45 ++++++ 17 files changed, 315 insertions(+), 19 deletions(-) create mode 100644 src/Operations.Service.Application/UseCases/GetCustomerOrderDetailUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/GetCustomerOrderHistoryUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/GetPosTransactionDetailUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/IGetCustomerOrderDetailUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/IGetCustomerOrderHistoryUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/IGetPosTransactionDetailUseCase.cs create mode 100644 src/Operations.Service.Contracts/Requests/GetCustomerOrderDetailRequest.cs create mode 100644 src/Operations.Service.Contracts/Requests/GetCustomerOrderHistoryRequest.cs create mode 100644 src/Operations.Service.Contracts/Requests/GetPosTransactionDetailRequest.cs create mode 100644 src/Operations.Service.Contracts/Responses/GetCustomerOrderDetailResponse.cs create mode 100644 src/Operations.Service.Contracts/Responses/GetCustomerOrderHistoryResponse.cs create mode 100644 src/Operations.Service.Contracts/Responses/GetPosTransactionDetailResponse.cs diff --git a/docs/api/internal-workflow-contracts.md b/docs/api/internal-workflow-contracts.md index f3fd508..4ce46fa 100644 --- a/docs/api/internal-workflow-contracts.md +++ b/docs/api/internal-workflow-contracts.md @@ -11,7 +11,10 @@ - `GET /internal/operations/waiter/assignments?contextId=` - `POST /internal/operations/orders` - `GET /internal/operations/customer/status?contextId=` +- `GET /internal/operations/customer/orders/?contextId=` +- `GET /internal/operations/customer/history?contextId=` - `GET /internal/operations/pos/summary?contextId=` +- `GET /internal/operations/pos/transactions/?contextId=` - `POST /internal/operations/pos/payments` - `GET /internal/operations/admin/config?contextId=` - `POST /internal/operations/admin/service-window` @@ -23,16 +26,19 @@ This repo now orchestrates restaurant workflow over the shared lifecycle store e That means: - submitted orders are persisted as shared order/check records - customer status reads come from persisted restaurant state rather than static arrays +- customer detail and history no longer need to be projected from the status summary payload - POS summary reads only served checks that remain payable +- POS detail can resolve a specific check directly from the shared lifecycle store - payment capture updates persisted check state and appends lifecycle events ## Contract Intent - waiter assignments surface floor-facing table attention derived from shared order/check state -- customer order status reflects the same lifecycle that waiter and POS flows observe +- customer status, detail, and history all reflect the same lifecycle that waiter and POS flows observe - POS payment only opens for served orders with outstanding balance +- POS detail stays lifecycle-backed even when the check is not yet payable, which keeps downstream error handling honest - restaurant-admin configuration remains control-plane oriented and intentionally separate from order persistence ## Remaining Limitation -- Kitchen ticket creation and kitchen state feedback are not owned by this repo; they will be linked in the Stage 46 kitchen-service task. +- Kitchen ticket creation and kitchen state feedback still depend on `kitchen-service` propagation alignment in the Stage 50 companion task. diff --git a/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs b/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs index ded9dcb..2f507a7 100644 --- a/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs +++ b/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs @@ -128,14 +128,7 @@ public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort cancellationToken.ThrowIfCancellationRequested(); var orders = await lifecycleStore.ListOrdersAsync(request.ContextId, cancellationToken); - var contracts = orders - .Select(order => new CustomerOrderStatusContract( - order.OrderId, - order.TableId, - MapCustomerStatus(order.OrderState), - order.GuestCount, - order.ItemIds)) - .ToArray(); + var contracts = MapCustomerOrders(orders); var events = await BuildRecentActivityAsync(orders, cancellationToken); @@ -148,6 +141,45 @@ public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort events); } + public async Task GetCustomerOrderDetailAsync( + GetCustomerOrderDetailRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var order = await lifecycleStore.GetOrderAsync(request.ContextId, request.OrderId, cancellationToken); + var recentEvents = order is null + ? Array.Empty() + : await BuildOrderActivityAsync(request.ContextId, request.OrderId, cancellationToken); + + return new GetCustomerOrderDetailResponse( + request.ContextId, + order is null + ? $"Order {request.OrderId} is not visible in the shared restaurant lifecycle." + : $"Order {request.OrderId} is currently {MapCustomerStatus(order.OrderState)} in the shared restaurant lifecycle.", + order is null ? null : MapCustomerOrder(order), + recentEvents); + } + + public async Task GetCustomerOrderHistoryAsync( + GetCustomerOrderHistoryRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var orders = await lifecycleStore.ListOrdersAsync(request.ContextId, cancellationToken); + var contracts = MapCustomerOrders(orders); + var recentEvents = await BuildRecentActivityAsync(orders, cancellationToken); + + return new GetCustomerOrderHistoryResponse( + request.ContextId, + contracts.Length == 0 + ? "No customer order history is currently visible for the active context." + : $"{contracts.Length} customer orders are currently available through lifecycle-backed history.", + contracts, + recentEvents); + } + public async Task GetPosTransactionSummaryAsync( GetPosTransactionSummaryRequest request, CancellationToken cancellationToken) @@ -156,15 +188,7 @@ public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort var payableOrders = await lifecycleStore.ListPayableOrdersAsync(request.ContextId, cancellationToken); var openBalance = payableOrders.Sum(order => order.OutstandingBalance); - var payments = payableOrders - .Select(order => new PosPaymentActivityContract( - order.CheckId, - "check", - order.OutstandingBalance, - order.Currency, - string.Equals(order.CheckState, "AwaitingPayment", StringComparison.Ordinal) ? "partial-payment" : "awaiting-payment", - order.UpdatedAtUtc)) - .ToArray(); + var payments = MapPayments(payableOrders); return new GetPosTransactionSummaryResponse( request.ContextId, @@ -176,6 +200,26 @@ public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort payments); } + public async Task GetPosTransactionDetailAsync( + GetPosTransactionDetailRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var record = await ResolveTransactionAsync(request.ContextId, request.TransactionId, cancellationToken); + var payableOrders = await lifecycleStore.ListPayableOrdersAsync(request.ContextId, cancellationToken); + var openBalance = payableOrders.Sum(order => order.OutstandingBalance); + + return new GetPosTransactionDetailResponse( + request.ContextId, + record is null + ? $"Transaction {request.TransactionId} is not visible in the shared restaurant lifecycle." + : $"Transaction {request.TransactionId} maps to check {record.CheckId} and is currently {MapPaymentStatus(record)}.", + openBalance, + record?.Currency ?? payableOrders.FirstOrDefault()?.Currency ?? "USD", + record is null ? null : MapPayment(record)); + } + public async Task CapturePosPaymentAsync( CapturePosPaymentRequest request, CancellationToken cancellationToken) @@ -317,6 +361,16 @@ public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort return orders.FirstOrDefault(order => string.Equals(order.CheckId, transactionId, StringComparison.Ordinal)); } + private async Task> BuildOrderActivityAsync(string contextId, string orderId, CancellationToken cancellationToken) + { + var events = await lifecycleStore.ListEventsAsync(contextId, orderId, cancellationToken); + return events + .OrderByDescending(record => record.OccurredAtUtc) + .Select(record => record.Description) + .Take(6) + .ToArray(); + } + private async Task> BuildRecentActivityAsync(IEnumerable orders, CancellationToken cancellationToken) { var activity = new List(); @@ -333,6 +387,41 @@ public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort .ToArray(); } + private static CustomerOrderStatusContract[] MapCustomerOrders(IEnumerable orders) + { + return orders + .Select(MapCustomerOrder) + .ToArray(); + } + + private static CustomerOrderStatusContract MapCustomerOrder(PersistedRestaurantLifecycleRecord order) + { + return new CustomerOrderStatusContract( + order.OrderId, + order.TableId, + MapCustomerStatus(order.OrderState), + order.GuestCount, + order.ItemIds); + } + + private static PosPaymentActivityContract[] MapPayments(IEnumerable payableOrders) + { + return payableOrders + .Select(MapPayment) + .ToArray(); + } + + private static PosPaymentActivityContract MapPayment(PersistedRestaurantLifecycleRecord order) + { + return new PosPaymentActivityContract( + order.CheckId, + "check", + order.OutstandingBalance, + order.Currency, + MapPaymentStatus(order), + order.UpdatedAtUtc); + } + private static bool IsPayable(PersistedRestaurantLifecycleRecord record) { return string.Equals(record.OrderState, "Served", StringComparison.Ordinal) @@ -343,6 +432,23 @@ public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort private static string MapCustomerStatus(string orderState) => orderState.ToLowerInvariant(); + private static string MapPaymentStatus(PersistedRestaurantLifecycleRecord record) + { + if (string.Equals(record.CheckState, "Paid", StringComparison.Ordinal)) + { + return "captured"; + } + + if (!IsPayable(record)) + { + return "not-payable"; + } + + return string.Equals(record.CheckState, "AwaitingPayment", StringComparison.Ordinal) + ? "partial-payment" + : "awaiting-payment"; + } + private static string MapWaiterStatus(PersistedRestaurantLifecycleRecord record) { if (string.Equals(record.OrderState, "Served", StringComparison.Ordinal) diff --git a/src/Operations.Service.Application/Ports/IOperationsWorkflowPort.cs b/src/Operations.Service.Application/Ports/IOperationsWorkflowPort.cs index 862d4fd..c4c2446 100644 --- a/src/Operations.Service.Application/Ports/IOperationsWorkflowPort.cs +++ b/src/Operations.Service.Application/Ports/IOperationsWorkflowPort.cs @@ -8,7 +8,10 @@ public interface IOperationsWorkflowPort Task GetWaiterAssignmentsAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken); Task SubmitRestaurantOrderAsync(SubmitRestaurantOrderRequest request, CancellationToken cancellationToken); Task GetCustomerOrderStatusAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken); + Task GetCustomerOrderDetailAsync(GetCustomerOrderDetailRequest request, CancellationToken cancellationToken); + Task GetCustomerOrderHistoryAsync(GetCustomerOrderHistoryRequest request, CancellationToken cancellationToken); Task GetPosTransactionSummaryAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken); + Task GetPosTransactionDetailAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken); Task CapturePosPaymentAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken); Task GetRestaurantAdminConfigAsync(GetRestaurantAdminConfigRequest request, CancellationToken cancellationToken); Task SetServiceWindowAsync(SetServiceWindowRequest request, CancellationToken cancellationToken); diff --git a/src/Operations.Service.Application/UseCases/GetCustomerOrderDetailUseCase.cs b/src/Operations.Service.Application/UseCases/GetCustomerOrderDetailUseCase.cs new file mode 100644 index 0000000..2904292 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/GetCustomerOrderDetailUseCase.cs @@ -0,0 +1,13 @@ +using Operations.Service.Application.Ports; +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public sealed class GetCustomerOrderDetailUseCase(IOperationsWorkflowPort workflowPort) : IGetCustomerOrderDetailUseCase +{ + public Task HandleAsync(GetCustomerOrderDetailRequest request, CancellationToken cancellationToken) + { + return workflowPort.GetCustomerOrderDetailAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/GetCustomerOrderHistoryUseCase.cs b/src/Operations.Service.Application/UseCases/GetCustomerOrderHistoryUseCase.cs new file mode 100644 index 0000000..cb15776 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/GetCustomerOrderHistoryUseCase.cs @@ -0,0 +1,13 @@ +using Operations.Service.Application.Ports; +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public sealed class GetCustomerOrderHistoryUseCase(IOperationsWorkflowPort workflowPort) : IGetCustomerOrderHistoryUseCase +{ + public Task HandleAsync(GetCustomerOrderHistoryRequest request, CancellationToken cancellationToken) + { + return workflowPort.GetCustomerOrderHistoryAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/GetPosTransactionDetailUseCase.cs b/src/Operations.Service.Application/UseCases/GetPosTransactionDetailUseCase.cs new file mode 100644 index 0000000..8e327e0 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/GetPosTransactionDetailUseCase.cs @@ -0,0 +1,13 @@ +using Operations.Service.Application.Ports; +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public sealed class GetPosTransactionDetailUseCase(IOperationsWorkflowPort workflowPort) : IGetPosTransactionDetailUseCase +{ + public Task HandleAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken) + { + return workflowPort.GetPosTransactionDetailAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/IGetCustomerOrderDetailUseCase.cs b/src/Operations.Service.Application/UseCases/IGetCustomerOrderDetailUseCase.cs new file mode 100644 index 0000000..407782a --- /dev/null +++ b/src/Operations.Service.Application/UseCases/IGetCustomerOrderDetailUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface IGetCustomerOrderDetailUseCase +{ + Task HandleAsync(GetCustomerOrderDetailRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/IGetCustomerOrderHistoryUseCase.cs b/src/Operations.Service.Application/UseCases/IGetCustomerOrderHistoryUseCase.cs new file mode 100644 index 0000000..241940c --- /dev/null +++ b/src/Operations.Service.Application/UseCases/IGetCustomerOrderHistoryUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface IGetCustomerOrderHistoryUseCase +{ + Task HandleAsync(GetCustomerOrderHistoryRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/IGetPosTransactionDetailUseCase.cs b/src/Operations.Service.Application/UseCases/IGetPosTransactionDetailUseCase.cs new file mode 100644 index 0000000..60db771 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/IGetPosTransactionDetailUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface IGetPosTransactionDetailUseCase +{ + Task HandleAsync(GetPosTransactionDetailRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Contracts/Requests/GetCustomerOrderDetailRequest.cs b/src/Operations.Service.Contracts/Requests/GetCustomerOrderDetailRequest.cs new file mode 100644 index 0000000..75b1aa8 --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/GetCustomerOrderDetailRequest.cs @@ -0,0 +1,5 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record GetCustomerOrderDetailRequest( + string ContextId, + string OrderId); diff --git a/src/Operations.Service.Contracts/Requests/GetCustomerOrderHistoryRequest.cs b/src/Operations.Service.Contracts/Requests/GetCustomerOrderHistoryRequest.cs new file mode 100644 index 0000000..a42f1b0 --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/GetCustomerOrderHistoryRequest.cs @@ -0,0 +1,3 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record GetCustomerOrderHistoryRequest(string ContextId); diff --git a/src/Operations.Service.Contracts/Requests/GetPosTransactionDetailRequest.cs b/src/Operations.Service.Contracts/Requests/GetPosTransactionDetailRequest.cs new file mode 100644 index 0000000..3bd34dc --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/GetPosTransactionDetailRequest.cs @@ -0,0 +1,5 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record GetPosTransactionDetailRequest( + string ContextId, + string TransactionId); diff --git a/src/Operations.Service.Contracts/Responses/GetCustomerOrderDetailResponse.cs b/src/Operations.Service.Contracts/Responses/GetCustomerOrderDetailResponse.cs new file mode 100644 index 0000000..bea65be --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/GetCustomerOrderDetailResponse.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Contracts; + +namespace Operations.Service.Contracts.Responses; + +public sealed record GetCustomerOrderDetailResponse( + string ContextId, + string Summary, + CustomerOrderStatusContract? Order, + IReadOnlyCollection RecentEvents); diff --git a/src/Operations.Service.Contracts/Responses/GetCustomerOrderHistoryResponse.cs b/src/Operations.Service.Contracts/Responses/GetCustomerOrderHistoryResponse.cs new file mode 100644 index 0000000..9e90b25 --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/GetCustomerOrderHistoryResponse.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Contracts; + +namespace Operations.Service.Contracts.Responses; + +public sealed record GetCustomerOrderHistoryResponse( + string ContextId, + string Summary, + IReadOnlyCollection Orders, + IReadOnlyCollection RecentEvents); diff --git a/src/Operations.Service.Contracts/Responses/GetPosTransactionDetailResponse.cs b/src/Operations.Service.Contracts/Responses/GetPosTransactionDetailResponse.cs new file mode 100644 index 0000000..d659590 --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/GetPosTransactionDetailResponse.cs @@ -0,0 +1,10 @@ +using Operations.Service.Contracts.Contracts; + +namespace Operations.Service.Contracts.Responses; + +public sealed record GetPosTransactionDetailResponse( + string ContextId, + string Summary, + decimal OpenBalance, + string Currency, + PosPaymentActivityContract? Transaction); diff --git a/src/Operations.Service.Grpc/Program.cs b/src/Operations.Service.Grpc/Program.cs index 1340dc9..05e072a 100644 --- a/src/Operations.Service.Grpc/Program.cs +++ b/src/Operations.Service.Grpc/Program.cs @@ -17,7 +17,10 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -62,6 +65,23 @@ app.MapGet("/internal/operations/customer/status", async ( return Results.Ok(await useCase.HandleAsync(new GetCustomerOrderStatusRequest(contextId), ct)); }); +app.MapGet("/internal/operations/customer/orders/{orderId}", async ( + string contextId, + string orderId, + IGetCustomerOrderDetailUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(new GetCustomerOrderDetailRequest(contextId, orderId), ct)); +}); + +app.MapGet("/internal/operations/customer/history", async ( + string contextId, + IGetCustomerOrderHistoryUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(new GetCustomerOrderHistoryRequest(contextId), ct)); +}); + app.MapGet("/internal/operations/pos/summary", async ( string contextId, IGetPosTransactionSummaryUseCase useCase, @@ -70,6 +90,15 @@ app.MapGet("/internal/operations/pos/summary", async ( return Results.Ok(await useCase.HandleAsync(new GetPosTransactionSummaryRequest(contextId), ct)); }); +app.MapGet("/internal/operations/pos/transactions/{transactionId}", async ( + string contextId, + string transactionId, + IGetPosTransactionDetailUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(new GetPosTransactionDetailRequest(contextId, transactionId), ct)); +}); + app.MapPost("/internal/operations/pos/payments", async ( CapturePosPaymentRequest request, ICapturePosPaymentUseCase useCase, diff --git a/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs b/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs index 83a414a..d9199ed 100644 --- a/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs +++ b/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs @@ -57,6 +57,36 @@ public class OperationsWorkflowUseCasesTests Assert.Contains(response.Orders, order => order.OrderId == "ORD-1001"); } + [Fact] + public async Task GetCustomerOrderDetailUseCase_ReturnsSpecificLifecycleOrder() + { + var useCase = new GetCustomerOrderDetailUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new GetCustomerOrderDetailRequest("demo-context", "ORD-1001"), + CancellationToken.None); + + Assert.Equal("demo-context", response.ContextId); + Assert.NotNull(response.Order); + Assert.Equal("ORD-1001", response.Order!.OrderId); + Assert.NotEmpty(response.RecentEvents); + } + + [Fact] + public async Task GetCustomerOrderHistoryUseCase_ReturnsLifecycleBackedHistory() + { + var useCase = new GetCustomerOrderHistoryUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new GetCustomerOrderHistoryRequest("demo-context"), + CancellationToken.None); + + Assert.Equal("demo-context", response.ContextId); + Assert.NotEmpty(response.Orders); + Assert.NotEmpty(response.RecentEvents); + Assert.Contains("lifecycle-backed history", response.Summary); + } + [Fact] public async Task GetPosTransactionSummaryUseCase_ReturnsOnlyPayableChecks() { @@ -69,6 +99,21 @@ public class OperationsWorkflowUseCasesTests Assert.All(response.RecentPayments, payment => Assert.Equal("check", payment.PaymentMethod)); } + [Fact] + public async Task GetPosTransactionDetailUseCase_ReturnsLifecycleBackedCheck() + { + var useCase = new GetPosTransactionDetailUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new GetPosTransactionDetailRequest("demo-context", "CHK-1002"), + CancellationToken.None); + + Assert.Equal("demo-context", response.ContextId); + Assert.NotNull(response.Transaction); + Assert.Equal("CHK-1002", response.Transaction!.TransactionId); + Assert.Equal("awaiting-payment", response.Transaction.Status); + } + [Fact] public async Task CapturePosPaymentUseCase_WhenOrderServed_ReturnsCapturedStatus() {