Compare commits

..

No commits in common. "bff24f312466d8f11d5f408278916002a4ab6dd5" and "464730658350ca0af383cd800aaba6a2d024cfa8" have entirely different histories.

20 changed files with 161 additions and 270 deletions

View File

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

63
.gitignore vendored
View File

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

View File

@ -1,10 +0,0 @@
<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>

View File

@ -1,23 +0,0 @@
# 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,7 +6,6 @@
- Use cases depend on DAL-facing ports, not persistence implementations.
- Use cases consume BuildingBlock capability contracts through adapter and port boundaries.
- 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

View File

@ -1,27 +0,0 @@
# 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

@ -1,14 +0,0 @@
# 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

@ -1,10 +0,0 @@
# 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

@ -1,10 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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

@ -1,40 +0,0 @@
# 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

@ -0,0 +1,56 @@
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

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

View File

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

View File

@ -1,7 +1,6 @@
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports;
using Furniture.Service.Contracts.UseCases;
using Furniture.Domain.Contracts;
using Furniture.Domain.Decisions;
namespace Furniture.Service.Application.UseCases;
@ -9,67 +8,25 @@ namespace Furniture.Service.Application.UseCases;
/// Default orchestration implementation for furniture availability lookup.
/// </summary>
public sealed class GetFurnitureAvailabilityUseCase(
IFurnitureAvailabilityDecisionService decisionService,
IFurnitureAvailabilityContractAdapter contractAdapter,
ICatalogProductReadPort catalogReadPort,
IFurnitureAvailabilityReadPort inventoryReadPort)
: 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 />
public async Task<GetFurnitureAvailabilityResponse> HandleAsync(GetFurnitureAvailabilityRequest request)
{
var domainRequest = new FurnitureAvailabilityDecisionRequest(
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 catalogRequest = contractAdapter.ToCatalogRequest(request);
var inventoryRequest = contractAdapter.ToInventoryRequest(request);
var catalogTask = catalogReadPort.ReadProductAsync(catalogRequest);
var inventoryTask = inventoryReadPort.ReadAvailabilityAsync(inventoryRequest);
await Task.WhenAll(catalogTask, inventoryTask);
var domainResponse = decisionService.ComposeResponse(
domainRequest,
return contractAdapter.ToServiceResponse(
request,
await catalogTask,
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>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Core.Blueprint.Common" Version="0.2.0" />
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -1,16 +1,7 @@
using Furniture.Service.Application.DependencyInjection;
using Furniture.Service.Grpc.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
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.AddHealthChecks();
@ -20,6 +11,5 @@ var app = builder.Build();
app.MapGrpcService<FurnitureRuntimeGrpcService>();
app.MapHealthChecks("/healthz");
app.MapHealthChecks("/health");
app.Run();

View File

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

View File

@ -21,23 +21,6 @@ public class RuntimeWiringTests
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123"));
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);
}