Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Distribt.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -36,4 +39,13 @@ public async Task<IActionResult> UpdateProductDetails(int id, ProductDetails pro

return result.Success().UseSuccessHttpStatusCode(HttpStatusCode.OK).ToActionResult();
}

[HttpPut("updateprice/{id}")]
[ProducesResponseType(typeof(ResultDto<bool>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> UpdateProductPrice(int id, UpdateProductPriceRequest request)
{
bool result = await _updateProductPrice.Execute(id, request);

return result.Success().UseSuccessHttpStatusCode(HttpStatusCode.OK).ToActionResult();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.AddScoped<IProductsWriteStore, ProductsWriteStore>()
.AddScoped<IUpdateProductDetails, UpdateProductDetails>()
.AddScoped<ICreateProductDetails, CreateProductDetails>()
.AddScoped<IUpdateProductPrice, UpdateProductPrice>()
.AddScoped<IStockApi,ProductsDependencyFakeType>() //testing purposes
.AddScoped<IWarehouseApi, ProductsDependencyFakeType>() //testing purposes
.AddServiceBusDomainPublisher(builder.Configuration);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<bool> 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<int, decimal> LastPublishedPrice = new();

public UpdateProductPrice(IWarehouseApi warehouseApi, IDomainMessagePublisher domainMessagePublisher)
{
_warehouseApi = warehouseApi;
_domainMessagePublisher = domainMessagePublisher;
}

public async Task<bool> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<ProductPriceChanged>
{
public async Task Handle(DomainMessage<ProductPriceChanged> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Original file line number Diff line number Diff line change
@@ -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<IWarehouseApi> warehouse = new Mock<IWarehouseApi>();
Mock<IDomainMessagePublisher> publisher = new Mock<IDomainMessagePublisher>();
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<int>(),
It.IsAny<decimal>()), Times.Once);
}

[Fact]
public async Task Execute_WhenWarehouseUpdateFails_StillReportsSuccess()
{
Mock<IWarehouseApi> warehouse = new Mock<IWarehouseApi>();
Mock<IDomainMessagePublisher> publisher = new Mock<IDomainMessagePublisher>();
warehouse.Setup(a=>a.ModifySalesPrice(It.IsAny<int>(), It.IsAny<decimal>()))
.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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\Services\Products\Distribt.Services.Products.Api.Read\Distribt.Services.Products.Api.Read.csproj" />
<ProjectReference Include="..\..\..\..\Services\Products\Distribt.Services.Products.Api.Write\Distribt.Services.Products.Api.Write.csproj" />
</ItemGroup>

</Project>