Compare commits

..

10 Commits

20 changed files with 270 additions and 161 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
**/bin/
**/obj/
.vs/
TestResults/
.git/
.repo-tasks/
.repo-context/
.tasks/
.agile/

63
.gitignore vendored
View File

@ -1,53 +1,24 @@
# AgileWebs local orchestration # Repository orchestration folders (local only)
.repo-tasks/
.repo-context/
.tasks/ .tasks/
.agile/ .agile/
# Build artifacts # .NET build outputs
**/[Bb]in/ **/bin/
**/[Oo]bj/ **/obj/
/**/out/
/**/artifacts/
# IDE and editor files
.vs/ .vs/
.idea/ TestResults/
.vscode/
*.suo
*.user
*.userosscache
*.sln.docstates
*.rsuser
*.swp
*.swo
# NuGet
*.nupkg
*.snupkg
**/packages/*
!**/packages/build/
# Test output
**/TestResults/ **/TestResults/
*.trx *.user
*.coverage *.suo
*.coveragexml *.rsuser
# Logs # IDE
*.log .idea/
# Runtime-local artifacts
logs/ logs/
*.log
# Local environment files .env.local
.env .env.*.local
.env.*
!.env.example
# Docker
.docker/
**/.docker/
*.pid
docker-compose.override.yml
docker-compose.*.override.yml
# OS files
.DS_Store
Thumbs.db

10
Directory.Build.props Normal file
View File

@ -0,0 +1,10 @@
<Project>
<PropertyGroup>
<Authors>AgileWebs</Authors>
<Company>AgileWebs</Company>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://gitea.dream-views.com/AgileWebs/furniture-service</RepositoryUrl>
<PackageProjectUrl>https://gitea.dream-views.com/AgileWebs/furniture-service</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
</PropertyGroup>
</Project>

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# syntax=docker/dockerfile:1.7
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/sdk:10.0
ARG RUNTIME_IMAGE=mcr.microsoft.com/dotnet/aspnet:10.0
FROM ${SDK_IMAGE} AS build
ARG NUGET_FEED_URL=https://gitea.dream-views.com/api/packages/AgileWebs/nuget/index.json
ARG NUGET_FEED_USERNAME=
ARG NUGET_FEED_TOKEN=
WORKDIR /src
COPY . .
RUN if [ -n "$NUGET_FEED_USERNAME" ] && [ -n "$NUGET_FEED_TOKEN" ]; then dotnet nuget add source "$NUGET_FEED_URL" --name gitea-org --username "$NUGET_FEED_USERNAME" --password "$NUGET_FEED_TOKEN" --store-password-in-clear-text --allow-insecure-connections --configfile /root/.nuget/NuGet/NuGet.Config; fi
RUN dotnet restore "src/Furniture.Service.Grpc/Furniture.Service.Grpc.csproj" --configfile /root/.nuget/NuGet/NuGet.Config
RUN dotnet publish "src/Furniture.Service.Grpc/Furniture.Service.Grpc.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
FROM ${RUNTIME_IMAGE} AS runtime
WORKDIR /app
ENV ASPNETCORE_ENVIRONMENT=Production
EXPOSE 8080
EXPOSE 8081
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "Furniture.Service.Grpc.dll"]

View File

@ -6,6 +6,7 @@
- Use cases depend on DAL-facing ports, not persistence implementations. - Use cases depend on DAL-facing ports, not persistence implementations.
- Use cases consume BuildingBlock capability contracts through adapter and port boundaries. - Use cases consume BuildingBlock capability contracts through adapter and port boundaries.
- Transport handlers map to use-case contracts and do not own orchestration logic. - Transport handlers map to use-case contracts and do not own orchestration logic.
- Demo/runtime aliases (`demo-context`, `FURN-*`, `PROD-FURN-*`) are normalized at use-case orchestration before port calls.
## Current Skeleton ## Current Skeleton

View File

@ -0,0 +1,27 @@
# Furniture Service Contract Package Baseline
## Feed
- Source: `https://gitea.dream-views.com/api/packages/AgileWebs/nuget/index.json`
- Authentication: Gitea login + token
- HTTP requirement: `allowInsecureConnections="true"` in `nuget.config`
## Published Baseline
| Package | Version | Published On |
| --- | --- | --- |
| Furniture.Service.Contracts | 0.2.0 | 2026-02-25 |
## Dependency Note
`Furniture.Service.Contracts` depends on:
- `Core.Blueprint.Common` version `0.2.0`
## Consumer Validation
Restore validation passed using:
- `TargetFramework`: `net10.0`
- `PackageReference`: `Furniture.Service.Contracts` `0.2.0`
- Restore flags: `--no-cache --force`

View File

@ -0,0 +1,14 @@
# Furniture Service Orchestration Boundary
## Purpose
Constrain furniture-service to orchestration responsibilities after domain extraction.
## Service Responsibilities
- Coordinate use-case flow
- Call domain abstractions for decisions
- Adapt transport contracts
- Depend on Domain and DAL boundaries without direct BuildingBlock ownership
## Prohibited Responsibilities
- Owning business decision rules
- Owning persistence decision concerns

View File

@ -0,0 +1,10 @@
# Domain Delegation Plan
## Delegation Model
- Use cases invoke furniture-domain abstractions for decision logic.
- Service adapters retain technical mapping responsibilities.
## Transition Steps
1. Replace in-service decision branches with domain calls.
2. Keep contract shapes stable at service boundary.
3. Validate orchestration-only responsibilities.

View File

@ -0,0 +1,10 @@
# Orchestration Regression Checks
## Checks
- Service no longer contains domain decision branches.
- Service still orchestrates required dependencies.
- Transport contract outputs remain stable.
## Evidence
- Updated architecture docs
- Migration mapping confirmation

View File

@ -0,0 +1,18 @@
# Feature Epics
## Repository
furniture-service
## Core Epics
- Epic 1: Expand domain-aligned capabilities for restaurant operations.
- Epic 2: Stabilize service contracts for containerized runtime integration.
- Epic 3: Improve observability and operational readiness for demo compose environments.
## Domain-Specific Candidate Features
- Order lifecycle consistency and state transitions.
- Kitchen queue and dispatch optimization hooks.
- Operations control-plane policies (flags, service windows, overrides).
- POS closeout and settlement summary alignment.
## Documentation Contract
Any code change in this repository must include docs updates in the same branch.

View File

@ -0,0 +1,40 @@
# Containerization Runbook
## Image Build
If the repo consumes internal packages from Gitea, pass feed credentials as build args.
```bash
docker build --build-arg NUGET_FEED_USERNAME=<gitea-login> --build-arg NUGET_FEED_TOKEN=<gitea-token> -t agilewebs/furniture-service:dev .
```
## Local Run
```bash
docker run --rm -p 8080:8080 --name furniture-service agilewebs/furniture-service:dev
```
## Health Probe
- Path: `/health`
- Fallback path: `/healthz`
- Port: `8080`
## Runtime Notes
- Exposes internal service endpoint set plus gRPC runtime surface.
## Health Endpoint Consistency
- Canonical probe: `/health`
- Compatibility probe: `/healthz`
- Container port: `8080`
## Demo Integration
- Participates in: **furniture** demo compose stack.
- Integration artifact path: `greenfield/demo/furniture/docker-compose.yml`
## Known Limitations
- 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.

View File

@ -1,56 +0,0 @@
using BuildingBlock.Catalog.Contracts.Conventions;
using BuildingBlock.Catalog.Contracts.Products;
using BuildingBlock.Catalog.Contracts.Responses;
using BuildingBlock.Inventory.Contracts.Conventions;
using BuildingBlock.Inventory.Contracts.Requests;
using BuildingBlock.Inventory.Contracts.Responses;
using Furniture.Service.Contracts.UseCases;
namespace Furniture.Service.Application.Adapters;
/// <summary>
/// Default adapter implementation for furniture service contract composition.
/// </summary>
public sealed class FurnitureAvailabilityContractAdapter : IFurnitureAvailabilityContractAdapter
{
private const string ContractVersion = "1.0.0";
/// <inheritdoc />
public InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request)
{
return new InventoryItemLookupRequest(
new InventoryContractEnvelope(ContractVersion, ResolveCorrelationId(request.CorrelationId)),
request.FurnitureId);
}
/// <inheritdoc />
public ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request)
{
return new ProductContract(
new CatalogContractEnvelope(ContractVersion, ResolveCorrelationId(request.CorrelationId)),
request.FurnitureId,
string.Empty);
}
/// <inheritdoc />
public GetFurnitureAvailabilityResponse ToServiceResponse(
GetFurnitureAvailabilityRequest request,
ProductContractResponse catalogResponse,
InventoryItemLookupResponse inventoryResponse)
{
return new GetFurnitureAvailabilityResponse(
request.FurnitureId,
catalogResponse.DisplayName,
inventoryResponse.QuantityAvailable);
}
private static string ResolveCorrelationId(string correlationId)
{
if (!string.IsNullOrWhiteSpace(correlationId))
{
return correlationId;
}
return $"corr-{Guid.NewGuid():N}";
}
}

View File

@ -1,39 +0,0 @@
using BuildingBlock.Catalog.Contracts.Products;
using BuildingBlock.Catalog.Contracts.Responses;
using BuildingBlock.Inventory.Contracts.Requests;
using BuildingBlock.Inventory.Contracts.Responses;
using Furniture.Service.Contracts.UseCases;
namespace Furniture.Service.Application.Adapters;
/// <summary>
/// Defines adapter boundary for furniture service contract composition.
/// </summary>
public interface IFurnitureAvailabilityContractAdapter
{
/// <summary>
/// Maps service request into an inventory capability request.
/// </summary>
/// <param name="request">Furniture availability request.</param>
/// <returns>Inventory lookup request.</returns>
InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request);
/// <summary>
/// Maps service request into a catalog capability request.
/// </summary>
/// <param name="request">Furniture availability request.</param>
/// <returns>Catalog product request.</returns>
ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request);
/// <summary>
/// Maps capability responses back into the service response contract.
/// </summary>
/// <param name="request">Furniture availability request.</param>
/// <param name="catalogResponse">Catalog product response.</param>
/// <param name="inventoryResponse">Inventory lookup response.</param>
/// <returns>Furniture availability response.</returns>
GetFurnitureAvailabilityResponse ToServiceResponse(
GetFurnitureAvailabilityRequest request,
ProductContractResponse catalogResponse,
InventoryItemLookupResponse inventoryResponse);
}

View File

@ -1,5 +1,6 @@
using Core.Blueprint.Common.DependencyInjection; using Core.Blueprint.Common.DependencyInjection;
using Furniture.DAL.DependencyInjection; using Furniture.DAL.DependencyInjection;
using Furniture.Domain.Decisions;
using Furniture.Service.Application.Adapters; using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports; using Furniture.Service.Application.Ports;
using Furniture.Service.Application.UseCases; using Furniture.Service.Application.UseCases;
@ -22,8 +23,8 @@ public static class FurnitureServiceRuntimeServiceCollectionExtensions
{ {
services.AddBlueprintRuntimeCore(); services.AddBlueprintRuntimeCore();
services.AddFurnitureDalRuntime(); services.AddFurnitureDalRuntime();
services.TryAddSingleton<IFurnitureAvailabilityDecisionService, FurnitureAvailabilityDecisionService>();
services.TryAddSingleton<IFurnitureAvailabilityContractAdapter, FurnitureAvailabilityContractAdapter>();
services.TryAddSingleton<IFurnitureAvailabilityGrpcContractAdapter, FurnitureAvailabilityGrpcContractAdapter>(); services.TryAddSingleton<IFurnitureAvailabilityGrpcContractAdapter, FurnitureAvailabilityGrpcContractAdapter>();
services.TryAddSingleton<IFurnitureAvailabilityReadPort, FurnitureAvailabilityReadPortDalAdapter>(); services.TryAddSingleton<IFurnitureAvailabilityReadPort, FurnitureAvailabilityReadPortDalAdapter>();
services.TryAddSingleton<ICatalogProductReadPort, CatalogProductReadPortDalAdapter>(); services.TryAddSingleton<ICatalogProductReadPort, CatalogProductReadPortDalAdapter>();

View File

@ -6,9 +6,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<ProjectReference Include="..\..\..\building-block-catalog\src\BuildingBlock.Catalog.Contracts\BuildingBlock.Catalog.Contracts.csproj" /> <PackageReference Include="Furniture.Domain" Version="0.2.0" />
<ProjectReference Include="..\..\..\building-block-inventory\src\BuildingBlock.Inventory.Contracts\BuildingBlock.Inventory.Contracts.csproj" /> <PackageReference Include="Furniture.DAL" Version="0.2.0" />
<ProjectReference Include="..\..\..\furniture-dal\src\Furniture.DAL\Furniture.DAL.csproj" />
<ProjectReference Include="..\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" /> <ProjectReference Include="..\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,6 +1,7 @@
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports; using Furniture.Service.Application.Ports;
using Furniture.Service.Contracts.UseCases; using Furniture.Service.Contracts.UseCases;
using Furniture.Domain.Contracts;
using Furniture.Domain.Decisions;
namespace Furniture.Service.Application.UseCases; namespace Furniture.Service.Application.UseCases;
@ -8,25 +9,67 @@ namespace Furniture.Service.Application.UseCases;
/// Default orchestration implementation for furniture availability lookup. /// Default orchestration implementation for furniture availability lookup.
/// </summary> /// </summary>
public sealed class GetFurnitureAvailabilityUseCase( public sealed class GetFurnitureAvailabilityUseCase(
IFurnitureAvailabilityContractAdapter contractAdapter, IFurnitureAvailabilityDecisionService decisionService,
ICatalogProductReadPort catalogReadPort, ICatalogProductReadPort catalogReadPort,
IFurnitureAvailabilityReadPort inventoryReadPort) IFurnitureAvailabilityReadPort inventoryReadPort)
: IGetFurnitureAvailabilityUseCase : IGetFurnitureAvailabilityUseCase
{ {
private static readonly IReadOnlyDictionary<string, (string InventoryItemCode, string CatalogProductId)> LookupAliases =
new Dictionary<string, (string InventoryItemCode, string CatalogProductId)>(StringComparer.OrdinalIgnoreCase)
{
["demo-context"] = ("FUR-001", "PRD-001"),
["FURN-001"] = ("FUR-001", "PRD-001"),
["FURN-002"] = ("FUR-002", "PRD-002"),
["FURN-003"] = ("FUR-003", "PRD-003"),
["PROD-FURN-001"] = ("FUR-001", "PRD-001"),
["PROD-FURN-002"] = ("FUR-002", "PRD-002"),
["PROD-FURN-003"] = ("FUR-003", "PRD-003"),
["FUR-001"] = ("FUR-001", "PRD-001"),
["FUR-002"] = ("FUR-002", "PRD-002"),
["FUR-003"] = ("FUR-003", "PRD-003"),
["PRD-001"] = ("FUR-001", "PRD-001"),
["PRD-002"] = ("FUR-002", "PRD-002"),
["PRD-003"] = ("FUR-003", "PRD-003")
};
/// <inheritdoc /> /// <inheritdoc />
public async Task<GetFurnitureAvailabilityResponse> HandleAsync(GetFurnitureAvailabilityRequest request) public async Task<GetFurnitureAvailabilityResponse> HandleAsync(GetFurnitureAvailabilityRequest request)
{ {
var catalogRequest = contractAdapter.ToCatalogRequest(request); var domainRequest = new FurnitureAvailabilityDecisionRequest(
var inventoryRequest = contractAdapter.ToInventoryRequest(request); request.FurnitureId,
request.CorrelationId);
var lookupKeys = ResolveLookupKeys(request.FurnitureId);
var catalogRequest = decisionService
.BuildCatalogRequest(domainRequest)
with { ProductId = lookupKeys.CatalogProductId };
var inventoryRequest = decisionService
.BuildInventoryRequest(domainRequest)
with { ItemCode = lookupKeys.InventoryItemCode };
var catalogTask = catalogReadPort.ReadProductAsync(catalogRequest); var catalogTask = catalogReadPort.ReadProductAsync(catalogRequest);
var inventoryTask = inventoryReadPort.ReadAvailabilityAsync(inventoryRequest); var inventoryTask = inventoryReadPort.ReadAvailabilityAsync(inventoryRequest);
await Task.WhenAll(catalogTask, inventoryTask); await Task.WhenAll(catalogTask, inventoryTask);
return contractAdapter.ToServiceResponse( var domainResponse = decisionService.ComposeResponse(
request, domainRequest,
await catalogTask, await catalogTask,
await inventoryTask); await inventoryTask);
return new GetFurnitureAvailabilityResponse(
domainResponse.FurnitureId,
domainResponse.DisplayName,
domainResponse.QuantityAvailable);
}
private static (string InventoryItemCode, string CatalogProductId) ResolveLookupKeys(string furnitureId)
{
if (LookupAliases.TryGetValue(furnitureId, out var mappedKeys))
{
return mappedKeys;
}
return (furnitureId, furnitureId);
} }
} }

View File

@ -5,6 +5,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" /> <PackageReference Include="Core.Blueprint.Common" Version="0.2.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,7 +1,16 @@
using Furniture.Service.Application.DependencyInjection; using Furniture.Service.Application.DependencyInjection;
using Furniture.Service.Grpc.Services; using Furniture.Service.Grpc.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var httpPort = builder.Configuration.GetValue("FurnitureService:HttpPort", 8080);
var grpcPort = builder.Configuration.GetValue("FurnitureService:GrpcPort", 8081);
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(httpPort, listenOptions => listenOptions.Protocols = HttpProtocols.Http1);
options.ListenAnyIP(grpcPort, listenOptions => listenOptions.Protocols = HttpProtocols.Http2);
});
builder.Services.AddGrpc(); builder.Services.AddGrpc();
builder.Services.AddHealthChecks(); builder.Services.AddHealthChecks();
@ -11,5 +20,6 @@ var app = builder.Build();
app.MapGrpcService<FurnitureRuntimeGrpcService>(); app.MapGrpcService<FurnitureRuntimeGrpcService>();
app.MapHealthChecks("/healthz"); app.MapHealthChecks("/healthz");
app.MapHealthChecks("/health");
app.Run(); app.Run();

View File

@ -4,10 +4,11 @@ using BuildingBlock.Catalog.Contracts.Responses;
using BuildingBlock.Inventory.Contracts.Conventions; using BuildingBlock.Inventory.Contracts.Conventions;
using BuildingBlock.Inventory.Contracts.Requests; using BuildingBlock.Inventory.Contracts.Requests;
using BuildingBlock.Inventory.Contracts.Responses; using BuildingBlock.Inventory.Contracts.Responses;
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports; using Furniture.Service.Application.Ports;
using Furniture.Service.Application.UseCases; using Furniture.Service.Application.UseCases;
using Furniture.Service.Contracts.UseCases; using Furniture.Service.Contracts.UseCases;
using Furniture.Domain.Contracts;
using Furniture.Domain.Decisions;
namespace Furniture.Service.Application.UnitTests; namespace Furniture.Service.Application.UnitTests;
@ -16,10 +17,10 @@ public class GetFurnitureAvailabilityUseCaseTests
[Fact] [Fact]
public async Task HandleAsync_WhenCalled_DelegatesToReadPort() public async Task HandleAsync_WhenCalled_DelegatesToReadPort()
{ {
var adapter = new FakeFurnitureAvailabilityContractAdapter(); var decisionService = new FakeFurnitureAvailabilityDecisionService();
var catalogPort = new FakeCatalogProductReadPort(); var catalogPort = new FakeCatalogProductReadPort();
var inventoryPort = new FakeFurnitureAvailabilityReadPort(); var inventoryPort = new FakeFurnitureAvailabilityReadPort();
var useCase = new GetFurnitureAvailabilityUseCase(adapter, catalogPort, inventoryPort); var useCase = new GetFurnitureAvailabilityUseCase(decisionService, catalogPort, inventoryPort);
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123")); var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123"));
@ -28,9 +29,9 @@ public class GetFurnitureAvailabilityUseCaseTests
Assert.Equal(10, response.QuantityAvailable); Assert.Equal(10, response.QuantityAvailable);
} }
private sealed class FakeFurnitureAvailabilityContractAdapter : IFurnitureAvailabilityContractAdapter private sealed class FakeFurnitureAvailabilityDecisionService : IFurnitureAvailabilityDecisionService
{ {
public ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request) public ProductContract BuildCatalogRequest(FurnitureAvailabilityDecisionRequest request)
{ {
return new ProductContract( return new ProductContract(
new CatalogContractEnvelope("1.0.0", request.CorrelationId), new CatalogContractEnvelope("1.0.0", request.CorrelationId),
@ -38,19 +39,19 @@ public class GetFurnitureAvailabilityUseCaseTests
string.Empty); string.Empty);
} }
public InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request) public InventoryItemLookupRequest BuildInventoryRequest(FurnitureAvailabilityDecisionRequest request)
{ {
return new InventoryItemLookupRequest( return new InventoryItemLookupRequest(
new InventoryContractEnvelope("1.0.0", request.CorrelationId), new InventoryContractEnvelope("1.0.0", request.CorrelationId),
request.FurnitureId); request.FurnitureId);
} }
public GetFurnitureAvailabilityResponse ToServiceResponse( public FurnitureAvailabilityDecisionResponse ComposeResponse(
GetFurnitureAvailabilityRequest request, FurnitureAvailabilityDecisionRequest request,
ProductContractResponse catalogResponse, ProductContractResponse catalogResponse,
InventoryItemLookupResponse inventoryResponse) InventoryItemLookupResponse inventoryResponse)
{ {
return new GetFurnitureAvailabilityResponse( return new FurnitureAvailabilityDecisionResponse(
request.FurnitureId, request.FurnitureId,
catalogResponse.DisplayName, catalogResponse.DisplayName,
inventoryResponse.QuantityAvailable); inventoryResponse.QuantityAvailable);

View File

@ -21,6 +21,23 @@ public class RuntimeWiringTests
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123")); var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123"));
Assert.Equal("FUR-001", response.FurnitureId); Assert.Equal("FUR-001", response.FurnitureId);
Assert.Equal("Contoso Lounge Chair", response.DisplayName);
Assert.Equal(8, response.QuantityAvailable);
}
[Fact]
public async Task AddFurnitureServiceRuntime_WhenUsingDemoFurnitureAlias_ResolvesMappedAvailability()
{
var services = new ServiceCollection();
services.AddFurnitureServiceRuntime();
using var provider = services.BuildServiceProvider();
var useCase = provider.GetRequiredService<IGetFurnitureAvailabilityUseCase>();
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FURN-001", "corr-123"));
Assert.Equal("FURN-001", response.FurnitureId);
Assert.Equal("Contoso Lounge Chair", response.DisplayName);
Assert.Equal(8, response.QuantityAvailable); Assert.Equal(8, response.QuantityAvailable);
} }