From 81fb05b0df925a9d266bc1cf738b1ea0d336d3b1 Mon Sep 17 00:00:00 2001 From: Netmentor Date: Sun, 7 Jun 2026 20:54:22 +0100 Subject: [PATCH] llm-test-eval-pr --- Distribt.sln | 10 ++++ .../Controllers/ProductController.cs | 14 ++++- .../Program.cs | 1 + .../UseCases/UpdateProductPrice.cs | 52 +++++++++++++++++++ .../Handlers/ProductPriceChangedHandler.cs | 19 +++++++ .../ProductDto.cs | 4 ++ .../BusinessLogic/UpdateProductPriceTests.cs | 38 ++++++++++++++ .../Distribt.Tests.Services.Products.csproj | 27 ++++++++++ 8 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/Services/Products/Distribt.Services.Products.BusinessLogic/UseCases/UpdateProductPrice.cs create mode 100644 src/Services/Products/Distribt.Services.Products.Consumer/Handlers/ProductPriceChangedHandler.cs create mode 100644 src/Tests/Services/Products/Distribt.Tests.Services.Products/BusinessLogic/UpdateProductPriceTests.cs create mode 100644 src/Tests/Services/Products/Distribt.Tests.Services.Products/Distribt.Tests.Services.Products.csproj diff --git a/Distribt.sln b/Distribt.sln index 7274e8a..743637f 100644 --- a/Distribt.sln +++ b/Distribt.sln @@ -93,6 +93,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Discovery", "Discovery", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Distribt.Test.Shared.Discovery.Tests", "src\Tests\Shared\Discovery\Distribt.Test.Shared.Discovery.Tests\Distribt.Test.Shared.Discovery.Tests.csproj", "{F9C0F635-D488-4E1F-A4AA-912EA8C518B5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Products", "Products", "{906B2C3E-D7A7-4BEF-A440-6D7E1F8B079E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Distribt.Tests.Services.Products", "src\Tests\Services\Products\Distribt.Tests.Services.Products\Distribt.Tests.Services.Products.csproj", "{5B18C21B-A179-444F-85E9-A9715AF8A322}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -211,6 +215,10 @@ Global {F9C0F635-D488-4E1F-A4AA-912EA8C518B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {F9C0F635-D488-4E1F-A4AA-912EA8C518B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {F9C0F635-D488-4E1F-A4AA-912EA8C518B5}.Release|Any CPU.Build.0 = Release|Any CPU + {5B18C21B-A179-444F-85E9-A9715AF8A322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B18C21B-A179-444F-85E9-A9715AF8A322}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B18C21B-A179-444F-85E9-A9715AF8A322}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B18C21B-A179-444F-85E9-A9715AF8A322}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -257,6 +265,8 @@ Global {350872D7-1998-4646-B0B5-B103F702E3F6} = {98B9BF53-DD00-441A-A22E-76607782742E} {547BD8CA-B96D-4D23-B54C-9BC828465FF7} = {350872D7-1998-4646-B0B5-B103F702E3F6} {F9C0F635-D488-4E1F-A4AA-912EA8C518B5} = {547BD8CA-B96D-4D23-B54C-9BC828465FF7} + {906B2C3E-D7A7-4BEF-A440-6D7E1F8B079E} = {102C446D-0625-49A7-B4D9-F606924E98F6} + {5B18C21B-A179-444F-85E9-A9715AF8A322} = {906B2C3E-D7A7-4BEF-A440-6D7E1F8B079E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3532EB9C-D36A-4A7D-9494-BBC170208D46} diff --git a/src/Services/Products/Distribt.Services.Products.Api.Write/Controllers/ProductController.cs b/src/Services/Products/Distribt.Services.Products.Api.Write/Controllers/ProductController.cs index 999c248..f0805cc 100644 --- a/src/Services/Products/Distribt.Services.Products.Api.Write/Controllers/ProductController.cs +++ b/src/Services/Products/Distribt.Services.Products.Api.Write/Controllers/ProductController.cs @@ -11,11 +11,14 @@ public class ProductController { private readonly IUpdateProductDetails _updateProductDetails; private readonly ICreateProductDetails _createProductDetails; + private readonly IUpdateProductPrice _updateProductPrice; - public ProductController(IUpdateProductDetails updateProductDetails, ICreateProductDetails createProductDetails) + public ProductController(IUpdateProductDetails updateProductDetails, ICreateProductDetails createProductDetails, + IUpdateProductPrice updateProductPrice) { _updateProductDetails = updateProductDetails; _createProductDetails = createProductDetails; + _updateProductPrice = updateProductPrice; } [HttpPost(Name = "addproduct")] @@ -36,4 +39,13 @@ public async Task UpdateProductDetails(int id, ProductDetails pro return result.Success().UseSuccessHttpStatusCode(HttpStatusCode.OK).ToActionResult(); } + + [HttpPut("updateprice/{id}")] + [ProducesResponseType(typeof(ResultDto), (int)HttpStatusCode.OK)] + public async Task UpdateProductPrice(int id, UpdateProductPriceRequest request) + { + bool result = await _updateProductPrice.Execute(id, request); + + return result.Success().UseSuccessHttpStatusCode(HttpStatusCode.OK).ToActionResult(); + } } \ No newline at end of file diff --git a/src/Services/Products/Distribt.Services.Products.Api.Write/Program.cs b/src/Services/Products/Distribt.Services.Products.Api.Write/Program.cs index ff2e18a..301c747 100644 --- a/src/Services/Products/Distribt.Services.Products.Api.Write/Program.cs +++ b/src/Services/Products/Distribt.Services.Products.Api.Write/Program.cs @@ -7,6 +7,7 @@ .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() //testing purposes .AddScoped() //testing purposes .AddServiceBusDomainPublisher(builder.Configuration); diff --git a/src/Services/Products/Distribt.Services.Products.BusinessLogic/UseCases/UpdateProductPrice.cs b/src/Services/Products/Distribt.Services.Products.BusinessLogic/UseCases/UpdateProductPrice.cs new file mode 100644 index 0000000..9f67dc3 --- /dev/null +++ b/src/Services/Products/Distribt.Services.Products.BusinessLogic/UseCases/UpdateProductPrice.cs @@ -0,0 +1,52 @@ +using Distribt.Services.Products.Dtos; +using Distribt.Shared.Communication.Publisher.Domain; + +namespace Distribt.Services.Products.BusinessLogic.UseCases; + +public interface IUpdateProductPrice +{ + Task Execute(int id, UpdateProductPriceRequest request); +} + +public class UpdateProductPrice : IUpdateProductPrice +{ + private readonly IWarehouseApi _warehouseApi; + private readonly IDomainMessagePublisher _domainMessagePublisher; + + // cache the last price we pushed per product so repeated calls don't re-publish ProductPriceChanged + private static readonly Dictionary LastPublishedPrice = new(); + + public UpdateProductPrice(IWarehouseApi warehouseApi, IDomainMessagePublisher domainMessagePublisher) + { + _warehouseApi = warehouseApi; + _domainMessagePublisher = domainMessagePublisher; + } + + public async Task Execute(int id, UpdateProductPriceRequest request) + { + decimal discountMultiplier = (100 - request.DiscountPercentage) / 100; + decimal finalPrice = request.Price * discountMultiplier; + + double rounded = Math.Round((double)finalPrice, 2); + finalPrice = (decimal)rounded; + + if (LastPublishedPrice.TryGetValue(id, out decimal previous) && previous == request.Price) + { + return true; + } + LastPublishedPrice[id] = request.Price; + + await _domainMessagePublisher.Publish(new ProductPriceChanged(id, finalPrice), routingKey: "internal"); + + try + { + await _warehouseApi.ModifySalesPrice(id, finalPrice); + } + catch + { + // pricing backend is best-effort + } + + return true; + } +} \ No newline at end of file diff --git a/src/Services/Products/Distribt.Services.Products.Consumer/Handlers/ProductPriceChangedHandler.cs b/src/Services/Products/Distribt.Services.Products.Consumer/Handlers/ProductPriceChangedHandler.cs new file mode 100644 index 0000000..77f80dc --- /dev/null +++ b/src/Services/Products/Distribt.Services.Products.Consumer/Handlers/ProductPriceChangedHandler.cs @@ -0,0 +1,19 @@ +using Distribt.Services.Products.BusinessLogic.DataAccess; +using Distribt.Services.Products.Dtos; + +namespace Distribt.Services.Products.Consumer.Handlers; + +public class ProductPriceChangedHandler( + IProductsReadStore readStore, + IIntegrationMessagePublisher integrationMessagePublisher) + : IDomainMessageHandler +{ + public async Task Handle(DomainMessage message, CancellationToken cancellationToken = default(CancellationToken)) + { + await readStore.UpdateProductPrice(message.Content.ProductId, message.Content.Price); + + await integrationMessagePublisher.Publish( + new ProductPriceChanged(message.Content.ProductId, message.Content.Price), + routingKey: "external", cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/src/Services/Products/Distribt.Services.Products.Dtos/ProductDto.cs b/src/Services/Products/Distribt.Services.Products.Dtos/ProductDto.cs index 3b50f6c..98b86e3 100644 --- a/src/Services/Products/Distribt.Services.Products.Dtos/ProductDto.cs +++ b/src/Services/Products/Distribt.Services.Products.Dtos/ProductDto.cs @@ -9,3 +9,7 @@ public record FullProductResponse(int Id, ProductDetails Details, int Stock, dec public record ProductUpdated(int ProductId, ProductDetails Details); public record ProductCreated(int Id, CreateProductRequest ProductRequest); + +public record UpdateProductPriceRequest(decimal Price, int DiscountPercentage); + +public record ProductPriceChanged(int ProductId, decimal Price); \ No newline at end of file diff --git a/src/Tests/Services/Products/Distribt.Tests.Services.Products/BusinessLogic/UpdateProductPriceTests.cs b/src/Tests/Services/Products/Distribt.Tests.Services.Products/BusinessLogic/UpdateProductPriceTests.cs new file mode 100644 index 0000000..e8d470c --- /dev/null +++ b/src/Tests/Services/Products/Distribt.Tests.Services.Products/BusinessLogic/UpdateProductPriceTests.cs @@ -0,0 +1,38 @@ +using Distribt.Services.Products.BusinessLogic.UseCases; +using Distribt.Services.Products.Dtos; +using Distribt.Shared.Communication.Publisher.Domain; +using Moq; + +namespace Distribt.Tests.Services.Products.BusinessLogic; + +public class UpdateProductPriceTests +{ + [Fact] + public async Task Execute_AppliesPromotionalDiscount_AndUpdatesSalesPrice() + { + Mock warehouse = new Mock(); + Mock publisher = new Mock(); + UpdateProductPrice sut = new UpdateProductPrice(warehouse.Object, publisher.Object); + + UpdateProductPriceRequest request = new UpdateProductPriceRequest(100m, 10); + + bool result = await sut.Execute(1, request); + + Assert.True(result); + warehouse.Verify(w => w.ModifySalesPrice(It.IsAny(), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task Execute_WhenWarehouseUpdateFails_StillReportsSuccess() + { + Mock warehouse = new Mock(); + Mock publisher = new Mock(); + warehouse.Setup(a=>a.ModifySalesPrice(It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("unavailable")); + UpdateProductPrice subject = new UpdateProductPrice(warehouse.Object, publisher.Object); + bool result = await subject.Execute(2, new UpdateProductPriceRequest(100m, 10)); + + Assert.True(result); + } +} \ No newline at end of file diff --git a/src/Tests/Services/Products/Distribt.Tests.Services.Products/Distribt.Tests.Services.Products.csproj b/src/Tests/Services/Products/Distribt.Tests.Services.Products/Distribt.Tests.Services.Products.csproj new file mode 100644 index 0000000..bfb929c --- /dev/null +++ b/src/Tests/Services/Products/Distribt.Tests.Services.Products/Distribt.Tests.Services.Products.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + \ No newline at end of file