diff --git a/.gitignore b/.gitignore index 31c7257..89d521f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ .tasks/ .agile/ +bin/ +obj/ +TestResults/ +.vs/ +*.user +*.suo diff --git a/Furniture.Domain.slnx b/Furniture.Domain.slnx new file mode 100644 index 0000000..aa254fa --- /dev/null +++ b/Furniture.Domain.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/migration/behavior-invariants.md b/docs/migration/behavior-invariants.md index 0233310..ba3889c 100644 --- a/docs/migration/behavior-invariants.md +++ b/docs/migration/behavior-invariants.md @@ -4,6 +4,8 @@ - Availability decision outcome remains unchanged for equivalent inputs. - Correlation propagation behavior remains unchanged. - Transport contracts stay stable at service boundary. +- Missing display names map to `Unknown Furniture`. +- Negative inventory quantities are clamped to `0`. ## Validation Approach - Compare pre/post extraction contract examples. diff --git a/src/Furniture.Domain/Contracts/FurnitureAvailabilityDecisionRequest.cs b/src/Furniture.Domain/Contracts/FurnitureAvailabilityDecisionRequest.cs new file mode 100644 index 0000000..c8ea067 --- /dev/null +++ b/src/Furniture.Domain/Contracts/FurnitureAvailabilityDecisionRequest.cs @@ -0,0 +1,8 @@ +namespace Furniture.Domain.Contracts; + +/// +/// Domain input for furniture availability decisions. +/// +/// Furniture identifier. +/// Correlation identifier. +public sealed record FurnitureAvailabilityDecisionRequest(string FurnitureId, string CorrelationId); diff --git a/src/Furniture.Domain/Contracts/FurnitureAvailabilityDecisionResponse.cs b/src/Furniture.Domain/Contracts/FurnitureAvailabilityDecisionResponse.cs new file mode 100644 index 0000000..e0e8a50 --- /dev/null +++ b/src/Furniture.Domain/Contracts/FurnitureAvailabilityDecisionResponse.cs @@ -0,0 +1,12 @@ +namespace Furniture.Domain.Contracts; + +/// +/// Domain output for furniture availability decisions. +/// +/// Furniture identifier. +/// Furniture display name. +/// Quantity available. +public sealed record FurnitureAvailabilityDecisionResponse( + string FurnitureId, + string DisplayName, + int QuantityAvailable); diff --git a/src/Furniture.Domain/Conventions/FurnitureDomainPackageContract.cs b/src/Furniture.Domain/Conventions/FurnitureDomainPackageContract.cs new file mode 100644 index 0000000..0f19aec --- /dev/null +++ b/src/Furniture.Domain/Conventions/FurnitureDomainPackageContract.cs @@ -0,0 +1,8 @@ +namespace Furniture.Domain.Conventions; + +/// +/// Marker type for furniture-domain package discovery. +/// +public sealed class FurnitureDomainPackageContract +{ +} diff --git a/src/Furniture.Domain/Decisions/FurnitureAvailabilityDecisionService.cs b/src/Furniture.Domain/Decisions/FurnitureAvailabilityDecisionService.cs new file mode 100644 index 0000000..c9a9a24 --- /dev/null +++ b/src/Furniture.Domain/Decisions/FurnitureAvailabilityDecisionService.cs @@ -0,0 +1,63 @@ +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.Domain.Contracts; + +namespace Furniture.Domain.Decisions; + +/// +/// Default domain implementation for furniture availability composition decisions. +/// +public sealed class FurnitureAvailabilityDecisionService : IFurnitureAvailabilityDecisionService +{ + private const string ContractVersion = "1.0.0"; + + /// + public InventoryItemLookupRequest BuildInventoryRequest(FurnitureAvailabilityDecisionRequest request) + { + return new InventoryItemLookupRequest( + new InventoryContractEnvelope(ContractVersion, ResolveCorrelationId(request.CorrelationId)), + request.FurnitureId); + } + + /// + public ProductContract BuildCatalogRequest(FurnitureAvailabilityDecisionRequest request) + { + return new ProductContract( + new CatalogContractEnvelope(ContractVersion, ResolveCorrelationId(request.CorrelationId)), + request.FurnitureId, + string.Empty); + } + + /// + public FurnitureAvailabilityDecisionResponse ComposeResponse( + FurnitureAvailabilityDecisionRequest request, + ProductContractResponse catalogResponse, + InventoryItemLookupResponse inventoryResponse) + { + var displayName = string.IsNullOrWhiteSpace(catalogResponse.DisplayName) + ? "Unknown Furniture" + : catalogResponse.DisplayName; + var quantityAvailable = inventoryResponse.QuantityAvailable < 0 + ? 0 + : inventoryResponse.QuantityAvailable; + + return new FurnitureAvailabilityDecisionResponse( + request.FurnitureId, + displayName, + quantityAvailable); + } + + private static string ResolveCorrelationId(string correlationId) + { + if (!string.IsNullOrWhiteSpace(correlationId)) + { + return correlationId; + } + + return $"corr-{Guid.NewGuid():N}"; + } +} diff --git a/src/Furniture.Domain/Decisions/IFurnitureAvailabilityDecisionService.cs b/src/Furniture.Domain/Decisions/IFurnitureAvailabilityDecisionService.cs new file mode 100644 index 0000000..5a2e6c2 --- /dev/null +++ b/src/Furniture.Domain/Decisions/IFurnitureAvailabilityDecisionService.cs @@ -0,0 +1,31 @@ +using BuildingBlock.Catalog.Contracts.Products; +using BuildingBlock.Catalog.Contracts.Responses; +using BuildingBlock.Inventory.Contracts.Requests; +using BuildingBlock.Inventory.Contracts.Responses; +using Furniture.Domain.Contracts; + +namespace Furniture.Domain.Decisions; + +/// +/// Defines domain decision boundary for furniture availability composition. +/// +public interface IFurnitureAvailabilityDecisionService +{ + /// + /// Creates inventory capability request from furniture availability input. + /// + InventoryItemLookupRequest BuildInventoryRequest(FurnitureAvailabilityDecisionRequest request); + + /// + /// Creates catalog capability request from furniture availability input. + /// + ProductContract BuildCatalogRequest(FurnitureAvailabilityDecisionRequest request); + + /// + /// Composes final furniture availability response from capability responses. + /// + FurnitureAvailabilityDecisionResponse ComposeResponse( + FurnitureAvailabilityDecisionRequest request, + ProductContractResponse catalogResponse, + InventoryItemLookupResponse inventoryResponse); +} diff --git a/src/Furniture.Domain/Furniture.Domain.csproj b/src/Furniture.Domain/Furniture.Domain.csproj new file mode 100644 index 0000000..911464b --- /dev/null +++ b/src/Furniture.Domain/Furniture.Domain.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/tests/Furniture.Domain.UnitTests/Furniture.Domain.UnitTests.csproj b/tests/Furniture.Domain.UnitTests/Furniture.Domain.UnitTests.csproj new file mode 100644 index 0000000..b24e147 --- /dev/null +++ b/tests/Furniture.Domain.UnitTests/Furniture.Domain.UnitTests.csproj @@ -0,0 +1,20 @@ + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + diff --git a/tests/Furniture.Domain.UnitTests/FurnitureAvailabilityDecisionServiceTests.cs b/tests/Furniture.Domain.UnitTests/FurnitureAvailabilityDecisionServiceTests.cs new file mode 100644 index 0000000..3dbc098 --- /dev/null +++ b/tests/Furniture.Domain.UnitTests/FurnitureAvailabilityDecisionServiceTests.cs @@ -0,0 +1,92 @@ +using BuildingBlock.Catalog.Contracts.Conventions; +using BuildingBlock.Catalog.Contracts.Responses; +using BuildingBlock.Inventory.Contracts.Conventions; +using BuildingBlock.Inventory.Contracts.Responses; +using Furniture.Domain.Contracts; +using Furniture.Domain.Decisions; + +namespace Furniture.Domain.UnitTests; + +public class FurnitureAvailabilityDecisionServiceTests +{ + [Fact] + public void BuildCatalogRequest_WhenCalled_UsesFurnitureIdAsProductId() + { + var service = new FurnitureAvailabilityDecisionService(); + + var request = service.BuildCatalogRequest(new FurnitureAvailabilityDecisionRequest("FUR-001", "corr-001")); + + Assert.Equal("FUR-001", request.ProductId); + Assert.Equal("corr-001", request.Envelope.CorrelationId); + } + + [Fact] + public void BuildInventoryRequest_WhenCorrelationMissing_GeneratesCorrelation() + { + var service = new FurnitureAvailabilityDecisionService(); + + var request = service.BuildInventoryRequest(new FurnitureAvailabilityDecisionRequest("FUR-001", string.Empty)); + + Assert.Equal("FUR-001", request.ItemCode); + Assert.NotEmpty(request.Envelope.CorrelationId); + } + + [Fact] + public void ComposeResponse_WhenCalled_UsesCatalogAndInventoryValues() + { + var service = new FurnitureAvailabilityDecisionService(); + var request = new FurnitureAvailabilityDecisionRequest("FUR-001", "corr-001"); + var catalog = new ProductContractResponse( + new CatalogContractEnvelope("1.0.0", "corr-001"), + "FUR-001", + "Chair"); + var inventory = new InventoryItemLookupResponse( + new InventoryContractEnvelope("1.0.0", "corr-001"), + "FUR-001", + 12); + + var response = service.ComposeResponse(request, catalog, inventory); + + Assert.Equal("FUR-001", response.FurnitureId); + Assert.Equal("Chair", response.DisplayName); + Assert.Equal(12, response.QuantityAvailable); + } + + [Fact] + public void ComposeResponse_WhenCatalogDisplayNameMissing_UsesFallbackName() + { + var service = new FurnitureAvailabilityDecisionService(); + var request = new FurnitureAvailabilityDecisionRequest("FUR-004", "corr-004"); + var catalog = new ProductContractResponse( + new CatalogContractEnvelope("1.0.0", "corr-004"), + "FUR-004", + string.Empty); + var inventory = new InventoryItemLookupResponse( + new InventoryContractEnvelope("1.0.0", "corr-004"), + "FUR-004", + 5); + + var response = service.ComposeResponse(request, catalog, inventory); + + Assert.Equal("Unknown Furniture", response.DisplayName); + } + + [Fact] + public void ComposeResponse_WhenInventoryNegative_ClampsQuantityToZero() + { + var service = new FurnitureAvailabilityDecisionService(); + var request = new FurnitureAvailabilityDecisionRequest("FUR-005", "corr-005"); + var catalog = new ProductContractResponse( + new CatalogContractEnvelope("1.0.0", "corr-005"), + "FUR-005", + "Shelf"); + var inventory = new InventoryItemLookupResponse( + new InventoryContractEnvelope("1.0.0", "corr-005"), + "FUR-005", + -3); + + var response = service.ComposeResponse(request, catalog, inventory); + + Assert.Equal(0, response.QuantityAvailable); + } +}