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);
+ }
+}