From a24ccc12d942807d84322d0ed740321263e77cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Tue, 31 Mar 2026 16:14:50 -0600 Subject: [PATCH] feat(operations-service): expand restaurant workflow contracts --- Operations.Service.slnx | 3 + docs/api/internal-workflow-contracts.md | 33 ++++ docs/roadmap/feature-epics.md | 1 + docs/runbooks/containerization.md | 2 + .../Ports/DefaultOperationsWorkflowPort.cs | 185 ++++++++++++++++++ .../Ports/IOperationsWorkflowPort.cs | 15 ++ .../UseCases/CapturePosPaymentUseCase.cs | 13 ++ .../UseCases/GetCustomerOrderStatusUseCase.cs | 13 ++ .../GetPosTransactionSummaryUseCase.cs | 13 ++ .../GetRestaurantAdminConfigUseCase.cs | 13 ++ .../UseCases/GetWaiterAssignmentsUseCase.cs | 13 ++ .../UseCases/ICapturePosPaymentUseCase.cs | 9 + .../IGetCustomerOrderStatusUseCase.cs | 9 + .../IGetPosTransactionSummaryUseCase.cs | 9 + .../IGetRestaurantAdminConfigUseCase.cs | 9 + .../UseCases/IGetWaiterAssignmentsUseCase.cs | 9 + .../UseCases/ISetServiceWindowUseCase.cs | 9 + .../UseCases/ISubmitRestaurantOrderUseCase.cs | 9 + .../UseCases/SetServiceWindowUseCase.cs | 13 ++ .../UseCases/SubmitRestaurantOrderUseCase.cs | 13 ++ .../Contracts/ConfigChangeContract.cs | 8 + .../Contracts/CustomerOrderStatusContract.cs | 8 + .../Contracts/PosPaymentActivityContract.cs | 9 + .../ServiceWindowSnapshotContract.cs | 7 + .../Contracts/WaiterAssignmentContract.cs | 7 + .../Requests/CapturePosPaymentRequest.cs | 8 + .../Requests/GetCustomerOrderStatusRequest.cs | 3 + .../GetPosTransactionSummaryRequest.cs | 3 + .../GetRestaurantAdminConfigRequest.cs | 3 + .../Requests/GetWaiterAssignmentsRequest.cs | 3 + .../Requests/SetServiceWindowRequest.cs | 8 + .../Requests/SubmitRestaurantOrderRequest.cs | 7 + .../Responses/CapturePosPaymentResponse.cs | 9 + .../GetCustomerOrderStatusResponse.cs | 9 + .../GetPosTransactionSummaryResponse.cs | 10 + .../GetRestaurantAdminConfigResponse.cs | 11 ++ .../Responses/GetWaiterAssignmentsResponse.cs | 10 + .../Responses/SetServiceWindowResponse.cs | 9 + .../SubmitRestaurantOrderResponse.cs | 9 + src/Operations.Service.Grpc/Program.cs | 64 ++++++ ...tions.Service.Application.UnitTests.csproj | 19 ++ .../OperationsWorkflowUseCasesTests.cs | 97 +++++++++ 42 files changed, 714 insertions(+) create mode 100644 docs/api/internal-workflow-contracts.md create mode 100644 src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs create mode 100644 src/Operations.Service.Application/Ports/IOperationsWorkflowPort.cs create mode 100644 src/Operations.Service.Application/UseCases/CapturePosPaymentUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/GetCustomerOrderStatusUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/GetPosTransactionSummaryUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/GetRestaurantAdminConfigUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/GetWaiterAssignmentsUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/ICapturePosPaymentUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/IGetCustomerOrderStatusUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/IGetPosTransactionSummaryUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/IGetRestaurantAdminConfigUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/IGetWaiterAssignmentsUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/ISetServiceWindowUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/ISubmitRestaurantOrderUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/SetServiceWindowUseCase.cs create mode 100644 src/Operations.Service.Application/UseCases/SubmitRestaurantOrderUseCase.cs create mode 100644 src/Operations.Service.Contracts/Contracts/ConfigChangeContract.cs create mode 100644 src/Operations.Service.Contracts/Contracts/CustomerOrderStatusContract.cs create mode 100644 src/Operations.Service.Contracts/Contracts/PosPaymentActivityContract.cs create mode 100644 src/Operations.Service.Contracts/Contracts/ServiceWindowSnapshotContract.cs create mode 100644 src/Operations.Service.Contracts/Contracts/WaiterAssignmentContract.cs create mode 100644 src/Operations.Service.Contracts/Requests/CapturePosPaymentRequest.cs create mode 100644 src/Operations.Service.Contracts/Requests/GetCustomerOrderStatusRequest.cs create mode 100644 src/Operations.Service.Contracts/Requests/GetPosTransactionSummaryRequest.cs create mode 100644 src/Operations.Service.Contracts/Requests/GetRestaurantAdminConfigRequest.cs create mode 100644 src/Operations.Service.Contracts/Requests/GetWaiterAssignmentsRequest.cs create mode 100644 src/Operations.Service.Contracts/Requests/SetServiceWindowRequest.cs create mode 100644 src/Operations.Service.Contracts/Requests/SubmitRestaurantOrderRequest.cs create mode 100644 src/Operations.Service.Contracts/Responses/CapturePosPaymentResponse.cs create mode 100644 src/Operations.Service.Contracts/Responses/GetCustomerOrderStatusResponse.cs create mode 100644 src/Operations.Service.Contracts/Responses/GetPosTransactionSummaryResponse.cs create mode 100644 src/Operations.Service.Contracts/Responses/GetRestaurantAdminConfigResponse.cs create mode 100644 src/Operations.Service.Contracts/Responses/GetWaiterAssignmentsResponse.cs create mode 100644 src/Operations.Service.Contracts/Responses/SetServiceWindowResponse.cs create mode 100644 src/Operations.Service.Contracts/Responses/SubmitRestaurantOrderResponse.cs create mode 100644 tests/Operations.Service.Application.UnitTests/Operations.Service.Application.UnitTests.csproj create mode 100644 tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs diff --git a/Operations.Service.slnx b/Operations.Service.slnx index 2387a1d..f770ed3 100644 --- a/Operations.Service.slnx +++ b/Operations.Service.slnx @@ -4,4 +4,7 @@ + + + diff --git a/docs/api/internal-workflow-contracts.md b/docs/api/internal-workflow-contracts.md new file mode 100644 index 0000000..279722e --- /dev/null +++ b/docs/api/internal-workflow-contracts.md @@ -0,0 +1,33 @@ +# Internal Workflow Contracts + +## Purpose + +`operations-service` now exposes workflow-shaped internal endpoints that the restaurant BFFs can consume without inventing their own orchestration payloads. + +## Endpoint Surface + +- `GET /internal/operations/config?locationId=` +- `POST /internal/operations/decision` +- `GET /internal/operations/waiter/assignments?contextId=` +- `POST /internal/operations/orders` +- `GET /internal/operations/customer/status?contextId=` +- `GET /internal/operations/pos/summary?contextId=` +- `POST /internal/operations/pos/payments` +- `GET /internal/operations/admin/config?contextId=` +- `POST /internal/operations/admin/service-window` + +## Contract Depth Added In Stage 41 + +The new workflow contracts add enough shape for the next BFF layer tasks to expose richer responses: + +- waiter assignments plus recent activity +- customer order status plus recent status events +- POS summary plus recent payment activity +- restaurant admin snapshot plus service windows and recent config changes +- workflow write responses that include status/message detail instead of only a boolean summary + +## Current Runtime Shape + +- The default implementation is still in-memory and deterministic. +- This repo remains orchestration-only; no DAL redesign is introduced by this task. +- Demo realism can deepen later without forcing BFF or SPA contract churn. diff --git a/docs/roadmap/feature-epics.md b/docs/roadmap/feature-epics.md index dfa1d60..51326a2 100644 --- a/docs/roadmap/feature-epics.md +++ b/docs/roadmap/feature-epics.md @@ -13,6 +13,7 @@ operations-service - Kitchen queue and dispatch optimization hooks. - Operations control-plane policies (flags, service windows, overrides). - POS closeout and settlement summary alignment. +- Waiter/customer/POS/admin workflow contracts that can be reused by multiple restaurant BFFs. ## Documentation Contract Any code change in this repository must include docs updates in the same branch. diff --git a/docs/runbooks/containerization.md b/docs/runbooks/containerization.md index ca954df..8d15ad9 100644 --- a/docs/runbooks/containerization.md +++ b/docs/runbooks/containerization.md @@ -23,6 +23,7 @@ docker run --rm -p 8080:8080 --name operations-service agilewebs/operations-serv ## Runtime Notes - Exposes internal control-plane evaluation/config endpoints. +- Also exposes workflow-shaped internal endpoints for waiter assignments, customer order status, POS summaries/payment capture, and restaurant-admin service-window updates. ## Health Endpoint Consistency @@ -38,3 +39,4 @@ docker run --rm -p 8080:8080 --name operations-service agilewebs/operations-serv - Current runtime adapters are still predominantly in-memory for deterministic local/demo behavior. - Demo PostgreSQL seeds validate integration contracts and smoke determinism, but do not yet imply full persistence implementation parity. +- Stage 41 adds contract depth first; downstream BFFs still need to adopt the new internal endpoints before the richer workflow data reaches the web apps. diff --git a/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs b/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs new file mode 100644 index 0000000..f406027 --- /dev/null +++ b/src/Operations.Service.Application/Ports/DefaultOperationsWorkflowPort.cs @@ -0,0 +1,185 @@ +using Operations.Service.Contracts.Contracts; +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.Ports; + +public sealed class DefaultOperationsWorkflowPort : IOperationsWorkflowPort +{ + public Task GetWaiterAssignmentsAsync( + GetWaiterAssignmentsRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var assignments = new[] + { + new WaiterAssignmentContract("waiter-01", "T-12", "serving", 2), + new WaiterAssignmentContract("waiter-07", "T-08", "ready-for-check", 1) + }; + var activity = new[] + { + $"{request.ContextId}: table T-12 requested dessert menus", + $"{request.ContextId}: table T-08 is waiting for payment capture" + }; + + return Task.FromResult(new GetWaiterAssignmentsResponse( + request.ContextId, + "restaurant-demo", + $"{assignments.Length} active waiter assignments are currently visible.", + assignments, + activity)); + } + + public Task SubmitRestaurantOrderAsync( + SubmitRestaurantOrderRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var accepted = request.ItemCount > 0 && + !string.IsNullOrWhiteSpace(request.ContextId) && + !string.IsNullOrWhiteSpace(request.OrderId) && + !string.IsNullOrWhiteSpace(request.TableId); + + return Task.FromResult(new SubmitRestaurantOrderResponse( + request.ContextId, + request.OrderId, + accepted, + accepted + ? $"Order {request.OrderId} for table {request.TableId} was accepted with {request.ItemCount} items." + : "Order payload is incomplete.", + accepted ? "queued" : "rejected", + DateTime.UtcNow)); + } + + public Task GetCustomerOrderStatusAsync( + GetCustomerOrderStatusRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var orders = new[] + { + new CustomerOrderStatusContract("CO-1001", "T-08", "preparing", 2, new[] { "ITEM-101", "ITEM-202" }), + new CustomerOrderStatusContract("CO-1002", "T-15", "ready", 4, new[] { "ITEM-301", "ITEM-404", "ITEM-405" }) + }; + var events = new[] + { + "CO-1001 moved to preparing at kitchen hot-line station.", + "CO-1002 is ready for table pickup." + }; + + return Task.FromResult(new GetCustomerOrderStatusResponse( + request.ContextId, + $"{orders.Length} recent customer orders are visible for the active context.", + orders, + events)); + } + + public Task GetPosTransactionSummaryAsync( + GetPosTransactionSummaryRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var payments = new[] + { + new PosPaymentActivityContract("POS-9001", "card", 25.50m, "USD", "captured", DateTime.UtcNow.AddMinutes(-18)), + new PosPaymentActivityContract("POS-9002", "wallet", 12.00m, "USD", "pending", DateTime.UtcNow.AddMinutes(-6)) + }; + + return Task.FromResult(new GetPosTransactionSummaryResponse( + request.ContextId, + "Open POS balance reflects one captured payment and one pending settlement.", + 37.50m, + "USD", + payments)); + } + + public Task CapturePosPaymentAsync( + CapturePosPaymentRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var succeeded = request.Amount > 0 && + !string.IsNullOrWhiteSpace(request.TransactionId) && + !string.IsNullOrWhiteSpace(request.PaymentMethod); + + return Task.FromResult(new CapturePosPaymentResponse( + request.ContextId, + request.TransactionId, + succeeded, + succeeded + ? $"Captured {request.Amount:0.00} {request.Currency} using {request.PaymentMethod}." + : "Payment capture request is incomplete.", + succeeded ? "captured" : "failed", + DateTime.UtcNow)); + } + + public Task GetRestaurantAdminConfigAsync( + GetRestaurantAdminConfigRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var featureFlags = new[] + { + new FeatureFlagStateContract("kitchen.dispatch.enabled", true), + new FeatureFlagStateContract("orders.priority.escalation", false), + new FeatureFlagStateContract("pos.closeout.preview", true) + }; + var serviceWindows = BuildServiceWindows(); + var changes = new[] + { + new ConfigChangeContract("CFG-100", "service-window", "Extended Friday dinner service window.", "admin-operator", DateTime.UtcNow.AddHours(-6)), + new ConfigChangeContract("CFG-101", "feature-flag", "Enabled POS closeout preview mode.", "ops-lead", DateTime.UtcNow.AddHours(-2)) + }; + + return Task.FromResult(new GetRestaurantAdminConfigResponse( + request.ContextId, + "Restaurant admin snapshot includes current flags, service windows, and recent control-plane changes.", + "v2", + featureFlags, + serviceWindows, + changes)); + } + + public Task SetServiceWindowAsync( + SetServiceWindowRequest request, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var applied = !string.IsNullOrWhiteSpace(request.ContextId) && + request.DayOfWeek is >= 0 and <= 6 && + !string.IsNullOrWhiteSpace(request.OpenAt) && + !string.IsNullOrWhiteSpace(request.CloseAt) && + !string.IsNullOrWhiteSpace(request.UpdatedBy); + + var serviceWindow = new ServiceWindowSnapshotContract( + request.DayOfWeek, + request.OpenAt, + request.CloseAt, + false); + + return Task.FromResult(new SetServiceWindowResponse( + request.ContextId, + applied, + applied + ? $"Service window updated by {request.UpdatedBy}." + : "Service window request is incomplete.", + serviceWindow)); + } + + private static IReadOnlyCollection BuildServiceWindows() + { + return new[] + { + new ServiceWindowSnapshotContract(1, "08:00:00", "22:00:00", false), + new ServiceWindowSnapshotContract(5, "08:00:00", "23:30:00", false), + new ServiceWindowSnapshotContract(6, "09:00:00", "23:00:00", false) + }; + } +} diff --git a/src/Operations.Service.Application/Ports/IOperationsWorkflowPort.cs b/src/Operations.Service.Application/Ports/IOperationsWorkflowPort.cs new file mode 100644 index 0000000..862d4fd --- /dev/null +++ b/src/Operations.Service.Application/Ports/IOperationsWorkflowPort.cs @@ -0,0 +1,15 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.Ports; + +public interface IOperationsWorkflowPort +{ + Task GetWaiterAssignmentsAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken); + Task SubmitRestaurantOrderAsync(SubmitRestaurantOrderRequest request, CancellationToken cancellationToken); + Task GetCustomerOrderStatusAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken); + Task GetPosTransactionSummaryAsync(GetPosTransactionSummaryRequest 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/CapturePosPaymentUseCase.cs b/src/Operations.Service.Application/UseCases/CapturePosPaymentUseCase.cs new file mode 100644 index 0000000..fb59e86 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/CapturePosPaymentUseCase.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 CapturePosPaymentUseCase(IOperationsWorkflowPort workflowPort) : ICapturePosPaymentUseCase +{ + public Task HandleAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken) + { + return workflowPort.CapturePosPaymentAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/GetCustomerOrderStatusUseCase.cs b/src/Operations.Service.Application/UseCases/GetCustomerOrderStatusUseCase.cs new file mode 100644 index 0000000..c499304 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/GetCustomerOrderStatusUseCase.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 GetCustomerOrderStatusUseCase(IOperationsWorkflowPort workflowPort) : IGetCustomerOrderStatusUseCase +{ + public Task HandleAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken) + { + return workflowPort.GetCustomerOrderStatusAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/GetPosTransactionSummaryUseCase.cs b/src/Operations.Service.Application/UseCases/GetPosTransactionSummaryUseCase.cs new file mode 100644 index 0000000..ed27ef6 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/GetPosTransactionSummaryUseCase.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 GetPosTransactionSummaryUseCase(IOperationsWorkflowPort workflowPort) : IGetPosTransactionSummaryUseCase +{ + public Task HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken) + { + return workflowPort.GetPosTransactionSummaryAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/GetRestaurantAdminConfigUseCase.cs b/src/Operations.Service.Application/UseCases/GetRestaurantAdminConfigUseCase.cs new file mode 100644 index 0000000..924d486 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/GetRestaurantAdminConfigUseCase.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 GetRestaurantAdminConfigUseCase(IOperationsWorkflowPort workflowPort) : IGetRestaurantAdminConfigUseCase +{ + public Task HandleAsync(GetRestaurantAdminConfigRequest request, CancellationToken cancellationToken) + { + return workflowPort.GetRestaurantAdminConfigAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/GetWaiterAssignmentsUseCase.cs b/src/Operations.Service.Application/UseCases/GetWaiterAssignmentsUseCase.cs new file mode 100644 index 0000000..e4d1598 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/GetWaiterAssignmentsUseCase.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 GetWaiterAssignmentsUseCase(IOperationsWorkflowPort workflowPort) : IGetWaiterAssignmentsUseCase +{ + public Task HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken) + { + return workflowPort.GetWaiterAssignmentsAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/ICapturePosPaymentUseCase.cs b/src/Operations.Service.Application/UseCases/ICapturePosPaymentUseCase.cs new file mode 100644 index 0000000..b5765b7 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/ICapturePosPaymentUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface ICapturePosPaymentUseCase +{ + Task HandleAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/IGetCustomerOrderStatusUseCase.cs b/src/Operations.Service.Application/UseCases/IGetCustomerOrderStatusUseCase.cs new file mode 100644 index 0000000..b6265a3 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/IGetCustomerOrderStatusUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface IGetCustomerOrderStatusUseCase +{ + Task HandleAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/IGetPosTransactionSummaryUseCase.cs b/src/Operations.Service.Application/UseCases/IGetPosTransactionSummaryUseCase.cs new file mode 100644 index 0000000..5e1f22e --- /dev/null +++ b/src/Operations.Service.Application/UseCases/IGetPosTransactionSummaryUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface IGetPosTransactionSummaryUseCase +{ + Task HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/IGetRestaurantAdminConfigUseCase.cs b/src/Operations.Service.Application/UseCases/IGetRestaurantAdminConfigUseCase.cs new file mode 100644 index 0000000..baa9c17 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/IGetRestaurantAdminConfigUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface IGetRestaurantAdminConfigUseCase +{ + Task HandleAsync(GetRestaurantAdminConfigRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/IGetWaiterAssignmentsUseCase.cs b/src/Operations.Service.Application/UseCases/IGetWaiterAssignmentsUseCase.cs new file mode 100644 index 0000000..c4c18cd --- /dev/null +++ b/src/Operations.Service.Application/UseCases/IGetWaiterAssignmentsUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface IGetWaiterAssignmentsUseCase +{ + Task HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/ISetServiceWindowUseCase.cs b/src/Operations.Service.Application/UseCases/ISetServiceWindowUseCase.cs new file mode 100644 index 0000000..bad09dc --- /dev/null +++ b/src/Operations.Service.Application/UseCases/ISetServiceWindowUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface ISetServiceWindowUseCase +{ + Task HandleAsync(SetServiceWindowRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/ISubmitRestaurantOrderUseCase.cs b/src/Operations.Service.Application/UseCases/ISubmitRestaurantOrderUseCase.cs new file mode 100644 index 0000000..87d88d9 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/ISubmitRestaurantOrderUseCase.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Requests; +using Operations.Service.Contracts.Responses; + +namespace Operations.Service.Application.UseCases; + +public interface ISubmitRestaurantOrderUseCase +{ + Task HandleAsync(SubmitRestaurantOrderRequest request, CancellationToken cancellationToken); +} diff --git a/src/Operations.Service.Application/UseCases/SetServiceWindowUseCase.cs b/src/Operations.Service.Application/UseCases/SetServiceWindowUseCase.cs new file mode 100644 index 0000000..670c7fa --- /dev/null +++ b/src/Operations.Service.Application/UseCases/SetServiceWindowUseCase.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 SetServiceWindowUseCase(IOperationsWorkflowPort workflowPort) : ISetServiceWindowUseCase +{ + public Task HandleAsync(SetServiceWindowRequest request, CancellationToken cancellationToken) + { + return workflowPort.SetServiceWindowAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Application/UseCases/SubmitRestaurantOrderUseCase.cs b/src/Operations.Service.Application/UseCases/SubmitRestaurantOrderUseCase.cs new file mode 100644 index 0000000..0597994 --- /dev/null +++ b/src/Operations.Service.Application/UseCases/SubmitRestaurantOrderUseCase.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 SubmitRestaurantOrderUseCase(IOperationsWorkflowPort workflowPort) : ISubmitRestaurantOrderUseCase +{ + public Task HandleAsync(SubmitRestaurantOrderRequest request, CancellationToken cancellationToken) + { + return workflowPort.SubmitRestaurantOrderAsync(request, cancellationToken); + } +} diff --git a/src/Operations.Service.Contracts/Contracts/ConfigChangeContract.cs b/src/Operations.Service.Contracts/Contracts/ConfigChangeContract.cs new file mode 100644 index 0000000..ed47bd4 --- /dev/null +++ b/src/Operations.Service.Contracts/Contracts/ConfigChangeContract.cs @@ -0,0 +1,8 @@ +namespace Operations.Service.Contracts.Contracts; + +public sealed record ConfigChangeContract( + string ChangeId, + string Category, + string Description, + string UpdatedBy, + DateTime UpdatedAtUtc); diff --git a/src/Operations.Service.Contracts/Contracts/CustomerOrderStatusContract.cs b/src/Operations.Service.Contracts/Contracts/CustomerOrderStatusContract.cs new file mode 100644 index 0000000..d88eac2 --- /dev/null +++ b/src/Operations.Service.Contracts/Contracts/CustomerOrderStatusContract.cs @@ -0,0 +1,8 @@ +namespace Operations.Service.Contracts.Contracts; + +public sealed record CustomerOrderStatusContract( + string OrderId, + string TableId, + string Status, + int GuestCount, + IReadOnlyCollection ItemIds); diff --git a/src/Operations.Service.Contracts/Contracts/PosPaymentActivityContract.cs b/src/Operations.Service.Contracts/Contracts/PosPaymentActivityContract.cs new file mode 100644 index 0000000..8d89ea7 --- /dev/null +++ b/src/Operations.Service.Contracts/Contracts/PosPaymentActivityContract.cs @@ -0,0 +1,9 @@ +namespace Operations.Service.Contracts.Contracts; + +public sealed record PosPaymentActivityContract( + string TransactionId, + string PaymentMethod, + decimal Amount, + string Currency, + string Status, + DateTime CapturedAtUtc); diff --git a/src/Operations.Service.Contracts/Contracts/ServiceWindowSnapshotContract.cs b/src/Operations.Service.Contracts/Contracts/ServiceWindowSnapshotContract.cs new file mode 100644 index 0000000..79a25df --- /dev/null +++ b/src/Operations.Service.Contracts/Contracts/ServiceWindowSnapshotContract.cs @@ -0,0 +1,7 @@ +namespace Operations.Service.Contracts.Contracts; + +public sealed record ServiceWindowSnapshotContract( + int DayOfWeek, + string OpenAt, + string CloseAt, + bool IsClosed); diff --git a/src/Operations.Service.Contracts/Contracts/WaiterAssignmentContract.cs b/src/Operations.Service.Contracts/Contracts/WaiterAssignmentContract.cs new file mode 100644 index 0000000..40d9aaa --- /dev/null +++ b/src/Operations.Service.Contracts/Contracts/WaiterAssignmentContract.cs @@ -0,0 +1,7 @@ +namespace Operations.Service.Contracts.Contracts; + +public sealed record WaiterAssignmentContract( + string WaiterId, + string TableId, + string Status, + int ActiveOrders); diff --git a/src/Operations.Service.Contracts/Requests/CapturePosPaymentRequest.cs b/src/Operations.Service.Contracts/Requests/CapturePosPaymentRequest.cs new file mode 100644 index 0000000..2155ce9 --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/CapturePosPaymentRequest.cs @@ -0,0 +1,8 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record CapturePosPaymentRequest( + string ContextId, + string TransactionId, + decimal Amount, + string Currency, + string PaymentMethod); diff --git a/src/Operations.Service.Contracts/Requests/GetCustomerOrderStatusRequest.cs b/src/Operations.Service.Contracts/Requests/GetCustomerOrderStatusRequest.cs new file mode 100644 index 0000000..10cd8cf --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/GetCustomerOrderStatusRequest.cs @@ -0,0 +1,3 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record GetCustomerOrderStatusRequest(string ContextId); diff --git a/src/Operations.Service.Contracts/Requests/GetPosTransactionSummaryRequest.cs b/src/Operations.Service.Contracts/Requests/GetPosTransactionSummaryRequest.cs new file mode 100644 index 0000000..6b5f8f3 --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/GetPosTransactionSummaryRequest.cs @@ -0,0 +1,3 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record GetPosTransactionSummaryRequest(string ContextId); diff --git a/src/Operations.Service.Contracts/Requests/GetRestaurantAdminConfigRequest.cs b/src/Operations.Service.Contracts/Requests/GetRestaurantAdminConfigRequest.cs new file mode 100644 index 0000000..2ff3855 --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/GetRestaurantAdminConfigRequest.cs @@ -0,0 +1,3 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record GetRestaurantAdminConfigRequest(string ContextId); diff --git a/src/Operations.Service.Contracts/Requests/GetWaiterAssignmentsRequest.cs b/src/Operations.Service.Contracts/Requests/GetWaiterAssignmentsRequest.cs new file mode 100644 index 0000000..0f37550 --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/GetWaiterAssignmentsRequest.cs @@ -0,0 +1,3 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record GetWaiterAssignmentsRequest(string ContextId); diff --git a/src/Operations.Service.Contracts/Requests/SetServiceWindowRequest.cs b/src/Operations.Service.Contracts/Requests/SetServiceWindowRequest.cs new file mode 100644 index 0000000..3158e5a --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/SetServiceWindowRequest.cs @@ -0,0 +1,8 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record SetServiceWindowRequest( + string ContextId, + int DayOfWeek, + string OpenAt, + string CloseAt, + string UpdatedBy); diff --git a/src/Operations.Service.Contracts/Requests/SubmitRestaurantOrderRequest.cs b/src/Operations.Service.Contracts/Requests/SubmitRestaurantOrderRequest.cs new file mode 100644 index 0000000..6e930c7 --- /dev/null +++ b/src/Operations.Service.Contracts/Requests/SubmitRestaurantOrderRequest.cs @@ -0,0 +1,7 @@ +namespace Operations.Service.Contracts.Requests; + +public sealed record SubmitRestaurantOrderRequest( + string ContextId, + string OrderId, + string TableId, + int ItemCount); diff --git a/src/Operations.Service.Contracts/Responses/CapturePosPaymentResponse.cs b/src/Operations.Service.Contracts/Responses/CapturePosPaymentResponse.cs new file mode 100644 index 0000000..582072f --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/CapturePosPaymentResponse.cs @@ -0,0 +1,9 @@ +namespace Operations.Service.Contracts.Responses; + +public sealed record CapturePosPaymentResponse( + string ContextId, + string TransactionId, + bool Succeeded, + string Summary, + string Status, + DateTime CapturedAtUtc); diff --git a/src/Operations.Service.Contracts/Responses/GetCustomerOrderStatusResponse.cs b/src/Operations.Service.Contracts/Responses/GetCustomerOrderStatusResponse.cs new file mode 100644 index 0000000..ca39bbc --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/GetCustomerOrderStatusResponse.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Contracts; + +namespace Operations.Service.Contracts.Responses; + +public sealed record GetCustomerOrderStatusResponse( + string ContextId, + string Summary, + IReadOnlyCollection Orders, + IReadOnlyCollection RecentEvents); diff --git a/src/Operations.Service.Contracts/Responses/GetPosTransactionSummaryResponse.cs b/src/Operations.Service.Contracts/Responses/GetPosTransactionSummaryResponse.cs new file mode 100644 index 0000000..c43adf1 --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/GetPosTransactionSummaryResponse.cs @@ -0,0 +1,10 @@ +using Operations.Service.Contracts.Contracts; + +namespace Operations.Service.Contracts.Responses; + +public sealed record GetPosTransactionSummaryResponse( + string ContextId, + string Summary, + decimal OpenBalance, + string Currency, + IReadOnlyCollection RecentPayments); diff --git a/src/Operations.Service.Contracts/Responses/GetRestaurantAdminConfigResponse.cs b/src/Operations.Service.Contracts/Responses/GetRestaurantAdminConfigResponse.cs new file mode 100644 index 0000000..ddc3bff --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/GetRestaurantAdminConfigResponse.cs @@ -0,0 +1,11 @@ +using Operations.Service.Contracts.Contracts; + +namespace Operations.Service.Contracts.Responses; + +public sealed record GetRestaurantAdminConfigResponse( + string ContextId, + string Summary, + string Version, + IReadOnlyCollection FeatureFlags, + IReadOnlyCollection ServiceWindows, + IReadOnlyCollection RecentChanges); diff --git a/src/Operations.Service.Contracts/Responses/GetWaiterAssignmentsResponse.cs b/src/Operations.Service.Contracts/Responses/GetWaiterAssignmentsResponse.cs new file mode 100644 index 0000000..a7cb15b --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/GetWaiterAssignmentsResponse.cs @@ -0,0 +1,10 @@ +using Operations.Service.Contracts.Contracts; + +namespace Operations.Service.Contracts.Responses; + +public sealed record GetWaiterAssignmentsResponse( + string ContextId, + string LocationId, + string Summary, + IReadOnlyCollection Assignments, + IReadOnlyCollection RecentActivity); diff --git a/src/Operations.Service.Contracts/Responses/SetServiceWindowResponse.cs b/src/Operations.Service.Contracts/Responses/SetServiceWindowResponse.cs new file mode 100644 index 0000000..df39c6e --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/SetServiceWindowResponse.cs @@ -0,0 +1,9 @@ +using Operations.Service.Contracts.Contracts; + +namespace Operations.Service.Contracts.Responses; + +public sealed record SetServiceWindowResponse( + string ContextId, + bool Applied, + string Message, + ServiceWindowSnapshotContract ServiceWindow); diff --git a/src/Operations.Service.Contracts/Responses/SubmitRestaurantOrderResponse.cs b/src/Operations.Service.Contracts/Responses/SubmitRestaurantOrderResponse.cs new file mode 100644 index 0000000..bd8ab4a --- /dev/null +++ b/src/Operations.Service.Contracts/Responses/SubmitRestaurantOrderResponse.cs @@ -0,0 +1,9 @@ +namespace Operations.Service.Contracts.Responses; + +public sealed record SubmitRestaurantOrderResponse( + string ContextId, + string OrderId, + bool Accepted, + string Summary, + string Status, + DateTime SubmittedAtUtc); diff --git a/src/Operations.Service.Grpc/Program.cs b/src/Operations.Service.Grpc/Program.cs index 7650559..718514a 100644 --- a/src/Operations.Service.Grpc/Program.cs +++ b/src/Operations.Service.Grpc/Program.cs @@ -4,8 +4,16 @@ using Operations.Service.Contracts.Requests; var builder = WebApplication.CreateBuilder(args); 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(); +builder.Services.AddSingleton(); var app = builder.Build(); @@ -23,6 +31,62 @@ app.MapPost("/internal/operations/decision", async ( return Results.Ok(await useCase.HandleAsync(request, ct)); }); +app.MapGet("/internal/operations/waiter/assignments", async ( + string contextId, + IGetWaiterAssignmentsUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(new GetWaiterAssignmentsRequest(contextId), ct)); +}); + +app.MapPost("/internal/operations/orders", async ( + SubmitRestaurantOrderRequest request, + ISubmitRestaurantOrderUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(request, ct)); +}); + +app.MapGet("/internal/operations/customer/status", async ( + string contextId, + IGetCustomerOrderStatusUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(new GetCustomerOrderStatusRequest(contextId), ct)); +}); + +app.MapGet("/internal/operations/pos/summary", async ( + string contextId, + IGetPosTransactionSummaryUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(new GetPosTransactionSummaryRequest(contextId), ct)); +}); + +app.MapPost("/internal/operations/pos/payments", async ( + CapturePosPaymentRequest request, + ICapturePosPaymentUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(request, ct)); +}); + +app.MapGet("/internal/operations/admin/config", async ( + string contextId, + IGetRestaurantAdminConfigUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(new GetRestaurantAdminConfigRequest(contextId), ct)); +}); + +app.MapPost("/internal/operations/admin/service-window", async ( + SetServiceWindowRequest request, + ISetServiceWindowUseCase useCase, + CancellationToken ct) => +{ + return Results.Ok(await useCase.HandleAsync(request, ct)); +}); + app.MapGet("/health", () => Results.Ok(new { status = "ok", service = "operations-service" })); app.MapGet("/healthz", () => Results.Ok(new { status = "ok", service = "operations-service" })); diff --git a/tests/Operations.Service.Application.UnitTests/Operations.Service.Application.UnitTests.csproj b/tests/Operations.Service.Application.UnitTests/Operations.Service.Application.UnitTests.csproj new file mode 100644 index 0000000..1cd1ab8 --- /dev/null +++ b/tests/Operations.Service.Application.UnitTests/Operations.Service.Application.UnitTests.csproj @@ -0,0 +1,19 @@ + + + net10.0 + enable + enable + false + + + + + + + + + + + + + diff --git a/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs b/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs new file mode 100644 index 0000000..a0b1b3e --- /dev/null +++ b/tests/Operations.Service.Application.UnitTests/OperationsWorkflowUseCasesTests.cs @@ -0,0 +1,97 @@ +using Operations.Service.Application.Ports; +using Operations.Service.Application.UseCases; +using Operations.Service.Contracts.Requests; + +namespace Operations.Service.Application.UnitTests; + +public class OperationsWorkflowUseCasesTests +{ + private readonly DefaultOperationsWorkflowPort workflowPort = new(); + + [Fact] + public async Task GetWaiterAssignmentsUseCase_ReturnsAssignmentsAndActivity() + { + var useCase = new GetWaiterAssignmentsUseCase(workflowPort); + + var response = await useCase.HandleAsync(new GetWaiterAssignmentsRequest("demo-context"), CancellationToken.None); + + Assert.Equal("demo-context", response.ContextId); + Assert.NotEmpty(response.Assignments); + Assert.NotEmpty(response.RecentActivity); + } + + [Fact] + public async Task SubmitRestaurantOrderUseCase_WhenRequestValid_AcceptsOrder() + { + var useCase = new SubmitRestaurantOrderUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new SubmitRestaurantOrderRequest("demo-context", "ORD-101", "T-12", 3), + CancellationToken.None); + + Assert.True(response.Accepted); + Assert.Equal("queued", response.Status); + } + + [Fact] + public async Task GetCustomerOrderStatusUseCase_ReturnsOrders() + { + var useCase = new GetCustomerOrderStatusUseCase(workflowPort); + + var response = await useCase.HandleAsync(new GetCustomerOrderStatusRequest("demo-context"), CancellationToken.None); + + Assert.Equal("demo-context", response.ContextId); + Assert.NotEmpty(response.Orders); + Assert.NotEmpty(response.RecentEvents); + } + + [Fact] + public async Task GetPosTransactionSummaryUseCase_ReturnsPaymentActivity() + { + var useCase = new GetPosTransactionSummaryUseCase(workflowPort); + + var response = await useCase.HandleAsync(new GetPosTransactionSummaryRequest("demo-context"), CancellationToken.None); + + Assert.Equal("USD", response.Currency); + Assert.NotEmpty(response.RecentPayments); + } + + [Fact] + public async Task CapturePosPaymentUseCase_WhenAmountPositive_ReturnsCapturedStatus() + { + var useCase = new CapturePosPaymentUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new CapturePosPaymentRequest("demo-context", "POS-9001", 25.50m, "USD", "card"), + CancellationToken.None); + + Assert.True(response.Succeeded); + Assert.Equal("captured", response.Status); + } + + [Fact] + public async Task GetRestaurantAdminConfigUseCase_ReturnsFlagsWindowsAndChanges() + { + var useCase = new GetRestaurantAdminConfigUseCase(workflowPort); + + var response = await useCase.HandleAsync(new GetRestaurantAdminConfigRequest("demo-context"), CancellationToken.None); + + Assert.Equal("demo-context", response.ContextId); + Assert.NotEmpty(response.FeatureFlags); + Assert.NotEmpty(response.ServiceWindows); + Assert.NotEmpty(response.RecentChanges); + } + + [Fact] + public async Task SetServiceWindowUseCase_WhenRequestValid_ReturnsAppliedResponse() + { + var useCase = new SetServiceWindowUseCase(workflowPort); + + var response = await useCase.HandleAsync( + new SetServiceWindowRequest("demo-context", 1, "08:00:00", "22:00:00", "admin-operator"), + CancellationToken.None); + + Assert.True(response.Applied); + Assert.Equal(1, response.ServiceWindow.DayOfWeek); + } +}