chore(furniture-service): merge contract integration branch

This commit is contained in:
José René White Enciso 2026-02-22 04:57:36 -06:00
commit c94b6cc56f
16 changed files with 245 additions and 18 deletions

View File

@ -4,10 +4,14 @@
- Application use cases orchestrate domain workflows. - Application use cases orchestrate domain workflows.
- 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.
- 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.
## Current Skeleton ## Current Skeleton
- `IGetFurnitureAvailabilityUseCase`: orchestration boundary contract. - `IGetFurnitureAvailabilityUseCase`: orchestration boundary contract.
- `GetFurnitureAvailabilityUseCase`: orchestration implementation. - `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. - This service deployment uses one active protocol at a time.
- Internal default protocol is gRPC. - Internal default protocol is gRPC.
- Multi-protocol exposure is not allowed in a single deployment. - Multi-protocol exposure is not allowed in a single deployment.
- gRPC message translation is isolated through adapter interfaces.
## Boundary Placement ## Boundary Placement
- Protocol adaptation stays at BFF boundaries. - Protocol adaptation stays at BFF boundaries.
- Service orchestration remains transport-neutral at application contracts. - 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 skinparam packageStyle rectangle
package "furniture-service" { package "furniture-service" {
interface IFurnitureAvailabilityContractAdapter
interface IGetFurnitureAvailabilityUseCase interface IGetFurnitureAvailabilityUseCase
class GetFurnitureAvailabilityUseCase class GetFurnitureAvailabilityUseCase
interface ICatalogProductReadPort
interface IFurnitureAvailabilityReadPort interface IFurnitureAvailabilityReadPort
interface IFurnitureAvailabilityGrpcContractAdapter
class GetFurnitureAvailabilityGrpcContract
class GetFurnitureAvailabilityRequest class GetFurnitureAvailabilityRequest
class GetFurnitureAvailabilityResponse class GetFurnitureAvailabilityResponse
GetFurnitureAvailabilityUseCase ..|> IGetFurnitureAvailabilityUseCase GetFurnitureAvailabilityUseCase ..|> IGetFurnitureAvailabilityUseCase
GetFurnitureAvailabilityUseCase --> IFurnitureAvailabilityContractAdapter
GetFurnitureAvailabilityUseCase --> ICatalogProductReadPort
GetFurnitureAvailabilityUseCase --> IFurnitureAvailabilityReadPort GetFurnitureAvailabilityUseCase --> IFurnitureAvailabilityReadPort
IFurnitureAvailabilityGrpcContractAdapter --> GetFurnitureAvailabilityGrpcContract
IFurnitureAvailabilityGrpcContractAdapter --> GetFurnitureAvailabilityRequest
IGetFurnitureAvailabilityUseCase --> GetFurnitureAvailabilityRequest IGetFurnitureAvailabilityUseCase --> GetFurnitureAvailabilityRequest
IGetFurnitureAvailabilityUseCase --> GetFurnitureAvailabilityResponse IGetFurnitureAvailabilityUseCase --> GetFurnitureAvailabilityResponse
} }
package "furniture-bff" as FurnitureBff package "furniture-bff" as FurnitureBff
package "furniture-dal" as FurnitureDal package "furniture-dal" as FurnitureDal
package "building-block-catalog" as CatalogContracts
package "building-block-inventory" as InventoryContracts
FurnitureBff --> IGetFurnitureAvailabilityUseCase FurnitureBff --> IGetFurnitureAvailabilityUseCase
GetFurnitureAvailabilityUseCase --> IFurnitureAvailabilityReadPort ICatalogProductReadPort ..> CatalogContracts
IFurnitureAvailabilityReadPort ..> FurnitureDal IFurnitureAvailabilityReadPort ..> FurnitureDal
IFurnitureAvailabilityReadPort ..> InventoryContracts
@enduml @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> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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" /> <ProjectReference Include="..\Furniture.Service.Contracts\Furniture.Service.Contracts.csproj" />
</ItemGroup> </ItemGroup>
</Project> </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; namespace Furniture.Service.Application.Ports;
/// <summary> /// <summary>
/// Defines DAL-facing read port for furniture availability. /// Defines read boundary for inventory availability contracts.
/// </summary> /// </summary>
public interface IFurnitureAvailabilityReadPort public interface IFurnitureAvailabilityReadPort
{ {
/// <summary> /// <summary>
/// Retrieves current availability for a furniture aggregate. /// Reads availability from the inventory capability contract boundary.
/// </summary> /// </summary>
/// <param name="furnitureId">Furniture aggregate identifier.</param> /// <param name="request">Inventory lookup request contract.</param>
/// <returns>Availability response contract.</returns> /// <returns>Inventory lookup response contract.</returns>
Task<GetFurnitureAvailabilityResponse> GetAvailabilityAsync(string furnitureId); Task<InventoryItemLookupResponse> ReadAvailabilityAsync(InventoryItemLookupRequest request);
} }

View File

@ -1,3 +1,4 @@
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;
@ -6,12 +7,26 @@ namespace Furniture.Service.Application.UseCases;
/// <summary> /// <summary>
/// Default orchestration implementation for furniture availability lookup. /// Default orchestration implementation for furniture availability lookup.
/// </summary> /// </summary>
public sealed class GetFurnitureAvailabilityUseCase(IFurnitureAvailabilityReadPort readPort) public sealed class GetFurnitureAvailabilityUseCase(
IFurnitureAvailabilityContractAdapter contractAdapter,
ICatalogProductReadPort catalogReadPort,
IFurnitureAvailabilityReadPort inventoryReadPort)
: IGetFurnitureAvailabilityUseCase : IGetFurnitureAvailabilityUseCase
{ {
/// <inheritdoc /> /// <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> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -4,4 +4,5 @@ namespace Furniture.Service.Contracts.UseCases;
/// Request contract for furniture availability lookup. /// Request contract for furniture availability lookup.
/// </summary> /// </summary>
/// <param name="FurnitureId">Furniture aggregate identifier.</param> /// <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. /// Response contract for furniture availability lookup.
/// </summary> /// </summary>
/// <param name="FurnitureId">Furniture aggregate identifier.</param> /// <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> /// <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.Ports;
using Furniture.Service.Application.UseCases; using Furniture.Service.Application.UseCases;
using Furniture.Service.Contracts.UseCases; using Furniture.Service.Contracts.UseCases;
@ -9,20 +16,68 @@ public class GetFurnitureAvailabilityUseCaseTests
[Fact] [Fact]
public async Task HandleAsync_WhenCalled_DelegatesToReadPort() public async Task HandleAsync_WhenCalled_DelegatesToReadPort()
{ {
var port = new FakeFurnitureAvailabilityReadPort(); var adapter = new FakeFurnitureAvailabilityContractAdapter();
var useCase = new GetFurnitureAvailabilityUseCase(port); 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("FUR-001", response.FurnitureId);
Assert.Equal("Chair", response.DisplayName);
Assert.Equal(10, response.QuantityAvailable); 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 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));
} }
} }
} }