From 4bd088e2b1f1f6c275e8f1243dcacbd73396358b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Tue, 31 Mar 2026 20:21:41 -0600 Subject: [PATCH] fix(operations-service): close orders after full payment --- docs/api/internal-workflow-contracts.md | 3 ++- .../Ports/DefaultOperationsWorkflowPort.cs | 2 ++ .../OperationsWorkflowUseCasesTests.cs | 20 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/api/internal-workflow-contracts.md b/docs/api/internal-workflow-contracts.md index 4ce46fa..2f43f79 100644 --- a/docs/api/internal-workflow-contracts.md +++ b/docs/api/internal-workflow-contracts.md @@ -29,13 +29,14 @@ That means: - 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 +- payment capture updates persisted check state, appends lifecycle events, and closes the customer-facing order lifecycle on full payment ## Contract Intent - waiter assignments surface floor-facing table attention derived from shared order/check state - 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 +- a fully paid check now advances customer-facing order state beyond `served`, which keeps order detail aligned with successful POS capture - 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 diff --git a/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs b/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs index 2f507a7..a09ad8f 100644 --- a/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs +++ b/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs @@ -265,10 +265,12 @@ public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort var nowUtc = DateTime.UtcNow; var remainingBalance = decimal.Max(record.OutstandingBalance - request.Amount, 0m); var nextCheckState = remainingBalance == 0m ? "Paid" : "AwaitingPayment"; + var nextOrderState = remainingBalance == 0m ? "Paid" : record.OrderState; // Partial captures keep the check open for the remaining balance; full captures close the check cleanly. var updated = record with { + OrderState = nextOrderState, CheckState = nextCheckState, OutstandingBalance = remainingBalance, UpdatedAtUtc = nowUtc diff --git a/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs b/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs index d9199ed..a0f2732 100644 --- a/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs +++ b/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs @@ -128,10 +128,30 @@ public class OperationsWorkflowUseCasesTests Assert.True(response.Succeeded); Assert.Equal("captured", response.Status); Assert.NotNull(updated); + Assert.Equal("Paid", updated!.OrderState); Assert.Equal("Paid", updated!.CheckState); Assert.Equal(0m, updated.OutstandingBalance); } + [Fact] + public async Task CapturePosPaymentUseCase_WhenPaymentIsPartial_KeepsOrderServed() + { + var useCase = new CapturePosPaymentUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new CapturePosPaymentRequest("demo-context", "CHK-1002", 12.50m, "USD", "card"), + CancellationToken.None); + + var updated = await lifecycleStore.GetOrderAsync("demo-context", "ORD-1002", CancellationToken.None); + + Assert.True(response.Succeeded); + Assert.Equal("partial", response.Status); + Assert.NotNull(updated); + Assert.Equal("Served", updated!.OrderState); + Assert.Equal("AwaitingPayment", updated.CheckState); + Assert.Equal(25.00m, updated.OutstandingBalance); + } + [Fact] public async Task CapturePosPaymentUseCase_WhenOrderNotServed_ReturnsBlockedStatus() {