feat(operations-service): expand restaurant workflow contracts

This commit is contained in:
José René White Enciso 2026-03-31 16:14:50 -06:00
parent b53a656d73
commit a24ccc12d9
42 changed files with 714 additions and 0 deletions

View File

@ -4,4 +4,7 @@
<Project Path="src/Operations.Service.Contracts/Operations.Service.Contracts.csproj" /> <Project Path="src/Operations.Service.Contracts/Operations.Service.Contracts.csproj" />
<Project Path="src/Operations.Service.Grpc/Operations.Service.Grpc.csproj" /> <Project Path="src/Operations.Service.Grpc/Operations.Service.Grpc.csproj" />
</Folder> </Folder>
<Folder Name="/tests/">
<Project Path="tests/Operations.Service.Application.UnitTests/Operations.Service.Application.UnitTests.csproj" />
</Folder>
</Solution> </Solution>

View 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.

View File

@ -13,6 +13,7 @@ operations-service
- Kitchen queue and dispatch optimization hooks. - Kitchen queue and dispatch optimization hooks.
- Operations control-plane policies (flags, service windows, overrides). - Operations control-plane policies (flags, service windows, overrides).
- POS closeout and settlement summary alignment. - POS closeout and settlement summary alignment.
- Waiter/customer/POS/admin workflow contracts that can be reused by multiple restaurant BFFs.
## Documentation Contract ## Documentation Contract
Any code change in this repository must include docs updates in the same branch. Any code change in this repository must include docs updates in the same branch.

View File

@ -23,6 +23,7 @@ docker run --rm -p 8080:8080 --name operations-service agilewebs/operations-serv
## Runtime Notes ## Runtime Notes
- Exposes internal control-plane evaluation/config endpoints. - 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 ## 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. - 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. - 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
namespace Operations.Service.Contracts.Contracts;
public sealed record ConfigChangeContract(
string ChangeId,
string Category,
string Description,
string UpdatedBy,
DateTime UpdatedAtUtc);

View File

@ -0,0 +1,8 @@
namespace Operations.Service.Contracts.Contracts;
public sealed record CustomerOrderStatusContract(
string OrderId,
string TableId,
string Status,
int GuestCount,
IReadOnlyCollection<string> ItemIds);

View File

@ -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);

View File

@ -0,0 +1,7 @@
namespace Operations.Service.Contracts.Contracts;
public sealed record ServiceWindowSnapshotContract(
int DayOfWeek,
string OpenAt,
string CloseAt,
bool IsClosed);

View File

@ -0,0 +1,7 @@
namespace Operations.Service.Contracts.Contracts;
public sealed record WaiterAssignmentContract(
string WaiterId,
string TableId,
string Status,
int ActiveOrders);

View File

@ -0,0 +1,8 @@
namespace Operations.Service.Contracts.Requests;
public sealed record CapturePosPaymentRequest(
string ContextId,
string TransactionId,
decimal Amount,
string Currency,
string PaymentMethod);

View File

@ -0,0 +1,3 @@
namespace Operations.Service.Contracts.Requests;
public sealed record GetCustomerOrderStatusRequest(string ContextId);

View File

@ -0,0 +1,3 @@
namespace Operations.Service.Contracts.Requests;
public sealed record GetPosTransactionSummaryRequest(string ContextId);

View File

@ -0,0 +1,3 @@
namespace Operations.Service.Contracts.Requests;
public sealed record GetRestaurantAdminConfigRequest(string ContextId);

View File

@ -0,0 +1,3 @@
namespace Operations.Service.Contracts.Requests;
public sealed record GetWaiterAssignmentsRequest(string ContextId);

View File

@ -0,0 +1,8 @@
namespace Operations.Service.Contracts.Requests;
public sealed record SetServiceWindowRequest(
string ContextId,
int DayOfWeek,
string OpenAt,
string CloseAt,
string UpdatedBy);

View File

@ -0,0 +1,7 @@
namespace Operations.Service.Contracts.Requests;
public sealed record SubmitRestaurantOrderRequest(
string ContextId,
string OrderId,
string TableId,
int ItemCount);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -4,8 +4,16 @@ using Operations.Service.Contracts.Requests;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IOperationsConfigReadPort, DefaultOperationsConfigReadPort>(); builder.Services.AddSingleton<IOperationsConfigReadPort, DefaultOperationsConfigReadPort>();
builder.Services.AddSingleton<IOperationsWorkflowPort, DefaultOperationsWorkflowPort>();
builder.Services.AddSingleton<IGetOperationsConfigUseCase, GetOperationsConfigUseCase>(); builder.Services.AddSingleton<IGetOperationsConfigUseCase, GetOperationsConfigUseCase>();
builder.Services.AddSingleton<IEvaluateOperationalDecisionUseCase, EvaluateOperationalDecisionUseCase>(); 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(); var app = builder.Build();
@ -23,6 +31,62 @@ app.MapPost("/internal/operations/decision", async (
return Results.Ok(await useCase.HandleAsync(request, ct)); 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("/health", () => Results.Ok(new { status = "ok", service = "operations-service" }));
app.MapGet("/healthz", () => Results.Ok(new { status = "ok", service = "operations-service" })); app.MapGet("/healthz", () => Results.Ok(new { status = "ok", service = "operations-service" }));

View File

@ -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>

View File

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