feat(furniture-service): integrate inventory and catalog contract adapters

This commit is contained in:
José René White Enciso 2026-02-22 03:58:36 -06:00
parent 04a543257d
commit 8d06ce9f35
16 changed files with 245 additions and 18 deletions

View File

@ -4,10 +4,14 @@
- Application use cases orchestrate domain workflows.
- 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.
## Current Skeleton
- `IGetFurnitureAvailabilityUseCase`: orchestration boundary contract.
- `GetFurnitureAvailabilityUseCase`: orchestration implementation.
- `IFurnitureAvailabilityReadPort`: DAL-facing port.
- `IFurnitureAvailabilityContractAdapter`: maps service contracts to inventory and catalog contracts.
- `ICatalogProductReadPort`: catalog capability read boundary.
- `IFurnitureAvailabilityReadPort`: inventory capability read boundary.
- `IFurnitureAvailabilityGrpcContractAdapter`: gRPC translation boundary for service contracts.

View File

@ -5,8 +5,10 @@
- This service deployment uses one active protocol at a time.
- Internal default protocol is gRPC.
- Multi-protocol exposure is not allowed in a single deployment.
- gRPC message translation is isolated through adapter interfaces.
## Boundary Placement
- Protocol adaptation stays at BFF boundaries.
- Service orchestration remains transport-neutral at application contracts.
- Service application composes catalog and inventory capability contracts via read ports.

View File

@ -2,22 +2,33 @@
skinparam packageStyle rectangle
package "furniture-service" {
interface IFurnitureAvailabilityContractAdapter
interface IGetFurnitureAvailabilityUseCase
class GetFurnitureAvailabilityUseCase
interface ICatalogProductReadPort
interface IFurnitureAvailabilityReadPort
interface IFurnitureAvailabilityGrpcContractAdapter
class GetFurnitureAvailabilityGrpcContract
class GetFurnitureAvailabilityRequest
class GetFurnitureAvailabilityResponse
GetFurnitureAvailabilityUseCase ..|> IGetFurnitureAvailabilityUseCase
GetFurnitureAvailabilityUseCase --> IFurnitureAvailabilityContractAdapter
GetFurnitureAvailabilityUseCase --> ICatalogProductReadPort
GetFurnitureAvailabilityUseCase --> IFurnitureAvailabilityReadPort
IFurnitureAvailabilityGrpcContractAdapter --> GetFurnitureAvailabilityGrpcContract
IFurnitureAvailabilityGrpcContractAdapter --> GetFurnitureAvailabilityRequest
IGetFurnitureAvailabilityUseCase --> GetFurnitureAvailabilityRequest
IGetFurnitureAvailabilityUseCase --> GetFurnitureAvailabilityResponse
}
package "furniture-bff" as FurnitureBff
package "furniture-dal" as FurnitureDal
package "building-block-catalog" as CatalogContracts
package "building-block-inventory" as InventoryContracts
FurnitureBff --> IGetFurnitureAvailabilityUseCase
GetFurnitureAvailabilityUseCase --> IFurnitureAvailabilityReadPort
ICatalogProductReadPort ..> CatalogContracts
IFurnitureAvailabilityReadPort ..> FurnitureDal
IFurnitureAvailabilityReadPort ..> InventoryContracts
@enduml

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

@ -0,0 +1,24 @@
using Furniture.Service.Application.Grpc;
using Furniture.Service.Contracts.UseCases;
namespace Furniture.Service.Application.Adapters;
/// <summary>
/// Defines adapter boundary for gRPC contract translation of furniture availability.
/// </summary>
public interface IFurnitureAvailabilityGrpcContractAdapter
{
/// <summary>
/// Maps transport-neutral request into gRPC contract shape.
/// </summary>
/// <param name="request">Furniture availability request.</param>
/// <returns>gRPC availability contract.</returns>
GetFurnitureAvailabilityGrpcContract ToGrpc(GetFurnitureAvailabilityRequest request);
/// <summary>
/// Maps gRPC contract into transport-neutral request.
/// </summary>
/// <param name="contract">gRPC availability contract.</param>
/// <returns>Furniture availability request.</returns>
GetFurnitureAvailabilityRequest FromGrpc(GetFurnitureAvailabilityGrpcContract contract);
}

View File

@ -5,6 +5,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<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.Service.Contracts\Furniture.Service.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
namespace Furniture.Service.Application.Grpc;
/// <summary>
/// Defines minimal gRPC contract shape for furniture availability adapter translation.
/// </summary>
/// <param name="FurnitureId">Furniture aggregate identifier.</param>
/// <param name="CorrelationId">Cross-service correlation identifier.</param>
public sealed record GetFurnitureAvailabilityGrpcContract(string FurnitureId, string CorrelationId);

View File

@ -0,0 +1,17 @@
using BuildingBlock.Catalog.Contracts.Products;
using BuildingBlock.Catalog.Contracts.Responses;
namespace Furniture.Service.Application.Ports;
/// <summary>
/// Defines read boundary for catalog product contracts.
/// </summary>
public interface ICatalogProductReadPort
{
/// <summary>
/// Reads product details from the catalog capability contract boundary.
/// </summary>
/// <param name="request">Catalog product request contract.</param>
/// <returns>Catalog product response contract.</returns>
Task<ProductContractResponse> ReadProductAsync(ProductContract request);
}

View File

@ -1,16 +1,17 @@
using Furniture.Service.Contracts.UseCases;
using BuildingBlock.Inventory.Contracts.Requests;
using BuildingBlock.Inventory.Contracts.Responses;
namespace Furniture.Service.Application.Ports;
/// <summary>
/// Defines DAL-facing read port for furniture availability.
/// Defines read boundary for inventory availability contracts.
/// </summary>
public interface IFurnitureAvailabilityReadPort
{
/// <summary>
/// Retrieves current availability for a furniture aggregate.
/// Reads availability from the inventory capability contract boundary.
/// </summary>
/// <param name="furnitureId">Furniture aggregate identifier.</param>
/// <returns>Availability response contract.</returns>
Task<GetFurnitureAvailabilityResponse> GetAvailabilityAsync(string furnitureId);
/// <param name="request">Inventory lookup request contract.</param>
/// <returns>Inventory lookup response contract.</returns>
Task<InventoryItemLookupResponse> ReadAvailabilityAsync(InventoryItemLookupRequest request);
}

View File

@ -1,3 +1,4 @@
using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports;
using Furniture.Service.Contracts.UseCases;
@ -6,12 +7,26 @@ namespace Furniture.Service.Application.UseCases;
/// <summary>
/// Default orchestration implementation for furniture availability lookup.
/// </summary>
public sealed class GetFurnitureAvailabilityUseCase(IFurnitureAvailabilityReadPort readPort)
public sealed class GetFurnitureAvailabilityUseCase(
IFurnitureAvailabilityContractAdapter contractAdapter,
ICatalogProductReadPort catalogReadPort,
IFurnitureAvailabilityReadPort inventoryReadPort)
: IGetFurnitureAvailabilityUseCase
{
/// <inheritdoc />
public Task<GetFurnitureAvailabilityResponse> HandleAsync(GetFurnitureAvailabilityRequest request)
public async Task<GetFurnitureAvailabilityResponse> HandleAsync(GetFurnitureAvailabilityRequest request)
{
return readPort.GetAvailabilityAsync(request.FurnitureId);
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);
return contractAdapter.ToServiceResponse(
request,
await catalogTask,
await inventoryTask);
}
}

View File

@ -0,0 +1,15 @@
using Core.Blueprint.Common.Contracts;
namespace Furniture.Service.Contracts.Conventions;
/// <summary>
/// Defines package descriptor metadata for furniture service contracts.
/// </summary>
public sealed class FurnitureServicePackageContract : IBlueprintPackageContract
{
/// <inheritdoc />
public BlueprintPackageDescriptor Descriptor { get; } = new(
"Furniture.Service.Contracts",
PackageVersionPolicy.Minor,
["Core.Blueprint.Common", "BuildingBlock.Inventory.Contracts", "BuildingBlock.Catalog.Contracts"]);
}

View File

@ -4,4 +4,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -4,4 +4,5 @@ namespace Furniture.Service.Contracts.UseCases;
/// Request contract for furniture availability lookup.
/// </summary>
/// <param name="FurnitureId">Furniture aggregate identifier.</param>
public sealed record GetFurnitureAvailabilityRequest(string FurnitureId);
/// <param name="CorrelationId">Cross-service correlation identifier.</param>
public sealed record GetFurnitureAvailabilityRequest(string FurnitureId, string CorrelationId = "");

View File

@ -4,5 +4,6 @@ namespace Furniture.Service.Contracts.UseCases;
/// Response contract for furniture availability lookup.
/// </summary>
/// <param name="FurnitureId">Furniture aggregate identifier.</param>
/// <param name="DisplayName">Catalog display name for the requested furniture.</param>
/// <param name="QuantityAvailable">Available quantity for the requested furniture.</param>
public sealed record GetFurnitureAvailabilityResponse(string FurnitureId, int QuantityAvailable);
public sealed record GetFurnitureAvailabilityResponse(string FurnitureId, string DisplayName, int QuantityAvailable);

View File

@ -0,0 +1,29 @@
using Core.Blueprint.Common.Contracts;
using Furniture.Service.Contracts.Conventions;
using Furniture.Service.Contracts.UseCases;
namespace Furniture.Service.Application.UnitTests;
public class ContractShapeTests
{
[Fact]
public void GetFurnitureAvailabilityRequest_WhenCreated_StoresCorrelation()
{
var request = new GetFurnitureAvailabilityRequest("FUR-001", "corr-123");
Assert.Equal("FUR-001", request.FurnitureId);
Assert.Equal("corr-123", request.CorrelationId);
}
[Fact]
public void FurnitureServicePackageContract_WhenCreated_UsesBlueprintDescriptorContract()
{
IBlueprintPackageContract contract = new FurnitureServicePackageContract();
Assert.Equal("Furniture.Service.Contracts", contract.Descriptor.PackageId);
Assert.Equal(PackageVersionPolicy.Minor, contract.Descriptor.VersionPolicy);
Assert.Contains("Core.Blueprint.Common", contract.Descriptor.DependencyPackageIds);
Assert.Contains("BuildingBlock.Inventory.Contracts", contract.Descriptor.DependencyPackageIds);
Assert.Contains("BuildingBlock.Catalog.Contracts", contract.Descriptor.DependencyPackageIds);
}
}

View File

@ -1,3 +1,10 @@
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.Application.Adapters;
using Furniture.Service.Application.Ports;
using Furniture.Service.Application.UseCases;
using Furniture.Service.Contracts.UseCases;
@ -9,20 +16,68 @@ public class GetFurnitureAvailabilityUseCaseTests
[Fact]
public async Task HandleAsync_WhenCalled_DelegatesToReadPort()
{
var port = new FakeFurnitureAvailabilityReadPort();
var useCase = new GetFurnitureAvailabilityUseCase(port);
var adapter = new FakeFurnitureAvailabilityContractAdapter();
var catalogPort = new FakeCatalogProductReadPort();
var inventoryPort = new FakeFurnitureAvailabilityReadPort();
var useCase = new GetFurnitureAvailabilityUseCase(adapter, catalogPort, inventoryPort);
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001"));
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123"));
Assert.Equal("FUR-001", response.FurnitureId);
Assert.Equal("Chair", response.DisplayName);
Assert.Equal(10, response.QuantityAvailable);
}
private sealed class FakeFurnitureAvailabilityContractAdapter : IFurnitureAvailabilityContractAdapter
{
public ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request)
{
return new ProductContract(
new CatalogContractEnvelope("1.0.0", request.CorrelationId),
request.FurnitureId,
string.Empty);
}
public InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request)
{
return new InventoryItemLookupRequest(
new InventoryContractEnvelope("1.0.0", request.CorrelationId),
request.FurnitureId);
}
public GetFurnitureAvailabilityResponse ToServiceResponse(
GetFurnitureAvailabilityRequest request,
ProductContractResponse catalogResponse,
InventoryItemLookupResponse inventoryResponse)
{
return new GetFurnitureAvailabilityResponse(
request.FurnitureId,
catalogResponse.DisplayName,
inventoryResponse.QuantityAvailable);
}
}
private sealed class FakeCatalogProductReadPort : ICatalogProductReadPort
{
public Task<ProductContractResponse> ReadProductAsync(ProductContract request)
{
return Task.FromResult(
new ProductContractResponse(
new CatalogContractEnvelope("1.0.0", request.Envelope.CorrelationId),
request.ProductId,
"Chair"));
}
}
private sealed class FakeFurnitureAvailabilityReadPort : IFurnitureAvailabilityReadPort
{
public Task<GetFurnitureAvailabilityResponse> GetAvailabilityAsync(string furnitureId)
public Task<InventoryItemLookupResponse> ReadAvailabilityAsync(InventoryItemLookupRequest request)
{
return Task.FromResult(new GetFurnitureAvailabilityResponse(furnitureId, 10));
return Task.FromResult(
new InventoryItemLookupResponse(
new InventoryContractEnvelope("1.0.0", request.Envelope.CorrelationId),
request.ItemCode,
10));
}
}
}