Merge
This commit is contained in:
commit
9074143577
@ -7,6 +7,11 @@ 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:
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_brand_hilo")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo);
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CatalogBrand");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo);
|
||||
|
||||
b.Property<int>("AvailableStock");
|
||||
|
||||
b.Property<int>("CatalogBrandId");
|
||||
|
||||
b.Property<int>("CatalogTypeId");
|
||||
|
||||
b.Property<string>("Description");
|
||||
|
||||
b.Property<int>("MaxStockThreshold");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<bool>("OnReorder");
|
||||
|
||||
b.Property<string>("PictureUri");
|
||||
|
||||
b.Property<decimal>("Price");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_type_hilo")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo);
|
||||
|
||||
b.Property<string>("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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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<int>(
|
||||
name: "AvailableStock",
|
||||
table: "Catalog",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxStockThreshold",
|
||||
table: "Catalog",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "OnReorder",
|
||||
table: "Catalog",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -42,20 +42,28 @@ namespace Catalog.API.Infrastructure.Migrations
|
||||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo);
|
||||
|
||||
b.Property<int>("AvailableStock");
|
||||
|
||||
b.Property<int>("CatalogBrandId");
|
||||
|
||||
b.Property<int>("CatalogTypeId");
|
||||
|
||||
b.Property<string>("Description");
|
||||
|
||||
b.Property<int>("MaxStockThreshold");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<bool>("OnReorder");
|
||||
|
||||
b.Property<string>("PictureUri");
|
||||
|
||||
b.Property<decimal>("Price");
|
||||
|
||||
b.Property<int>("RestockThreshold");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CatalogBrandId");
|
||||
|
@ -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<ConfirmOrderStockIntegrationEvent>
|
||||
{
|
||||
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<ConfirmedOrderStockItem>();
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<OrderStockItem> OrderStockItems { get; }
|
||||
|
||||
public ConfirmOrderStockIntegrationEvent(int orderId,
|
||||
IEnumerable<OrderStockItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<ConfirmedOrderStockItem> OrderStockItem { get; }
|
||||
|
||||
public OrderStockNotConfirmedIntegrationEvent(int orderId,
|
||||
IEnumerable<ConfirmedOrderStockItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
||||
// 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; }
|
||||
|
||||
/// <summary>
|
||||
/// True if item is on reorder
|
||||
/// </summary>
|
||||
public bool OnReorder { get; set; }
|
||||
|
||||
public CatalogItem() { }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="quantityDesired"></param>
|
||||
/// <returns>int: Returns the number actually removed from stock. </returns>
|
||||
///
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments the quantity of a particular item in inventory.
|
||||
/// <param name="quantity"></param>
|
||||
/// <returns>int: Returns the quantity that has been added to stock</returns>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<OrderStockMethodVerifiedDomainEvent>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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> OrderStockItem { get; }
|
||||
|
||||
public ConfirmOrderStockCommandMsg(int orderId,
|
||||
IEnumerable<OrderStockItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<OrderStockConfirmedIntegrationEvent>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<OrderStockNotConfirmedIntegrationEvent>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<ConfirmedOrderStockItem> OrderStockItems { get; }
|
||||
|
||||
public OrderStockNotConfirmedIntegrationEvent(int orderId,
|
||||
List<ConfirmedOrderStockItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Owned<OrderingContext>> _dbContextFactory;
|
||||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
|
||||
|
||||
public OrderProcessSaga(
|
||||
Func<Owned<OrderingContext>> dbContextFactory, OrderingContext orderingContext,
|
||||
IMediator mediator)
|
||||
IMediator mediator, IOrderingIntegrationEventService orderingIntegrationEventService)
|
||||
: base(orderingContext)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediator = mediator;
|
||||
_orderingIntegrationEventService = orderingIntegrationEventService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -72,17 +79,26 @@ namespace Ordering.API.Application.Sagas
|
||||
/// period has completed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handler which processes the command when
|
||||
|
@ -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();
|
||||
|
@ -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<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
|
||||
services.AddSingleton<IEventBus, EventBusRabbitMQ>();
|
||||
//services.AddTransient<UserCheckoutAcceptedIntegrationEventHandler>();
|
||||
services.AddTransient<IIntegrationEventHandler<ConfirmGracePeriodCommandMsg>, OrderProcessSaga>();
|
||||
services.AddTransient<OrderStockConfirmedIntegrationEventHandler>();
|
||||
services.AddTransient<OrderStockNotConfirmedIntegrationEventHandler>();
|
||||
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<UserCheckoutAcceptedIntegrationEvent,IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>>(
|
||||
() => app.ApplicationServices.GetRequiredService<IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>>());
|
||||
|
||||
eventBus.Subscribe<ConfirmGracePeriodCommandMsg, IIntegrationEventHandler<ConfirmGracePeriodCommandMsg>>
|
||||
(() => app.ApplicationServices.GetRequiredService<IIntegrationEventHandler<ConfirmGracePeriodCommandMsg>>());
|
||||
|
||||
eventBus.Subscribe<OrderStockConfirmedIntegrationEvent, OrderStockConfirmedIntegrationEventHandler>
|
||||
(() => app.ApplicationServices.GetRequiredService<OrderStockConfirmedIntegrationEventHandler>());
|
||||
|
||||
eventBus.Subscribe<OrderStockNotConfirmedIntegrationEvent, OrderStockNotConfirmedIntegrationEventHandler>
|
||||
(() => app.ApplicationServices.GetRequiredService<OrderStockNotConfirmedIntegrationEventHandler>());
|
||||
}
|
||||
|
||||
protected virtual void ConfigureAuth(IApplicationBuilder app)
|
||||
|
@ -46,7 +46,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O
|
||||
_orderItems = new List<OrderItem>();
|
||||
_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)
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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<OrderStatus> List()
|
||||
{
|
||||
return new[] { InProcess, Shipped, Canceled };
|
||||
}
|
||||
public static IEnumerable<OrderStatus> List() =>
|
||||
new[] { Submited, AwaitingValidation, StockValidated, Paid, Shipped, Cancelled };
|
||||
|
||||
public static OrderStatus FromName(string name)
|
||||
{
|
||||
|
@ -0,0 +1,22 @@
|
||||
namespace Ordering.Domain.Events
|
||||
{
|
||||
using MediatR;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// Event used when the order stock items are verified
|
||||
/// </summary>
|
||||
public class OrderStockMethodVerifiedDomainEvent
|
||||
: IAsyncNotification
|
||||
{
|
||||
public int OrderId { get; }
|
||||
public OrderStatus OrderStatus { get; }
|
||||
|
||||
public OrderStockMethodVerifiedDomainEvent(int orderId,
|
||||
OrderStatus orderStatus)
|
||||
{
|
||||
OrderId = orderId;
|
||||
OrderStatus = orderStatus;
|
||||
}
|
||||
}
|
||||
}
|
3
src/Services/SagaManager/SagaManager/.dockerignore
Normal file
3
src/Services/SagaManager/SagaManager/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!obj/Docker/publish/*
|
||||
!obj/Docker/empty/
|
5
src/Services/SagaManager/SagaManager/Dockerfile
Normal file
5
src/Services/SagaManager/SagaManager/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
||||
FROM microsoft/dotnet:1.1-runtime
|
||||
ARG source
|
||||
WORKDIR /app
|
||||
COPY ${source:-obj/Docker/publish} .
|
||||
ENTRYPOINT ["dotnet", "SagaManager.dll"]
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace SagaManager.IntegrationEvents
|
||||
{
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||
|
||||
public interface ISagaManagerIntegrationEventService
|
||||
{
|
||||
void PublishThroughEventBus(IntegrationEvent evt);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
95
src/Services/SagaManager/SagaManager/Program.cs
Normal file
95
src/Services/SagaManager/SagaManager/Program.cs
Normal file
@ -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<ILoggerFactory>();
|
||||
Configure(logger);
|
||||
|
||||
var sagaManagerService = serviceProvider
|
||||
.GetRequiredService<ISagaManagerService>();
|
||||
|
||||
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<SagaManagerSettings>(Configuration)
|
||||
.AddSingleton<ISagaManagerService, SagaManagerService>()
|
||||
.AddSingleton<ISagaManagerIntegrationEventService, SagaManagerIntegrationEventService>()
|
||||
.AddSingleton<IEventBus, EventBusRabbitMQ>()
|
||||
.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>()
|
||||
.AddSingleton<IRabbitMQPersistentConnection>(sp =>
|
||||
{
|
||||
var settings = sp.GetRequiredService<IOptions<SagaManagerSettings>>().Value;
|
||||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
|
||||
var factory = new ConnectionFactory()
|
||||
{
|
||||
HostName = settings.EventBusConnection
|
||||
};
|
||||
|
||||
return new DefaultRabbitMQPersistentConnection(factory, logger);
|
||||
})
|
||||
.AddSingleton<IEventBus, EventBusRabbitMQ>();
|
||||
|
||||
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<IEventBus, EventBusRabbitMQ>();
|
||||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
|
||||
}
|
||||
}
|
||||
}
|
34
src/Services/SagaManager/SagaManager/SagaManager.csproj
Normal file
34
src/Services/SagaManager/SagaManager/SagaManager.csproj
Normal file
@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp1.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="1.50.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\IntegrationEventLogEF\IntegrationEventLogEF.csproj" />
|
||||
<ProjectReference Include="..\..\Ordering\Ordering.Infrastructure\Ordering.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update=".dockerignore">
|
||||
<DependentUpon>Dockerfile</DependentUpon>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
11
src/Services/SagaManager/SagaManager/SagaManagerSettings.cs
Normal file
11
src/Services/SagaManager/SagaManager/SagaManagerSettings.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace SagaManager
|
||||
{
|
||||
public class SagaManagerSettings
|
||||
{
|
||||
public string ConnectionString { get; set; }
|
||||
|
||||
public string EventBusConnection { get; set; }
|
||||
|
||||
public int GracePeriod { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace SagaManager.Services
|
||||
{
|
||||
public interface ISagaManagerService
|
||||
{
|
||||
void CheckFinishedGracePeriodOrders();
|
||||
}
|
||||
}
|
@ -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<SagaManagerService> _logger;
|
||||
|
||||
public SagaManagerService(IOptions<SagaManagerSettings> settings,
|
||||
ISagaManagerIntegrationEventService sagaManagerIntegrationEventService,
|
||||
ILogger<SagaManagerService> logger)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_sagaManagerIntegrationEventService = sagaManagerIntegrationEventService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void CheckFinishedGracePeriodOrders()
|
||||
{
|
||||
var orderIds = GetFinishedGracePeriodOrders();
|
||||
|
||||
foreach (var orderId in orderIds)
|
||||
{
|
||||
Publish(orderId);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<int> GetFinishedGracePeriodOrders()
|
||||
{
|
||||
IEnumerable<int> orderIds = new List<int>();
|
||||
using (var conn = new SqlConnection(_settings.ConnectionString))
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("SagaManager Client is trying to connect to database server");
|
||||
conn.Open();
|
||||
orderIds = conn.Query<int>(
|
||||
@"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);
|
||||
}
|
||||
}
|
||||
}
|
11
src/Services/SagaManager/SagaManager/appsettings.json
Normal file
11
src/Services/SagaManager/SagaManager/appsettings.json
Normal file
@ -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;"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user