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.Grpc/Operations.Service.Grpc.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Operations.Service.Application.UnitTests/Operations.Service.Application.UnitTests.csproj" />
</Folder>
</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.
- 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.

View File

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

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

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