diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 278ff7d64..0e752fb0e 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -7,7 +7,12 @@ version: '2' # An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. services: - + sagamanager: + environment: + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word + - EventBusConnection=rabbitmq + - GracePeriod=15 #In minutes + basket.api: environment: - ASPNETCORE_ENVIRONMENT=Development diff --git a/docker-compose.vs.debug.yml b/docker-compose.vs.debug.yml index 88ebfc47c..eaa0b6f35 100644 --- a/docker-compose.vs.debug.yml +++ b/docker-compose.vs.debug.yml @@ -120,3 +120,16 @@ services: entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + sagamanager: + image: eshop/sagamanager:dev + build: + args: + source: ${DOCKER_BUILD_SOURCE} + volumes: + - ./src/Services/SagaManager/SagaManager:/app + - ~/.nuget/packages:/root/.nuget/packages:ro + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" diff --git a/docker-compose.vs.release.yml b/docker-compose.vs.release.yml index 9a51a08f5..a0c9d890d 100644 --- a/docker-compose.vs.release.yml +++ b/docker-compose.vs.release.yml @@ -80,3 +80,13 @@ services: entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + sagamanager: + build: + args: + source: ${DOCKER_BUILD_SOURCE} + volumes: + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" diff --git a/docker-compose.yml b/docker-compose.yml index 718bfc5f5..cc134a005 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,16 @@ version: '2' + services: + sagamanager: + image: eshop/sagamanager + build: + context: ./src/Services/SagaManager/SagaManager + dockerfile: Dockerfile + depends_on: + - sql.data + - rabbitmq + basket.api: image: eshop/basket.api build: @@ -9,7 +19,6 @@ services: depends_on: - basket.data - identity.api - - rabbitmq catalog.api: image: eshop/catalog.api @@ -35,6 +44,7 @@ services: dockerfile: Dockerfile depends_on: - sql.data + - rabbitmq webspa: image: eshop/webspa diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index 80c95a417..043d577b3 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -80,6 +80,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Health EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "src\BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{4A980AC4-7205-46BF-8CCB-09E44D700FD4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SagaManager", "SagaManager", "{F38B4FF0-0B49-405A-B1B4-F7A5E3BC4C4E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SagaManager", "src\Services\SagaManager\SagaManager\SagaManager.csproj", "{F6E0F0DD-1400-43C3-B5E0-7CC325728C47}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1054,6 +1058,54 @@ Global {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|x64.Build.0 = Release|Any CPU {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|x86.ActiveCfg = Release|Any CPU {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|x86.Build.0 = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|ARM.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|iPhone.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|x64.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|x64.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|x86.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.AppStore|x86.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|ARM.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|ARM.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|iPhone.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|x64.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|x64.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|x86.ActiveCfg = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Debug|x86.Build.0 = Debug|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|Any CPU.Build.0 = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|ARM.ActiveCfg = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|ARM.Build.0 = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|iPhone.ActiveCfg = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|iPhone.Build.0 = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|x64.ActiveCfg = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|x64.Build.0 = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|x86.ActiveCfg = Release|Any CPU + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1092,5 +1144,7 @@ Global {22A0F9C1-2D4A-4107-95B7-8459E6688BC5} = {A81ECBC2-6B00-4DCD-8388-469174033379} {4BD76717-3102-4969-8C2C-BAAA3F0263B6} = {A81ECBC2-6B00-4DCD-8388-469174033379} {4A980AC4-7205-46BF-8CCB-09E44D700FD4} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} + {F38B4FF0-0B49-405A-B1B4-F7A5E3BC4C4E} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} + {F6E0F0DD-1400-43C3-B5E0-7CC325728C47} = {F38B4FF0-0B49-405A-B1B4-F7A5E3BC4C4E} EndGlobalSection EndGlobal diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170509130025_AddStockProductItem.Designer.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170509130025_AddStockProductItem.Designer.cs new file mode 100644 index 000000000..779888aa3 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170509130025_AddStockProductItem.Designer.cs @@ -0,0 +1,107 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; + +namespace Catalog.API.Infrastructure.Migrations +{ + [DbContext(typeof(CatalogContext))] + [Migration("20170509130025_AddStockProductItem")] + partial class AddStockProductItem + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1") + .HasAnnotation("SqlServer:Sequence:.catalog_brand_hilo", "'catalog_brand_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.catalog_hilo", "'catalog_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.catalog_type_hilo", "'catalog_type_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "catalog_brand_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Brand") + .IsRequired() + .HasMaxLength(100); + + b.HasKey("Id"); + + b.ToTable("CatalogBrand"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("AvailableStock"); + + b.Property("CatalogBrandId"); + + b.Property("CatalogTypeId"); + + b.Property("Description"); + + b.Property("MaxStockThreshold"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50); + + b.Property("OnReorder"); + + b.Property("PictureUri"); + + b.Property("Price"); + + b.Property("RestockThreshold"); + + b.HasKey("Id"); + + b.HasIndex("CatalogBrandId"); + + b.HasIndex("CatalogTypeId"); + + b.ToTable("Catalog"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "catalog_type_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100); + + b.HasKey("Id"); + + b.ToTable("CatalogType"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", "CatalogBrand") + .WithMany() + .HasForeignKey("CatalogBrandId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", "CatalogType") + .WithMany() + .HasForeignKey("CatalogTypeId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170509130025_AddStockProductItem.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170509130025_AddStockProductItem.cs new file mode 100644 index 000000000..ede85cb4e --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170509130025_AddStockProductItem.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Catalog.API.Infrastructure.Migrations +{ + public partial class AddStockProductItem : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AvailableStock", + table: "Catalog", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "MaxStockThreshold", + table: "Catalog", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "OnReorder", + table: "Catalog", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "RestockThreshold", + table: "Catalog", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AvailableStock", + table: "Catalog"); + + migrationBuilder.DropColumn( + name: "MaxStockThreshold", + table: "Catalog"); + + migrationBuilder.DropColumn( + name: "OnReorder", + table: "Catalog"); + + migrationBuilder.DropColumn( + name: "RestockThreshold", + table: "Catalog"); + } + } +} diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/CatalogContextModelSnapshot.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/CatalogContextModelSnapshot.cs index ba1151672..851cfefe6 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/CatalogContextModelSnapshot.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/CatalogContextModelSnapshot.cs @@ -42,20 +42,28 @@ namespace Catalog.API.Infrastructure.Migrations .HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + b.Property("AvailableStock"); + b.Property("CatalogBrandId"); b.Property("CatalogTypeId"); b.Property("Description"); + b.Property("MaxStockThreshold"); + b.Property("Name") .IsRequired() .HasMaxLength(50); + b.Property("OnReorder"); + b.Property("PictureUri"); b.Property("Price"); + b.Property("RestockThreshold"); + b.HasKey("Id"); b.HasIndex("CatalogBrandId"); diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/ConfirmOrderStockIntegrationEventHandler.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/ConfirmOrderStockIntegrationEventHandler.cs new file mode 100644 index 000000000..21d12e1ee --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/ConfirmOrderStockIntegrationEventHandler.cs @@ -0,0 +1,61 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling +{ + using BuildingBlocks.EventBus.Abstractions; + using System.Threading.Tasks; + using BuildingBlocks.EventBus.Events; + using Infrastructure; + using System.Collections.Generic; + using System.Linq; + using global::Catalog.API.Infrastructure.Exceptions; + using global::Catalog.API.IntegrationEvents; + using Model; + using Events; + + public class ConfirmOrderStockIntegrationEventHandler : IIntegrationEventHandler + { + private readonly CatalogContext _catalogContext; + private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; + + public ConfirmOrderStockIntegrationEventHandler(CatalogContext catalogContext, + ICatalogIntegrationEventService catalogIntegrationEventService) + { + _catalogContext = catalogContext; + _catalogIntegrationEventService = catalogIntegrationEventService; + } + + public async Task Handle(ConfirmOrderStockIntegrationEvent @event) + { + var confirmedOrderStockItems = new List(); + + foreach (var orderStockItem in @event.OrderStockItems) + { + var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); + CheckValidcatalogItemId(catalogItem); + + var confirmedOrderStockItem = new ConfirmedOrderStockItem(catalogItem.Id, + catalogItem.AvailableStock >= orderStockItem.Units); + + confirmedOrderStockItems.Add(confirmedOrderStockItem); + } + + //Create Integration Event to be published through the Event Bus + var confirmedIntegrationEvent = confirmedOrderStockItems.Any(c => !c.Confirmed) + ? (IntegrationEvent) new OrderStockNotConfirmedIntegrationEvent(@event.OrderId, confirmedOrderStockItems) + : new OrderStockConfirmedIntegrationEvent(@event.OrderId); + + // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction + await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(confirmedIntegrationEvent); + + // Publish through the Event Bus and mark the saved event as published + await _catalogIntegrationEventService.PublishThroughEventBusAsync(confirmedIntegrationEvent); + } + + private void CheckValidcatalogItemId(CatalogItem catalogItem) + { + if (catalogItem is null) + { + throw new CatalogDomainException("Not able to process catalog event. Reason: no valid catalogItemId"); + } + } + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ConfirmOrderStockIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ConfirmOrderStockIntegrationEvent.cs new file mode 100644 index 000000000..366c8f854 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ConfirmOrderStockIntegrationEvent.cs @@ -0,0 +1,30 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events +{ + using BuildingBlocks.EventBus.Events; + using System.Collections.Generic; + + public class ConfirmOrderStockIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + public IEnumerable OrderStockItems { get; } + + public ConfirmOrderStockIntegrationEvent(int orderId, + IEnumerable orderStockItems) + { + OrderId = orderId; + OrderStockItems = orderStockItems; + } + } + + public class OrderStockItem + { + public int ProductId { get; } + public int Units { get; } + + public OrderStockItem(int productId, int units) + { + ProductId = productId; + Units = units; + } + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..1ce179c3a --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStockConfirmedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public OrderStockConfirmedIntegrationEvent(int orderId) => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockNotConfirmedIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockNotConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..8186f8a13 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockNotConfirmedIntegrationEvent.cs @@ -0,0 +1,31 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events +{ + using BuildingBlocks.EventBus.Events; + using System.Collections.Generic; + + public class OrderStockNotConfirmedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public IEnumerable OrderStockItem { get; } + + public OrderStockNotConfirmedIntegrationEvent(int orderId, + IEnumerable orderStockItem) + { + OrderId = orderId; + OrderStockItem = orderStockItem; + } + } + + public class ConfirmedOrderStockItem + { + public int ProductId { get; } + public bool Confirmed { get; } + + public ConfirmedOrderStockItem(int productId, bool confirmed) + { + ProductId = productId; + Confirmed = confirmed; + } + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs b/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs index a232cb2fe..f3c04f595 100644 --- a/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs +++ b/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs @@ -1,4 +1,5 @@ -using System; +using Catalog.API.Infrastructure.Exceptions; +using System; namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model { @@ -22,6 +23,79 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model public CatalogBrand CatalogBrand { get; set; } - public CatalogItem() { } + // Quantity in stock + public int AvailableStock { get; set; } + + // Available stock at which we should reorder + public int RestockThreshold { get; set; } + + + // Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses) + public int MaxStockThreshold { get; set; } + + /// + /// True if item is on reorder + /// + public bool OnReorder { get; set; } + + public CatalogItem() { } + + + /// + /// Decrements the quantity of a particular item in inventory and ensures the restockThreshold hasn't + /// been breached. If so, a RestockRequest is generated in CheckThreshold. + /// + /// If there is sufficient stock of an item, then the integer returned at the end of this call should be the same as quantityDesired. + /// In the event that there is not sufficient stock available, the method will remove whatever stock is available and return that quantity to the client. + /// In this case, it is the responsibility of the client to determine if the amount that is returned is the same as quantityDesired. + /// It is invalid to pass in a negative number. + /// + /// + /// int: Returns the number actually removed from stock. + /// + public int RemoveStock(int quantityDesired) + { + if (AvailableStock == 0) + { + throw new CatalogDomainException($"Empty stock, product item {Name} is sold out"); + } + + if (quantityDesired <= 0) + { + throw new CatalogDomainException($"Item units desired should be greater than cero"); + } + + int removed = Math.Min(quantityDesired, this.AvailableStock); + + this.AvailableStock -= removed; + + return removed; + } + + /// + /// Increments the quantity of a particular item in inventory. + /// + /// int: Returns the quantity that has been added to stock + /// + public int AddStock(int quantity) + { + int original = this.AvailableStock; + + // The quantity that the client is trying to add to stock is greater than what can be physically accommodated in the Warehouse + if ((this.AvailableStock + quantity) > this.MaxStockThreshold) + { + // For now, this method only adds new units up maximum stock threshold. In an expanded version of this application, we + //could include tracking for the remaining units and store information about overstock elsewhere. + this.AvailableStock += (this.MaxStockThreshold - this.AvailableStock); + } + else + { + this.AvailableStock += quantity; + } + + this.OnReorder = false; + + return this.AvailableStock - original; + } } -} +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmation/UpdateOrderWhenOrderStockMethodVerifiedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmation/UpdateOrderWhenOrderStockMethodVerifiedDomainEventHandler.cs new file mode 100644 index 000000000..abea5e0da --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmation/UpdateOrderWhenOrderStockMethodVerifiedDomainEventHandler.cs @@ -0,0 +1,50 @@ +namespace Ordering.API.Application.DomainEventHandlers.OrderStartedEvent +{ + using MediatR; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using Microsoft.Extensions.Logging; + using Domain.Events; + using System; + using System.Threading.Tasks; + + public class UpdateOrderWhenOrderStockMethodVerifiedDomainEventHandler + : IAsyncNotificationHandler + { + private readonly IOrderRepository _orderRepository; + private readonly ILoggerFactory _logger; + + public UpdateOrderWhenOrderStockMethodVerifiedDomainEventHandler( + IOrderRepository orderRepository, ILoggerFactory logger) + { + _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + // Domain Logic comment: + // When the Order Stock items method have been validate and confirmed, + // then we can update the original Order with the new order status + public async Task Handle(OrderStockMethodVerifiedDomainEvent orderStockMethodVerifiedDomainEvent) + { + var orderToUpdate = await _orderRepository.GetAsync(orderStockMethodVerifiedDomainEvent.OrderId); + orderToUpdate.SetOrderStatusId(orderStockMethodVerifiedDomainEvent.OrderStatus.Id); + + _orderRepository.Update(orderToUpdate); + + await _orderRepository.UnitOfWork + .SaveEntitiesAsync(); + + _logger.CreateLogger(nameof(UpdateOrderWhenOrderStockMethodVerifiedDomainEventHandler)) + .LogTrace($"Order with Id: {orderStockMethodVerifiedDomainEvent.OrderId} has been successfully updated with " + + $"a status order id: { orderStockMethodVerifiedDomainEvent.OrderStatus.Id }"); + + + //var payOrderCommandMsg = new PayOrderCommandMsg(order.Id); + + //// Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction + //await _orderingIntegrationEventService.SaveEventAndOrderingContextChangesAsync(payOrderCommandMsg); + + //// Publish through the Event Bus and mark the saved event as published + //await _orderingIntegrationEventService.PublishThroughEventBusAsync(payOrderCommandMsg); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/ConfirmGracePeriodCommandMsg.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/ConfirmGracePeriodCommandMsg.cs index a0767459e..f9d8b8923 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/ConfirmGracePeriodCommandMsg.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/ConfirmGracePeriodCommandMsg.cs @@ -1,15 +1,12 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Ordering.API.Application.IntegrationCommands.Commands { public class ConfirmGracePeriodCommandMsg : IntegrationEvent { - public int OrderNumber { get; private set; } + public int OrderId { get; } - //TODO: message should change to Integration command type once command bus is implemented + public ConfirmGracePeriodCommandMsg(int orderId) => + OrderId = orderId; } } diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/ConfirmOrderStockCommandMsg.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/ConfirmOrderStockCommandMsg.cs new file mode 100644 index 000000000..abf93d0ef --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/ConfirmOrderStockCommandMsg.cs @@ -0,0 +1,30 @@ +namespace Ordering.API.Application.IntegrationCommands.Commands +{ + using System.Collections.Generic; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class ConfirmOrderStockCommandMsg : IntegrationEvent + { + public int OrderId { get; } + public IEnumerable OrderStockItem { get; } + + public ConfirmOrderStockCommandMsg(int orderId, + IEnumerable orderStockItem) + { + OrderId = orderId; + OrderStockItem = orderStockItem; + } + } + + public class OrderStockItem + { + public int ProductId { get; } + public int Units { get; } + + public OrderStockItem(int productId, int units) + { + ProductId = productId; + Units = units; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/PayOrderCommandMsg.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/PayOrderCommandMsg.cs new file mode 100644 index 000000000..749ec882e --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationCommands/Commands/PayOrderCommandMsg.cs @@ -0,0 +1,14 @@ +namespace Ordering.API.Application.IntegrationCommands.Commands +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class PayOrderCommandMsg : IntegrationEvent + { + public int OrderId { get; } + + public PayOrderCommandMsg(int orderId) + { + OrderId = orderId; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs new file mode 100644 index 000000000..34dec2351 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs @@ -0,0 +1,48 @@ +namespace Ordering.API.Application.IntegrationEvents.EventHandling +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using System.Threading.Tasks; + using Events; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using Ordering.API.Application.IntegrationCommands.Commands; + using Ordering.Domain.Exceptions; + + public class OrderStockConfirmedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + private readonly IOrderRepository _orderRepository; + + public OrderStockConfirmedIntegrationEventHandler(IOrderRepository orderRepository, + IOrderingIntegrationEventService orderingIntegrationEventService) + { + _orderRepository = orderRepository; + _orderingIntegrationEventService = orderingIntegrationEventService; + } + + public async Task Handle(OrderStockConfirmedIntegrationEvent @event) + { + //TODO: 1) Updates the state to "StockValidated" and any meaningful OrderContextDescription message saying that all the items were confirmed with available stock, etc + var order = await _orderRepository.GetAsync(@event.OrderId); + CheckValidSagaId(order); + + order.SetOrderStockConfirmed(true); + + //Create Integration Event to be published through the Event Bus + var payOrderCommandMsg = new PayOrderCommandMsg(order.Id); + + // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction + await _orderingIntegrationEventService.SaveEventAndOrderingContextChangesAsync(payOrderCommandMsg); + + // Publish through the Event Bus and mark the saved event as published + await _orderingIntegrationEventService.PublishThroughEventBusAsync(payOrderCommandMsg); + } + + private void CheckValidSagaId(Order orderSaga) + { + if (orderSaga is null) + { + throw new OrderingDomainException("Not able to process order saga event. Reason: no valid orderId"); + } + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockNotConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockNotConfirmedIntegrationEventHandler.cs new file mode 100644 index 000000000..c5b7e6b01 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockNotConfirmedIntegrationEventHandler.cs @@ -0,0 +1,48 @@ +namespace Ordering.API.Application.IntegrationEvents.EventHandling +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using System; + using System.Threading.Tasks; + using Events; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using Ordering.API.Application.Sagas; + using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; + using Ordering.Domain.Exceptions; + + public class OrderStockNotConfirmedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + private readonly IOrderRepository _orderRepository; + + public OrderStockNotConfirmedIntegrationEventHandler(IOrderRepository orderRepository, + IOrderingIntegrationEventService orderingIntegrationEventService) + { + _orderRepository = orderRepository; + _orderingIntegrationEventService = orderingIntegrationEventService; + } + + public async Task Handle(OrderStockNotConfirmedIntegrationEvent @event) + { + //TODO: must update the order state to cancelled and the CurrentOrderStateContextDescription with the reasons of no-stock confirm + var order = await _orderRepository.GetAsync(@event.OrderId); + CheckValidSagaId(order); + + order.SetOrderStockConfirmed(false); + + var orderStockNotConfirmedItems = @event.OrderStockItems.FindAll(c => !c.Confirmed); + + foreach (var orderStockNotConfirmedItem in orderStockNotConfirmedItems) + { + //TODO: Add messages + } + } + + private void CheckValidSagaId(Order orderSaga) + { + if (orderSaga is null) + { + throw new OrderingDomainException("Not able to process order saga event. Reason: no valid orderId"); + } + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs index 82416db46..808b43a1d 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs @@ -16,4 +16,4 @@ namespace Ordering.API.Application.IntegrationEvents.Events public OrderStartedIntegrationEvent(string userId) => UserId = userId; } -} +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..4bcc9aab5 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs @@ -0,0 +1,11 @@ +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStockConfirmedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public OrderStockConfirmedIntegrationEvent(int orderId) => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockNotConfirmedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockNotConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..5337c5eae --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockNotConfirmedIntegrationEvent.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStockNotConfirmedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public List OrderStockItems { get; } + + public OrderStockNotConfirmedIntegrationEvent(int orderId, + List orderStockItems) + { + OrderId = orderId; + OrderStockItems = orderStockItems; + } + } + + public class ConfirmedOrderStockItem + { + public int ProductId { get; } + public bool Confirmed { get; } + + public ConfirmedOrderStockItem(int productId, bool confirmed) + { + ProductId = productId; + Confirmed = confirmed; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/Sagas/OrderProcessSaga.cs b/src/Services/Ordering/Ordering.API/Application/Sagas/OrderProcessSaga.cs index 91d8dd09f..3a35d3f85 100644 --- a/src/Services/Ordering/Ordering.API/Application/Sagas/OrderProcessSaga.cs +++ b/src/Services/Ordering/Ordering.API/Application/Sagas/OrderProcessSaga.cs @@ -10,7 +10,12 @@ using Ordering.API.Application.IntegrationCommands.Commands; using Ordering.API.Application.IntegrationEvents.Events; using Ordering.Domain.Exceptions; using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Threading.Tasks; +using Ordering.API.Application.IntegrationEvents; +using Ordering.API.Application.IntegrationEvents.Events; namespace Ordering.API.Application.Sagas { @@ -29,14 +34,16 @@ namespace Ordering.API.Application.Sagas { private readonly IMediator _mediator; private readonly Func> _dbContextFactory; + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; public OrderProcessSaga( Func> dbContextFactory, OrderingContext orderingContext, - IMediator mediator) + IMediator mediator, IOrderingIntegrationEventService orderingIntegrationEventService) : base(orderingContext) { _dbContextFactory = dbContextFactory; _mediator = mediator; + _orderingIntegrationEventService = orderingIntegrationEventService; } /// @@ -72,18 +79,27 @@ namespace Ordering.API.Application.Sagas /// period has completed. /// /// - public async Task Handle(ConfirmGracePeriodCommandMsg command) + public async Task Handle(ConfirmGracePeriodCommandMsg @event) { - var orderSaga = FindSagaById(command.OrderNumber); + var orderSaga = FindSagaById(@event.OrderId); CheckValidSagaId(orderSaga); - // TODO: This handler should change to Integration command handler type once command bus is implemented + if (orderSaga.OrderStatus != OrderStatus.Cancelled) + { + orderSaga.SetOrderStatusId(OrderStatus.AwaitingValidation.Id); + await SaveChangesAsync(); - // TODO: If order status is not cancelled, change state to awaitingValidation and - // send ConfirmOrderStockCommandMsg to Inventory api + var orderStockList = orderSaga.OrderItems + .Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.GetUnits())); + + //Create Integration Event to be published through the Event Bus + var confirmOrderStockEvent = new ConfirmOrderStockCommandMsg(orderSaga.Id, orderStockList); + + // Publish through the Event Bus and mark the saved event as published + await _orderingIntegrationEventService.PublishThroughEventBusAsync(confirmOrderStockEvent); + } } - /// /// Handler which processes the command when /// customer executes cancel order from app diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs b/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs index de3294df7..b41d019fb 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs @@ -31,9 +31,12 @@ if (!context.OrderStatus.Any()) { - context.OrderStatus.Add(OrderStatus.Canceled); - context.OrderStatus.Add(OrderStatus.InProcess); + context.OrderStatus.Add(OrderStatus.Submited); + context.OrderStatus.Add(OrderStatus.AwaitingValidation); + context.OrderStatus.Add(OrderStatus.StockValidated); + context.OrderStatus.Add(OrderStatus.Paid); context.OrderStatus.Add(OrderStatus.Shipped); + context.OrderStatus.Add(OrderStatus.Cancelled); } await context.SaveChangesAsync(); diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 0e7742cc6..2ba5e69f3 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -6,6 +6,9 @@ using global::Ordering.API.Application.IntegrationEvents; using global::Ordering.API.Application.IntegrationEvents.Events; using global::Ordering.API.Infrastructure.Middlewares; + using global::Ordering.API.Application.IntegrationCommands.Commands; + using global::Ordering.API.Application.IntegrationEvents.Events; + using global::Ordering.API.Application.Sagas; using Infrastructure; using Infrastructure.Auth; using Infrastructure.AutofacModules; @@ -125,6 +128,9 @@ services.AddSingleton(); services.AddSingleton(); //services.AddTransient(); + services.AddTransient, OrderProcessSaga>(); + services.AddTransient(); + services.AddTransient(); services.AddOptions(); //configure autofac @@ -163,6 +169,7 @@ .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Ordering.API")) .Options); integrationEventLogContext.Database.Migrate(); + } private void ConfigureEventBus(IApplicationBuilder app) @@ -172,6 +179,14 @@ eventBus.Subscribe>( () => app.ApplicationServices.GetRequiredService>()); + eventBus.Subscribe> + (() => app.ApplicationServices.GetRequiredService>()); + + eventBus.Subscribe + (() => app.ApplicationServices.GetRequiredService()); + + eventBus.Subscribe + (() => app.ApplicationServices.GetRequiredService()); } protected virtual void ConfigureAuth(IApplicationBuilder app) diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs index 281cf6ecc..7519a7de3 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -46,7 +46,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O _orderItems = new List(); _buyerId = buyerId; _paymentMethodId = paymentMethodId; - _orderStatusId = OrderStatus.InProcess.Id; + _orderStatusId = OrderStatus.Submited.Id; _orderDate = DateTime.UtcNow; Address = address; @@ -94,6 +94,25 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O _buyerId = id; } + public void SetOrderStatusId(int id) + { + _orderStatusId = id; + } + + public void SetOrderStockConfirmed(bool confirmed) + { + if(confirmed) + { + OrderStatus = OrderStatus.StockValidated; + AddDomainEvent(new OrderStockMethodVerifiedDomainEvent(Id, OrderStatus.StockValidated)); + } + else + { + OrderStatus = OrderStatus.Cancelled; + AddDomainEvent(new OrderStockMethodVerifiedDomainEvent(Id, OrderStatus.Cancelled)); + } + } + private void AddOrderStartedDomainEvent(int cardTypeId, string cardNumber, string cardSecurityNumber, string cardHolderName, DateTime cardExpiration) { diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs index 5d011981a..991fb8730 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs @@ -54,6 +54,11 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O return _discount; } + public int GetUnits() + { + return _units; + } + public void SetNewDiscount(decimal discount) { if (discount < 0) diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs index 6e3ff74b1..73178cbe5 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs @@ -10,9 +10,12 @@ public class OrderStatus : Enumeration { - public static OrderStatus InProcess = new OrderStatus(1, nameof(InProcess).ToLowerInvariant()); - public static OrderStatus Shipped = new OrderStatus(2, nameof(Shipped).ToLowerInvariant()); - public static OrderStatus Canceled = new OrderStatus(3, nameof(Canceled).ToLowerInvariant()); + public static OrderStatus Submited = new OrderStatus(1, nameof(Submited).ToLowerInvariant()); + public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant()); + public static OrderStatus StockValidated = new OrderStatus(3, nameof(StockValidated).ToLowerInvariant()); + public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant()); + public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant()); + public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant()); protected OrderStatus() { @@ -23,10 +26,8 @@ { } - public static IEnumerable List() - { - return new[] { InProcess, Shipped, Canceled }; - } + public static IEnumerable List() => + new[] { Submited, AwaitingValidation, StockValidated, Paid, Shipped, Cancelled }; public static OrderStatus FromName(string name) { diff --git a/src/Services/Ordering/Ordering.Domain/Events/OrderStockMethodVerifiedDomainEvent.cs b/src/Services/Ordering/Ordering.Domain/Events/OrderStockMethodVerifiedDomainEvent.cs new file mode 100644 index 000000000..c5acd26dd --- /dev/null +++ b/src/Services/Ordering/Ordering.Domain/Events/OrderStockMethodVerifiedDomainEvent.cs @@ -0,0 +1,22 @@ +namespace Ordering.Domain.Events +{ + using MediatR; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + + /// + /// Event used when the order stock items are verified + /// + public class OrderStockMethodVerifiedDomainEvent + : IAsyncNotification + { + public int OrderId { get; } + public OrderStatus OrderStatus { get; } + + public OrderStockMethodVerifiedDomainEvent(int orderId, + OrderStatus orderStatus) + { + OrderId = orderId; + OrderStatus = orderStatus; + } + } +} \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/.dockerignore b/src/Services/SagaManager/SagaManager/.dockerignore new file mode 100644 index 000000000..d8f8175f6 --- /dev/null +++ b/src/Services/SagaManager/SagaManager/.dockerignore @@ -0,0 +1,3 @@ +* +!obj/Docker/publish/* +!obj/Docker/empty/ diff --git a/src/Services/SagaManager/SagaManager/Dockerfile b/src/Services/SagaManager/SagaManager/Dockerfile new file mode 100644 index 000000000..f8adb603a --- /dev/null +++ b/src/Services/SagaManager/SagaManager/Dockerfile @@ -0,0 +1,5 @@ +FROM microsoft/dotnet:1.1-runtime +ARG source +WORKDIR /app +COPY ${source:-obj/Docker/publish} . +ENTRYPOINT ["dotnet", "SagaManager.dll"] \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/IntegrationEvents/Events/ConfirmGracePeriodCommandMsg.cs b/src/Services/SagaManager/SagaManager/IntegrationEvents/Events/ConfirmGracePeriodCommandMsg.cs new file mode 100644 index 000000000..fc47f44e5 --- /dev/null +++ b/src/Services/SagaManager/SagaManager/IntegrationEvents/Events/ConfirmGracePeriodCommandMsg.cs @@ -0,0 +1,14 @@ +namespace SagaManager.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + // Integration Events notes: + // An Event is “something that has happened in the past”, therefore its name has to be + // An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems. + public class ConfirmGracePeriodCommandMsg : IntegrationEvent + { + public int OrderId { get;} + + public ConfirmGracePeriodCommandMsg(int orderId) => OrderId = orderId; + } +} diff --git a/src/Services/SagaManager/SagaManager/IntegrationEvents/ISagaManagerIntegrationEventService.cs b/src/Services/SagaManager/SagaManager/IntegrationEvents/ISagaManagerIntegrationEventService.cs new file mode 100644 index 000000000..41407702d --- /dev/null +++ b/src/Services/SagaManager/SagaManager/IntegrationEvents/ISagaManagerIntegrationEventService.cs @@ -0,0 +1,9 @@ +namespace SagaManager.IntegrationEvents +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public interface ISagaManagerIntegrationEventService + { + void PublishThroughEventBus(IntegrationEvent evt); + } +} \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/IntegrationEvents/SagaManagerIntegrationEventService.cs b/src/Services/SagaManager/SagaManager/IntegrationEvents/SagaManagerIntegrationEventService.cs new file mode 100644 index 000000000..3643c0530 --- /dev/null +++ b/src/Services/SagaManager/SagaManager/IntegrationEvents/SagaManagerIntegrationEventService.cs @@ -0,0 +1,21 @@ +namespace SagaManager.IntegrationEvents +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + using System; + + public class SagaManagerIntegrationEventService : ISagaManagerIntegrationEventService + { + private readonly IEventBus _eventBus; + + public SagaManagerIntegrationEventService(IEventBus eventBus) + { + _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); + } + + public void PublishThroughEventBus(IntegrationEvent evt) + { + _eventBus.Publish(evt); + } + } +} \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/Program.cs b/src/Services/SagaManager/SagaManager/Program.cs new file mode 100644 index 000000000..06fdec6ba --- /dev/null +++ b/src/Services/SagaManager/SagaManager/Program.cs @@ -0,0 +1,95 @@ +using System.Reflection; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; +using Microsoft.EntityFrameworkCore; +using SagaManager.IntegrationEvents; + +namespace SagaManager +{ + using System.IO; + using System; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using RabbitMQ.Client; + using Services; + + + public class Program + { + public static IConfigurationRoot Configuration { get; set; } + + public static void Main(string[] args) + { + StartUp(); + + IServiceCollection services = new ServiceCollection(); + var serviceProvider = ConfigureServices(services); + + var logger = serviceProvider.GetService(); + Configure(logger); + + var sagaManagerService = serviceProvider + .GetRequiredService(); + + while (true) + { + sagaManagerService.CheckFinishedGracePeriodOrders(); + System.Threading.Thread.Sleep(30000); + } + } + + public static void StartUp() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public static IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddLogging() + .AddOptions() + .Configure(Configuration) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var logger = sp.GetRequiredService>(); + var factory = new ConnectionFactory() + { + HostName = settings.EventBusConnection + }; + + return new DefaultRabbitMQPersistentConnection(factory, logger); + }) + .AddSingleton(); + + RegisterServiceBus(services); + + return services.BuildServiceProvider(); + } + + public static void Configure(ILoggerFactory loggerFactory) + { + loggerFactory + .AddConsole(Configuration.GetSection("Logging")) + .AddConsole(LogLevel.Debug); + } + + private static void RegisterServiceBus(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } + } +} \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/SagaManager.csproj b/src/Services/SagaManager/SagaManager/SagaManager.csproj new file mode 100644 index 000000000..ed76ff6d8 --- /dev/null +++ b/src/Services/SagaManager/SagaManager/SagaManager.csproj @@ -0,0 +1,34 @@ + + + + Exe + netcoreapp1.1 + + + + + + + + + + + + + + + + + + + + + + + + + Dockerfile + + + + \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/SagaManagerSettings.cs b/src/Services/SagaManager/SagaManager/SagaManagerSettings.cs new file mode 100644 index 000000000..ab9184b8a --- /dev/null +++ b/src/Services/SagaManager/SagaManager/SagaManagerSettings.cs @@ -0,0 +1,11 @@ +namespace SagaManager +{ + public class SagaManagerSettings + { + public string ConnectionString { get; set; } + + public string EventBusConnection { get; set; } + + public int GracePeriod { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/Services/ISagaManagerService.cs b/src/Services/SagaManager/SagaManager/Services/ISagaManagerService.cs new file mode 100644 index 000000000..dc027b29c --- /dev/null +++ b/src/Services/SagaManager/SagaManager/Services/ISagaManagerService.cs @@ -0,0 +1,7 @@ +namespace SagaManager.Services +{ + public interface ISagaManagerService + { + void CheckFinishedGracePeriodOrders(); + } +} \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/Services/SagaManagerService.cs b/src/Services/SagaManager/SagaManager/Services/SagaManagerService.cs new file mode 100644 index 000000000..b40c2fb08 --- /dev/null +++ b/src/Services/SagaManager/SagaManager/Services/SagaManagerService.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace SagaManager.Services +{ + using System.Collections.Generic; + using System.Data.SqlClient; + using Microsoft.Extensions.Options; + using Dapper; + using IntegrationEvents; + using IntegrationEvents.Events; + + public class SagaManagerService : ISagaManagerService + { + private readonly SagaManagerSettings _settings; + private readonly ISagaManagerIntegrationEventService _sagaManagerIntegrationEventService; + private readonly ILogger _logger; + + public SagaManagerService(IOptions settings, + ISagaManagerIntegrationEventService sagaManagerIntegrationEventService, + ILogger logger) + { + _settings = settings.Value; + _sagaManagerIntegrationEventService = sagaManagerIntegrationEventService; + _logger = logger; + } + + public void CheckFinishedGracePeriodOrders() + { + var orderIds = GetFinishedGracePeriodOrders(); + + foreach (var orderId in orderIds) + { + Publish(orderId); + } + } + + private IEnumerable GetFinishedGracePeriodOrders() + { + IEnumerable orderIds = new List(); + using (var conn = new SqlConnection(_settings.ConnectionString)) + { + try + { + _logger.LogInformation("SagaManager Client is trying to connect to database server"); + conn.Open(); + orderIds = conn.Query( + @"SELECT Id FROM [Microsoft.eShopOnContainers.Services.OrderingDb].[ordering].[orders] + WHERE DATEDIFF(hour, [OrderDate], GETDATE()) >= @GracePeriod + AND [OrderStatusId] = 1", + new { GracePeriod = _settings.GracePeriod }); + } + catch (SqlException exception) + { + _logger.LogCritical($"FATAL ERROR: Database connections could not be opened: {exception.Message}"); + } + + } + + return orderIds; + } + + private void Publish(int orderId) + { + var confirmGracePeriodEvent = new ConfirmGracePeriodCommandMsg(orderId); + + // Publish through the Event Bus + _sagaManagerIntegrationEventService.PublishThroughEventBus(confirmGracePeriodEvent); + } + } +} \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/appsettings.json b/src/Services/SagaManager/SagaManager/appsettings.json new file mode 100644 index 000000000..00472e82a --- /dev/null +++ b/src/Services/SagaManager/SagaManager/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;" +}