feat(operations-service): expand restaurant workflow contracts
This commit is contained in:
parent
b53a656d73
commit
a24ccc12d9
@ -4,4 +4,7 @@
|
||||
<Project Path="src/Operations.Service.Contracts/Operations.Service.Contracts.csproj" />
|
||||
<Project Path="src/Operations.Service.Grpc/Operations.Service.Grpc.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/Operations.Service.Application.UnitTests/Operations.Service.Application.UnitTests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
33
docs/api/internal-workflow-contracts.md
Normal file
33
docs/api/internal-workflow-contracts.md
Normal file
@ -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=<id>`
|
||||
- `POST /internal/operations/decision`
|
||||
- `GET /internal/operations/waiter/assignments?contextId=<id>`
|
||||
- `POST /internal/operations/orders`
|
||||
- `GET /internal/operations/customer/status?contextId=<id>`
|
||||
- `GET /internal/operations/pos/summary?contextId=<id>`
|
||||
- `POST /internal/operations/pos/payments`
|
||||
- `GET /internal/operations/admin/config?contextId=<id>`
|
||||
- `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.
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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<GetWaiterAssignmentsResponse> 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<SubmitRestaurantOrderResponse> 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<GetCustomerOrderStatusResponse> 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<GetPosTransactionSummaryResponse> 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<CapturePosPaymentResponse> 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<GetRestaurantAdminConfigResponse> 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<SetServiceWindowResponse> 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<ServiceWindowSnapshotContract> 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using Operations.Service.Contracts.Requests;
|
||||
using Operations.Service.Contracts.Responses;
|
||||
|
||||
namespace Operations.Service.Application.Ports;
|
||||
|
||||
public interface IOperationsWorkflowPort
|
||||
{
|
||||
Task<GetWaiterAssignmentsResponse> GetWaiterAssignmentsAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
|
||||
Task<SubmitRestaurantOrderResponse> SubmitRestaurantOrderAsync(SubmitRestaurantOrderRequest request, CancellationToken cancellationToken);
|
||||
Task<GetCustomerOrderStatusResponse> GetCustomerOrderStatusAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken);
|
||||
Task<GetPosTransactionSummaryResponse> GetPosTransactionSummaryAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
|
||||
Task<CapturePosPaymentResponse> CapturePosPaymentAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken);
|
||||
Task<GetRestaurantAdminConfigResponse> GetRestaurantAdminConfigAsync(GetRestaurantAdminConfigRequest request, CancellationToken cancellationToken);
|
||||
Task<SetServiceWindowResponse> SetServiceWindowAsync(SetServiceWindowRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -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<CapturePosPaymentResponse> HandleAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return workflowPort.CapturePosPaymentAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -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<GetCustomerOrderStatusResponse> HandleAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return workflowPort.GetCustomerOrderStatusAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -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<GetPosTransactionSummaryResponse> HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return workflowPort.GetPosTransactionSummaryAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -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<GetRestaurantAdminConfigResponse> HandleAsync(GetRestaurantAdminConfigRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return workflowPort.GetRestaurantAdminConfigAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -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<GetWaiterAssignmentsResponse> HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return workflowPort.GetWaiterAssignmentsAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Operations.Service.Contracts.Requests;
|
||||
using Operations.Service.Contracts.Responses;
|
||||
|
||||
namespace Operations.Service.Application.UseCases;
|
||||
|
||||
public interface ICapturePosPaymentUseCase
|
||||
{
|
||||
Task<CapturePosPaymentResponse> HandleAsync(CapturePosPaymentRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Operations.Service.Contracts.Requests;
|
||||
using Operations.Service.Contracts.Responses;
|
||||
|
||||
namespace Operations.Service.Application.UseCases;
|
||||
|
||||
public interface IGetCustomerOrderStatusUseCase
|
||||
{
|
||||
Task<GetCustomerOrderStatusResponse> HandleAsync(GetCustomerOrderStatusRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Operations.Service.Contracts.Requests;
|
||||
using Operations.Service.Contracts.Responses;
|
||||
|
||||
namespace Operations.Service.Application.UseCases;
|
||||
|
||||
public interface IGetPosTransactionSummaryUseCase
|
||||
{
|
||||
Task<GetPosTransactionSummaryResponse> HandleAsync(GetPosTransactionSummaryRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Operations.Service.Contracts.Requests;
|
||||
using Operations.Service.Contracts.Responses;
|
||||
|
||||
namespace Operations.Service.Application.UseCases;
|
||||
|
||||
public interface IGetRestaurantAdminConfigUseCase
|
||||
{
|
||||
Task<GetRestaurantAdminConfigResponse> HandleAsync(GetRestaurantAdminConfigRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Operations.Service.Contracts.Requests;
|
||||
using Operations.Service.Contracts.Responses;
|
||||
|
||||
namespace Operations.Service.Application.UseCases;
|
||||
|
||||
public interface IGetWaiterAssignmentsUseCase
|
||||
{
|
||||
Task<GetWaiterAssignmentsResponse> HandleAsync(GetWaiterAssignmentsRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Operations.Service.Contracts.Requests;
|
||||
using Operations.Service.Contracts.Responses;
|
||||
|
||||
namespace Operations.Service.Application.UseCases;
|
||||
|
||||
public interface ISetServiceWindowUseCase
|
||||
{
|
||||
Task<SetServiceWindowResponse> HandleAsync(SetServiceWindowRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Operations.Service.Contracts.Requests;
|
||||
using Operations.Service.Contracts.Responses;
|
||||
|
||||
namespace Operations.Service.Application.UseCases;
|
||||
|
||||
public interface ISubmitRestaurantOrderUseCase
|
||||
{
|
||||
Task<SubmitRestaurantOrderResponse> HandleAsync(SubmitRestaurantOrderRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -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<SetServiceWindowResponse> HandleAsync(SetServiceWindowRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return workflowPort.SetServiceWindowAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -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<SubmitRestaurantOrderResponse> HandleAsync(SubmitRestaurantOrderRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return workflowPort.SubmitRestaurantOrderAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Operations.Service.Contracts.Contracts;
|
||||
|
||||
public sealed record ConfigChangeContract(
|
||||
string ChangeId,
|
||||
string Category,
|
||||
string Description,
|
||||
string UpdatedBy,
|
||||
DateTime UpdatedAtUtc);
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Operations.Service.Contracts.Contracts;
|
||||
|
||||
public sealed record CustomerOrderStatusContract(
|
||||
string OrderId,
|
||||
string TableId,
|
||||
string Status,
|
||||
int GuestCount,
|
||||
IReadOnlyCollection<string> ItemIds);
|
||||
@ -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);
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Operations.Service.Contracts.Contracts;
|
||||
|
||||
public sealed record ServiceWindowSnapshotContract(
|
||||
int DayOfWeek,
|
||||
string OpenAt,
|
||||
string CloseAt,
|
||||
bool IsClosed);
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Operations.Service.Contracts.Contracts;
|
||||
|
||||
public sealed record WaiterAssignmentContract(
|
||||
string WaiterId,
|
||||
string TableId,
|
||||
string Status,
|
||||
int ActiveOrders);
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Operations.Service.Contracts.Requests;
|
||||
|
||||
public sealed record CapturePosPaymentRequest(
|
||||
string ContextId,
|
||||
string TransactionId,
|
||||
decimal Amount,
|
||||
string Currency,
|
||||
string PaymentMethod);
|
||||
@ -0,0 +1,3 @@
|
||||
namespace Operations.Service.Contracts.Requests;
|
||||
|
||||
public sealed record GetCustomerOrderStatusRequest(string ContextId);
|
||||
@ -0,0 +1,3 @@
|
||||
namespace Operations.Service.Contracts.Requests;
|
||||
|
||||
public sealed record GetPosTransactionSummaryRequest(string ContextId);
|
||||
@ -0,0 +1,3 @@
|
||||
namespace Operations.Service.Contracts.Requests;
|
||||
|
||||
public sealed record GetRestaurantAdminConfigRequest(string ContextId);
|
||||
@ -0,0 +1,3 @@
|
||||
namespace Operations.Service.Contracts.Requests;
|
||||
|
||||
public sealed record GetWaiterAssignmentsRequest(string ContextId);
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Operations.Service.Contracts.Requests;
|
||||
|
||||
public sealed record SetServiceWindowRequest(
|
||||
string ContextId,
|
||||
int DayOfWeek,
|
||||
string OpenAt,
|
||||
string CloseAt,
|
||||
string UpdatedBy);
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Operations.Service.Contracts.Requests;
|
||||
|
||||
public sealed record SubmitRestaurantOrderRequest(
|
||||
string ContextId,
|
||||
string OrderId,
|
||||
string TableId,
|
||||
int ItemCount);
|
||||
@ -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);
|
||||
@ -0,0 +1,9 @@
|
||||
using Operations.Service.Contracts.Contracts;
|
||||
|
||||
namespace Operations.Service.Contracts.Responses;
|
||||
|
||||
public sealed record GetCustomerOrderStatusResponse(
|
||||
string ContextId,
|
||||
string Summary,
|
||||
IReadOnlyCollection<CustomerOrderStatusContract> Orders,
|
||||
IReadOnlyCollection<string> RecentEvents);
|
||||
@ -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<PosPaymentActivityContract> RecentPayments);
|
||||
@ -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<FeatureFlagStateContract> FeatureFlags,
|
||||
IReadOnlyCollection<ServiceWindowSnapshotContract> ServiceWindows,
|
||||
IReadOnlyCollection<ConfigChangeContract> RecentChanges);
|
||||
@ -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<WaiterAssignmentContract> Assignments,
|
||||
IReadOnlyCollection<string> RecentActivity);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -4,8 +4,16 @@ using Operations.Service.Contracts.Requests;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddSingleton<IOperationsConfigReadPort, DefaultOperationsConfigReadPort>();
|
||||
builder.Services.AddSingleton<IOperationsWorkflowPort, DefaultOperationsWorkflowPort>();
|
||||
builder.Services.AddSingleton<IGetOperationsConfigUseCase, GetOperationsConfigUseCase>();
|
||||
builder.Services.AddSingleton<IEvaluateOperationalDecisionUseCase, EvaluateOperationalDecisionUseCase>();
|
||||
builder.Services.AddSingleton<IGetWaiterAssignmentsUseCase, GetWaiterAssignmentsUseCase>();
|
||||
builder.Services.AddSingleton<ISubmitRestaurantOrderUseCase, SubmitRestaurantOrderUseCase>();
|
||||
builder.Services.AddSingleton<IGetCustomerOrderStatusUseCase, GetCustomerOrderStatusUseCase>();
|
||||
builder.Services.AddSingleton<IGetPosTransactionSummaryUseCase, GetPosTransactionSummaryUseCase>();
|
||||
builder.Services.AddSingleton<ICapturePosPaymentUseCase, CapturePosPaymentUseCase>();
|
||||
builder.Services.AddSingleton<IGetRestaurantAdminConfigUseCase, GetRestaurantAdminConfigUseCase>();
|
||||
builder.Services.AddSingleton<ISetServiceWindowUseCase, SetServiceWindowUseCase>();
|
||||
|
||||
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" }));
|
||||
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/Operations.Service.Application/Operations.Service.Application.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user