diff --git a/docs/application/use-case-boundaries.md b/docs/application/use-case-boundaries.md index 4d4828e..6f3d865 100644 --- a/docs/application/use-case-boundaries.md +++ b/docs/application/use-case-boundaries.md @@ -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. diff --git a/docs/architecture/protocol-selection-policy.md b/docs/architecture/protocol-selection-policy.md index 434eb85..2999f87 100644 --- a/docs/architecture/protocol-selection-policy.md +++ b/docs/architecture/protocol-selection-policy.md @@ -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. diff --git a/docs/architecture/service-contracts.puml b/docs/architecture/service-contracts.puml index ec950da..0f9d384 100644 --- a/docs/architecture/service-contracts.puml +++ b/docs/architecture/service-contracts.puml @@ -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 diff --git a/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityContractAdapter.cs b/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityContractAdapter.cs new file mode 100644 index 0000000..2f501e5 --- /dev/null +++ b/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityContractAdapter.cs @@ -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; + +/// +/// Defines adapter boundary for furniture service contract composition. +/// +public interface IFurnitureAvailabilityContractAdapter +{ + /// + /// Maps service request into an inventory capability request. + /// + /// Furniture availability request. + /// Inventory lookup request. + InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request); + + /// + /// Maps service request into a catalog capability request. + /// + /// Furniture availability request. + /// Catalog product request. + ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request); + + /// + /// Maps capability responses back into the service response contract. + /// + /// Furniture availability request. + /// Catalog product response. + /// Inventory lookup response. + /// Furniture availability response. + GetFurnitureAvailabilityResponse ToServiceResponse( + GetFurnitureAvailabilityRequest request, + ProductContractResponse catalogResponse, + InventoryItemLookupResponse inventoryResponse); +} diff --git a/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityGrpcContractAdapter.cs b/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityGrpcContractAdapter.cs new file mode 100644 index 0000000..7062948 --- /dev/null +++ b/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityGrpcContractAdapter.cs @@ -0,0 +1,24 @@ +using Furniture.Service.Application.Grpc; +using Furniture.Service.Contracts.UseCases; + +namespace Furniture.Service.Application.Adapters; + +/// +/// Defines adapter boundary for gRPC contract translation of furniture availability. +/// +public interface IFurnitureAvailabilityGrpcContractAdapter +{ + /// + /// Maps transport-neutral request into gRPC contract shape. + /// + /// Furniture availability request. + /// gRPC availability contract. + GetFurnitureAvailabilityGrpcContract ToGrpc(GetFurnitureAvailabilityRequest request); + + /// + /// Maps gRPC contract into transport-neutral request. + /// + /// gRPC availability contract. + /// Furniture availability request. + GetFurnitureAvailabilityRequest FromGrpc(GetFurnitureAvailabilityGrpcContract contract); +} diff --git a/src/Furniture.Service.Application/Furniture.Service.Application.csproj b/src/Furniture.Service.Application/Furniture.Service.Application.csproj index 9dbacfc..007938b 100644 --- a/src/Furniture.Service.Application/Furniture.Service.Application.csproj +++ b/src/Furniture.Service.Application/Furniture.Service.Application.csproj @@ -5,6 +5,8 @@ enable + + diff --git a/src/Furniture.Service.Application/Grpc/GetFurnitureAvailabilityGrpcContract.cs b/src/Furniture.Service.Application/Grpc/GetFurnitureAvailabilityGrpcContract.cs new file mode 100644 index 0000000..ae6d97f --- /dev/null +++ b/src/Furniture.Service.Application/Grpc/GetFurnitureAvailabilityGrpcContract.cs @@ -0,0 +1,8 @@ +namespace Furniture.Service.Application.Grpc; + +/// +/// Defines minimal gRPC contract shape for furniture availability adapter translation. +/// +/// Furniture aggregate identifier. +/// Cross-service correlation identifier. +public sealed record GetFurnitureAvailabilityGrpcContract(string FurnitureId, string CorrelationId); diff --git a/src/Furniture.Service.Application/Ports/ICatalogProductReadPort.cs b/src/Furniture.Service.Application/Ports/ICatalogProductReadPort.cs new file mode 100644 index 0000000..7e8e92f --- /dev/null +++ b/src/Furniture.Service.Application/Ports/ICatalogProductReadPort.cs @@ -0,0 +1,17 @@ +using BuildingBlock.Catalog.Contracts.Products; +using BuildingBlock.Catalog.Contracts.Responses; + +namespace Furniture.Service.Application.Ports; + +/// +/// Defines read boundary for catalog product contracts. +/// +public interface ICatalogProductReadPort +{ + /// + /// Reads product details from the catalog capability contract boundary. + /// + /// Catalog product request contract. + /// Catalog product response contract. + Task ReadProductAsync(ProductContract request); +} diff --git a/src/Furniture.Service.Application/Ports/IFurnitureAvailabilityReadPort.cs b/src/Furniture.Service.Application/Ports/IFurnitureAvailabilityReadPort.cs index 56c321d..44756c5 100644 --- a/src/Furniture.Service.Application/Ports/IFurnitureAvailabilityReadPort.cs +++ b/src/Furniture.Service.Application/Ports/IFurnitureAvailabilityReadPort.cs @@ -1,16 +1,17 @@ -using Furniture.Service.Contracts.UseCases; +using BuildingBlock.Inventory.Contracts.Requests; +using BuildingBlock.Inventory.Contracts.Responses; namespace Furniture.Service.Application.Ports; /// -/// Defines DAL-facing read port for furniture availability. +/// Defines read boundary for inventory availability contracts. /// public interface IFurnitureAvailabilityReadPort { /// - /// Retrieves current availability for a furniture aggregate. + /// Reads availability from the inventory capability contract boundary. /// - /// Furniture aggregate identifier. - /// Availability response contract. - Task GetAvailabilityAsync(string furnitureId); + /// Inventory lookup request contract. + /// Inventory lookup response contract. + Task ReadAvailabilityAsync(InventoryItemLookupRequest request); } diff --git a/src/Furniture.Service.Application/UseCases/GetFurnitureAvailabilityUseCase.cs b/src/Furniture.Service.Application/UseCases/GetFurnitureAvailabilityUseCase.cs index 88824b6..52acd3d 100644 --- a/src/Furniture.Service.Application/UseCases/GetFurnitureAvailabilityUseCase.cs +++ b/src/Furniture.Service.Application/UseCases/GetFurnitureAvailabilityUseCase.cs @@ -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; /// /// Default orchestration implementation for furniture availability lookup. /// -public sealed class GetFurnitureAvailabilityUseCase(IFurnitureAvailabilityReadPort readPort) +public sealed class GetFurnitureAvailabilityUseCase( + IFurnitureAvailabilityContractAdapter contractAdapter, + ICatalogProductReadPort catalogReadPort, + IFurnitureAvailabilityReadPort inventoryReadPort) : IGetFurnitureAvailabilityUseCase { /// - public Task HandleAsync(GetFurnitureAvailabilityRequest request) + public async Task 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); } } diff --git a/src/Furniture.Service.Contracts/Conventions/FurnitureServicePackageContract.cs b/src/Furniture.Service.Contracts/Conventions/FurnitureServicePackageContract.cs new file mode 100644 index 0000000..6486e2e --- /dev/null +++ b/src/Furniture.Service.Contracts/Conventions/FurnitureServicePackageContract.cs @@ -0,0 +1,15 @@ +using Core.Blueprint.Common.Contracts; + +namespace Furniture.Service.Contracts.Conventions; + +/// +/// Defines package descriptor metadata for furniture service contracts. +/// +public sealed class FurnitureServicePackageContract : IBlueprintPackageContract +{ + /// + public BlueprintPackageDescriptor Descriptor { get; } = new( + "Furniture.Service.Contracts", + PackageVersionPolicy.Minor, + ["Core.Blueprint.Common", "BuildingBlock.Inventory.Contracts", "BuildingBlock.Catalog.Contracts"]); +} diff --git a/src/Furniture.Service.Contracts/Furniture.Service.Contracts.csproj b/src/Furniture.Service.Contracts/Furniture.Service.Contracts.csproj index 6c3a887..04a4bbc 100644 --- a/src/Furniture.Service.Contracts/Furniture.Service.Contracts.csproj +++ b/src/Furniture.Service.Contracts/Furniture.Service.Contracts.csproj @@ -4,4 +4,7 @@ enable enable + + + diff --git a/src/Furniture.Service.Contracts/UseCases/GetFurnitureAvailabilityRequest.cs b/src/Furniture.Service.Contracts/UseCases/GetFurnitureAvailabilityRequest.cs index 044d73a..a1f46dd 100644 --- a/src/Furniture.Service.Contracts/UseCases/GetFurnitureAvailabilityRequest.cs +++ b/src/Furniture.Service.Contracts/UseCases/GetFurnitureAvailabilityRequest.cs @@ -4,4 +4,5 @@ namespace Furniture.Service.Contracts.UseCases; /// Request contract for furniture availability lookup. /// /// Furniture aggregate identifier. -public sealed record GetFurnitureAvailabilityRequest(string FurnitureId); +/// Cross-service correlation identifier. +public sealed record GetFurnitureAvailabilityRequest(string FurnitureId, string CorrelationId = ""); diff --git a/src/Furniture.Service.Contracts/UseCases/GetFurnitureAvailabilityResponse.cs b/src/Furniture.Service.Contracts/UseCases/GetFurnitureAvailabilityResponse.cs index abc8ca7..07fc4bd 100644 --- a/src/Furniture.Service.Contracts/UseCases/GetFurnitureAvailabilityResponse.cs +++ b/src/Furniture.Service.Contracts/UseCases/GetFurnitureAvailabilityResponse.cs @@ -4,5 +4,6 @@ namespace Furniture.Service.Contracts.UseCases; /// Response contract for furniture availability lookup. /// /// Furniture aggregate identifier. +/// Catalog display name for the requested furniture. /// Available quantity for the requested furniture. -public sealed record GetFurnitureAvailabilityResponse(string FurnitureId, int QuantityAvailable); +public sealed record GetFurnitureAvailabilityResponse(string FurnitureId, string DisplayName, int QuantityAvailable); diff --git a/tests/Furniture.Service.Application.UnitTests/ContractShapeTests.cs b/tests/Furniture.Service.Application.UnitTests/ContractShapeTests.cs new file mode 100644 index 0000000..66f41f0 --- /dev/null +++ b/tests/Furniture.Service.Application.UnitTests/ContractShapeTests.cs @@ -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); + } +} diff --git a/tests/Furniture.Service.Application.UnitTests/GetFurnitureAvailabilityUseCaseTests.cs b/tests/Furniture.Service.Application.UnitTests/GetFurnitureAvailabilityUseCaseTests.cs index 6ee97f4..3317df7 100644 --- a/tests/Furniture.Service.Application.UnitTests/GetFurnitureAvailabilityUseCaseTests.cs +++ b/tests/Furniture.Service.Application.UnitTests/GetFurnitureAvailabilityUseCaseTests.cs @@ -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 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 GetAvailabilityAsync(string furnitureId) + public Task 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)); } } }