diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 94bbfef83..c133de39a 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -7,7 +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: - + graceperiodmanager: + environment: + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word + - EventBusConnection=rabbitmq + basket.api: environment: - ASPNETCORE_ENVIRONMENT=Development @@ -109,6 +113,14 @@ services: ports: - "5107:80" + payment.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:5108 + - EventBusConnection=rabbitmq + ports: + - "5108:80" + locations.api: environment: - ASPNETCORE_ENVIRONMENT=Development @@ -118,4 +130,4 @@ services: - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: - - "5109:80" \ No newline at end of file + - "5109:80" diff --git a/docker-compose.vs.debug.yml b/docker-compose.vs.debug.yml index c2e5e4a06..04799273d 100644 --- a/docker-compose.vs.debug.yml +++ b/docker-compose.vs.debug.yml @@ -122,6 +122,34 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + payment.api: + image: eshop/payment.api:dev + build: + args: + source: ${DOCKER_BUILD_SOURCE} + environment: + - DOTNET_USE_POLLING_FILE_WATCHER=1 + volumes: + - ./src/Services/Payment/Payment.API:/app + - ~/.nuget/packages:/root/.nuget/packages:ro + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + graceperiodmanager: + image: eshop/graceperiodmanager:dev + build: + args: + source: ${DOCKER_BUILD_SOURCE} + volumes: + - ./src/Services/GracePeriod/GracePeriodManager:/app + - ~/.nuget/packages:/root/.nuget/packages:ro + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" + locations.api: image: eshop/locations.api:dev build: diff --git a/docker-compose.vs.release.yml b/docker-compose.vs.release.yml index 87e246780..15bd7e5be 100644 --- a/docker-compose.vs.release.yml +++ b/docker-compose.vs.release.yml @@ -81,6 +81,26 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + payment.api: + build: + args: + source: ${DOCKER_BUILD_SOURCE} + volumes: + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + graceperiodmanager: + build: + args: + source: ${DOCKER_BUILD_SOURCE} + volumes: + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" + locations.api: build: args: diff --git a/docker-compose.yml b/docker-compose.yml index e9fb14179..17284bce1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,16 @@ version: '2' + services: + graceperiodmanager: + image: eshop/graceperiodmanager + build: + context: ./src/Services/GracePeriod/GracePeriodManager + 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 marketing.api: image: eshop/marketing.api @@ -85,7 +95,15 @@ services: image: eshop/webstatus build: context: ./src/Web/WebStatus - dockerfile: Dockerfile + dockerfile: Dockerfile + + payment.api: + image: eshop/payment.api + build: + context: ./src/Services/Payment/Payment.API + dockerfile: Dockerfile + depends_on: + - rabbitmq locations.api: image: locations.api @@ -93,4 +111,4 @@ services: context: ./src/Services/Location/Locations.API dockerfile: Dockerfile depends_on: - - nosql.data \ No newline at end of file + - nosql.data diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index 22b122fe5..78e22a726 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.12 +VisualStudioVersion = 15.0.26430.6 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{932D8224-11F6-4D07-B109-DA28AD288A63}" EndProject @@ -70,11 +70,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resilience", "Resilience", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resilience.Http", "src\BuildingBlocks\Resilience\Resilience.Http\Resilience.Http.csproj", "{D1C47FF1-91F1-4CAF-9ABB-AD642B821502}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Payment", "Payment", "{022E145D-1593-47EE-9608-8E323D3C63F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Payment.API", "src\Services\Payment\Payment.API\Payment.API.csproj", "{1A01AF82-6FCB-464C-B39C-F127AEBD315D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HealthChecks", "src\BuildingBlocks\HealthChecks\src\Microsoft.AspNetCore.HealthChecks\Microsoft.AspNetCore.HealthChecks.csproj", "{22A0F9C1-2D4A-4107-95B7-8459E6688BC5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.HealthChecks.SqlServer", "src\BuildingBlocks\HealthChecks\src\Microsoft.Extensions.HealthChecks.SqlServer\Microsoft.Extensions.HealthChecks.SqlServer.csproj", "{4BD76717-3102-4969-8C2C-BAAA3F0263B6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventBus.Tests", "src\BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}" +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}") = "GracePeriod", "GracePeriod", "{F38B4FF0-0B49-405A-B1B4-F7A5E3BC4C4E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GracePeriodManager", "src\Services\GracePeriod\GracePeriodManager\GracePeriodManager.csproj", "{F6E0F0DD-1400-43C3-B5E0-7CC325728C47}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Location", "Location", "{41139F64-4046-4F16-96B7-D941D96FA9C6}" EndProject @@ -870,6 +878,54 @@ Global {D1C47FF1-91F1-4CAF-9ABB-AD642B821502}.Release|x64.Build.0 = Release|Any CPU {D1C47FF1-91F1-4CAF-9ABB-AD642B821502}.Release|x86.ActiveCfg = Release|Any CPU {D1C47FF1-91F1-4CAF-9ABB-AD642B821502}.Release|x86.Build.0 = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|ARM.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|iPhone.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|x64.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|x64.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|x86.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.AppStore|x86.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|ARM.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|iPhone.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|x64.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Debug|x86.Build.0 = Debug|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|Any CPU.Build.0 = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|ARM.ActiveCfg = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|ARM.Build.0 = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|iPhone.ActiveCfg = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|iPhone.Build.0 = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|x64.ActiveCfg = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|x64.Build.0 = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|x86.ActiveCfg = Release|Any CPU + {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|x86.Build.0 = Release|Any CPU {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU @@ -966,54 +1022,102 @@ Global {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|x64.Build.0 = Release|Any CPU {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|x86.ActiveCfg = Release|Any CPU {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|x86.Build.0 = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|ARM.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|iPhone.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|x64.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|x64.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|x86.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.AppStore|x86.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|ARM.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|ARM.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|iPhone.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|x64.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|x64.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|x86.ActiveCfg = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Debug|x86.Build.0 = Debug|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|Any CPU.Build.0 = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|ARM.ActiveCfg = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|ARM.Build.0 = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|iPhone.ActiveCfg = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|iPhone.Build.0 = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x64.ActiveCfg = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x64.Build.0 = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x86.ActiveCfg = Release|Any CPU - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x86.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|ARM.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|x64.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|x64.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|x86.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|x86.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|ARM.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|ARM.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|iPhone.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|x64.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|x86.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|Any CPU.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|ARM.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|ARM.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|iPhone.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|iPhone.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|x64.ActiveCfg = Release|Any CPU + {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 {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU @@ -1191,18 +1295,18 @@ Global {C0A7918D-B4F2-4E7F-8DE2-1E5279EF079F} = {E279BF0F-7F66-4F3A-A3AB-2CDA66C1CD04} {FBF43D93-F2E7-4FF8-B4AB-186895949B88} = {DB0EFB20-B024-4E5E-A75C-52143C131D25} {D1C47FF1-91F1-4CAF-9ABB-AD642B821502} = {FBF43D93-F2E7-4FF8-B4AB-186895949B88} + {022E145D-1593-47EE-9608-8E323D3C63F5} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} + {1A01AF82-6FCB-464C-B39C-F127AEBD315D} = {022E145D-1593-47EE-9608-8E323D3C63F5} {22A0F9C1-2D4A-4107-95B7-8459E6688BC5} = {A81ECBC2-6B00-4DCD-8388-469174033379} {4BD76717-3102-4969-8C2C-BAAA3F0263B6} = {A81ECBC2-6B00-4DCD-8388-469174033379} - {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} + {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} {41139F64-4046-4F16-96B7-D941D96FA9C6} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} {E7581357-FC34-474C-B8F5-307EE3CE05EF} = {41139F64-4046-4F16-96B7-D941D96FA9C6} {88B22DBB-AA8F-4290-A454-2C109352C345} = {DB0EFB20-B024-4E5E-A75C-52143C131D25} {23A33F9B-7672-426D-ACF9-FF8436ADC81A} = {88B22DBB-AA8F-4290-A454-2C109352C345} {A5260DE0-1FDD-467E-9CC1-A028AB081CEE} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} {DF395F85-B010-465D-857A-7EBCC512C0C2} = {A5260DE0-1FDD-467E-9CC1-A028AB081CEE} - {88B22DBB-AA8F-4290-A454-2C109352C345} = {DB0EFB20-B024-4E5E-A75C-52143C131D25} - {23A33F9B-7672-426D-ACF9-FF8436ADC81A} = {88B22DBB-AA8F-4290-A454-2C109352C345} - {41139F64-4046-4F16-96B7-D941D96FA9C6} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} - {E7581357-FC34-474C-B8F5-307EE3CE05EF} = {41139F64-4046-4F16-96B7-D941D96FA9C6} EndGlobalSection EndGlobal diff --git a/src/BuildingBlocks/CommandBus/CommandBus/CommandBus.csproj b/src/BuildingBlocks/CommandBus/CommandBus/CommandBus.csproj new file mode 100644 index 000000000..ae05359a1 --- /dev/null +++ b/src/BuildingBlocks/CommandBus/CommandBus/CommandBus.csproj @@ -0,0 +1,8 @@ + + + + netstandard1.4 + Microsoft.eShopOnContainers.BuildingBlocks.CommandBus + + + \ No newline at end of file diff --git a/src/BuildingBlocks/CommandBus/CommandBus/ICommandBus.cs b/src/BuildingBlocks/CommandBus/CommandBus/ICommandBus.cs new file mode 100644 index 000000000..2092e11b0 --- /dev/null +++ b/src/BuildingBlocks/CommandBus/CommandBus/ICommandBus.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public interface ICommandBus + { + Task SendAsync(T command) where T : IntegrationCommand; + + } +} diff --git a/src/BuildingBlocks/CommandBus/CommandBus/IntegrationCommand.cs b/src/BuildingBlocks/CommandBus/CommandBus/IntegrationCommand.cs new file mode 100644 index 000000000..36f1f0fdc --- /dev/null +++ b/src/BuildingBlocks/CommandBus/CommandBus/IntegrationCommand.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public abstract class IntegrationCommand + { + public Guid Id { get; private set; } + public DateTime Sent { get; private set; } + + protected IntegrationCommand() + { + Id = Guid.NewGuid(); + Sent = DateTime.UtcNow; + } + } +} diff --git a/src/BuildingBlocks/CommandBus/CommandBusRabbitMQ/CommandBusRabbitMQ.csproj b/src/BuildingBlocks/CommandBus/CommandBusRabbitMQ/CommandBusRabbitMQ.csproj new file mode 100644 index 000000000..954020d10 --- /dev/null +++ b/src/BuildingBlocks/CommandBus/CommandBusRabbitMQ/CommandBusRabbitMQ.csproj @@ -0,0 +1,7 @@ + + + + netstandard1.4 + + + \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/CommandBus/CommandBus.csproj b/src/BuildingBlocks/EventBus/CommandBus/CommandBus.csproj new file mode 100644 index 000000000..7c3327057 --- /dev/null +++ b/src/BuildingBlocks/EventBus/CommandBus/CommandBus.csproj @@ -0,0 +1,8 @@ + + + + netstandard1.0 + Microsoft.eShopOnContainers.BuildingBlocks.CommandBus + + + \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/CommandBus/ICommandBus.cs b/src/BuildingBlocks/EventBus/CommandBus/ICommandBus.cs new file mode 100644 index 000000000..813d9406c --- /dev/null +++ b/src/BuildingBlocks/EventBus/CommandBus/ICommandBus.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public interface ICommandBus + { + void Send(string name, T data); + void Handle(string name, IIntegrationCommandHandler handler); + void Handle(string name, IIntegrationCommandHandler handler); + void Handle(TI handler) + where TI : IIntegrationCommandHandler; + } +} diff --git a/src/BuildingBlocks/EventBus/CommandBus/IIntegrationCommandHandler.cs b/src/BuildingBlocks/EventBus/CommandBus/IIntegrationCommandHandler.cs new file mode 100644 index 000000000..07f0c1eea --- /dev/null +++ b/src/BuildingBlocks/EventBus/CommandBus/IIntegrationCommandHandler.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public interface IIntegrationCommandHandler + { + void Handle(IntegrationCommand command); + } + + public interface IIntegrationCommandHandler : IIntegrationCommandHandler + { + void Handle(IntegrationCommand command); + } +} diff --git a/src/BuildingBlocks/EventBus/CommandBus/IntegrationCommand.cs b/src/BuildingBlocks/EventBus/CommandBus/IntegrationCommand.cs new file mode 100644 index 000000000..8df6e5279 --- /dev/null +++ b/src/BuildingBlocks/EventBus/CommandBus/IntegrationCommand.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public abstract class IntegrationCommand + { + public Guid Id { get; } + public DateTime Sent { get; } + + public abstract object GetDataAsObject(); + + protected IntegrationCommand() + { + Id = Guid.NewGuid(); + Sent = DateTime.UtcNow; + } + + } + + public class IntegrationCommand : IntegrationCommand + { + public T Data { get; } + public string Name { get; } + public override object GetDataAsObject() => Data; + + public IntegrationCommand(string name, T data) : base() + { + Data = data; + Name = name; + } + } + +} diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs index dd5f7f5b4..13ae99afa 100644 --- a/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs @@ -18,7 +18,7 @@ namespace EventBus.Tests public void After_One_Event_Subscription_Should_Contain_The_Event() { var manager = new InMemoryEventBusSubscriptionsManager(); - manager.AddSubscription(() => new TestIntegrationEventHandler()); + manager.AddSubscription(); Assert.True(manager.HasSubscriptionsForEvent()); } @@ -26,7 +26,7 @@ namespace EventBus.Tests public void After_All_Subscriptions_Are_Deleted_Event_Should_No_Longer_Exists() { var manager = new InMemoryEventBusSubscriptionsManager(); - manager.AddSubscription(() => new TestIntegrationEventHandler()); + manager.AddSubscription(); manager.RemoveSubscription(); Assert.False(manager.HasSubscriptionsForEvent()); } @@ -37,7 +37,7 @@ namespace EventBus.Tests bool raised = false; var manager = new InMemoryEventBusSubscriptionsManager(); manager.OnEventRemoved += (o, e) => raised = true; - manager.AddSubscription(() => new TestIntegrationEventHandler()); + manager.AddSubscription(); manager.RemoveSubscription(); Assert.True(raised); } @@ -46,8 +46,8 @@ namespace EventBus.Tests public void Get_Handlers_For_Event_Should_Return_All_Handlers() { var manager = new InMemoryEventBusSubscriptionsManager(); - manager.AddSubscription(() => new TestIntegrationEventHandler()); - manager.AddSubscription(() => new TestIntegrationOtherEventHandler()); + manager.AddSubscription(); + manager.AddSubscription(); var handlers = manager.GetHandlersForEvent(); Assert.Equal(2, handlers.Count()); } diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IDynamicIntegrationEventHandler.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IDynamicIntegrationEventHandler.cs new file mode 100644 index 000000000..55d62ade2 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IDynamicIntegrationEventHandler.cs @@ -0,0 +1,13 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions +{ + public interface IDynamicIntegrationEventHandler + { + Task Handle(dynamic eventData); + } +} diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs index 9ab7a4499..21436d3cd 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs @@ -5,9 +5,15 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions { public interface IEventBus { - void Subscribe(Func handler) + void Subscribe() where T : IntegrationEvent where TH : IIntegrationEventHandler; + void SubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler; + + void UnsubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler; + void Unsubscribe() where TH : IIntegrationEventHandler where T : IntegrationEvent; diff --git a/src/BuildingBlocks/EventBus/EventBus/IEventBusSubscriptionsManager.cs b/src/BuildingBlocks/EventBus/EventBus/IEventBusSubscriptionsManager.cs index 2fdefc039..c83c505b1 100644 --- a/src/BuildingBlocks/EventBus/EventBus/IEventBusSubscriptionsManager.cs +++ b/src/BuildingBlocks/EventBus/EventBus/IEventBusSubscriptionsManager.cs @@ -2,6 +2,7 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using System; using System.Collections.Generic; +using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus { @@ -9,18 +10,25 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus { bool IsEmpty { get; } event EventHandler OnEventRemoved; - void AddSubscription(Func handler) + void AddDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler; + + void AddSubscription() where T : IntegrationEvent where TH : IIntegrationEventHandler; - void RemoveSubscription() - where TH : IIntegrationEventHandler - where T : IntegrationEvent; + void RemoveSubscription() + where TH : IIntegrationEventHandler + where T : IntegrationEvent; + void RemoveDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler; + bool HasSubscriptionsForEvent() where T : IntegrationEvent; bool HasSubscriptionsForEvent(string eventName); Type GetEventTypeByName(string eventName); void Clear(); - IEnumerable GetHandlersForEvent() where T : IntegrationEvent; - IEnumerable GetHandlersForEvent(string eventName); + IEnumerable GetHandlersForEvent() where T : IntegrationEvent; + IEnumerable GetHandlersForEvent(string eventName); + string GetEventKey(); } } \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs b/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs index 11fdba3c5..88be8cf96 100644 --- a/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs +++ b/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs @@ -8,64 +8,106 @@ using System.Text; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus { - public class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager + public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager { - private readonly Dictionary> _handlers; + + + private readonly Dictionary> _handlers; private readonly List _eventTypes; public event EventHandler OnEventRemoved; public InMemoryEventBusSubscriptionsManager() { - _handlers = new Dictionary>(); + _handlers = new Dictionary>(); _eventTypes = new List(); } public bool IsEmpty => !_handlers.Keys.Any(); public void Clear() => _handlers.Clear(); - public void AddSubscription(Func handler) + public void AddDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler + { + DoAddSubscription(typeof(TH), eventName, isDynamic: true); + } + + public void AddSubscription() where T : IntegrationEvent where TH : IIntegrationEventHandler { - var key = GetEventKey(); - if (!HasSubscriptionsForEvent()) + var eventName = GetEventKey(); + DoAddSubscription(typeof(TH), eventName, isDynamic: false); + _eventTypes.Add(typeof(T)); + } + + private void DoAddSubscription(Type handlerType, string eventName, bool isDynamic) + { + if (!HasSubscriptionsForEvent(eventName)) { - _handlers.Add(key, new List()); + _handlers.Add(eventName, new List()); } - _handlers[key].Add(handler); - _eventTypes.Add(typeof(T)); + + if (_handlers[eventName].Any(s => s.HandlerType == handlerType)) + { + throw new ArgumentException( + $"Handler Type {handlerType.Name} already registered for '{eventName}'", nameof(handlerType)); + } + + if (isDynamic) + { + _handlers[eventName].Add(SubscriptionInfo.Dynamic(handlerType)); + } + else + { + _handlers[eventName].Add(SubscriptionInfo.Typed(handlerType)); + } + } + + + public void RemoveDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler + { + var handlerToRemove = FindDynamicSubscriptionToRemove(eventName); + DoRemoveHandler(eventName, handlerToRemove); } + public void RemoveSubscription() where TH : IIntegrationEventHandler where T : IntegrationEvent { - var handlerToRemove = FindHandlerToRemove(); - if (handlerToRemove != null) + var handlerToRemove = FindSubscriptionToRemove(); + var eventName = GetEventKey(); + DoRemoveHandler(eventName, handlerToRemove); + } + + + private void DoRemoveHandler(string eventName, SubscriptionInfo subsToRemove) + { + if (subsToRemove != null) { - var key = GetEventKey(); - _handlers[key].Remove(handlerToRemove); - if (!_handlers[key].Any()) + _handlers[eventName].Remove(subsToRemove); + if (!_handlers[eventName].Any()) { - _handlers.Remove(key); - var eventType = _eventTypes.SingleOrDefault(e => e.Name == key); + _handlers.Remove(eventName); + var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); if (eventType != null) { _eventTypes.Remove(eventType); - RaiseOnEventRemoved(eventType.Name); } + RaiseOnEventRemoved(eventName); } - + } } - public IEnumerable GetHandlersForEvent() where T : IntegrationEvent + public IEnumerable GetHandlersForEvent() where T : IntegrationEvent { var key = GetEventKey(); return GetHandlersForEvent(key); } - public IEnumerable GetHandlersForEvent(string eventName) => _handlers[eventName]; + public IEnumerable GetHandlersForEvent(string eventName) => _handlers[eventName]; private void RaiseOnEventRemoved(string eventName) { @@ -76,26 +118,31 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus } } - private Delegate FindHandlerToRemove() - where T : IntegrationEvent - where TH : IIntegrationEventHandler + + private SubscriptionInfo FindDynamicSubscriptionToRemove(string eventName) + where TH : IDynamicIntegrationEventHandler + { + return DoFindSubscriptionToRemove(eventName, typeof(TH)); + } + + + private SubscriptionInfo FindSubscriptionToRemove() + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = GetEventKey(); + return DoFindSubscriptionToRemove(eventName, typeof(TH)); + } + + private SubscriptionInfo DoFindSubscriptionToRemove(string eventName, Type handlerType) { - if (!HasSubscriptionsForEvent()) + if (!HasSubscriptionsForEvent(eventName)) { return null; } - var key = GetEventKey(); - foreach (var func in _handlers[key]) - { - var genericArgs = func.GetType().GetGenericArguments(); - if (genericArgs.SingleOrDefault() == typeof(TH)) - { - return func; - } - } + return _handlers[eventName].SingleOrDefault(s => s.HandlerType == handlerType); - return null; } public bool HasSubscriptionsForEvent() where T : IntegrationEvent @@ -104,10 +151,10 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus return HasSubscriptionsForEvent(key); } public bool HasSubscriptionsForEvent(string eventName) => _handlers.ContainsKey(eventName); - - public Type GetEventTypeByName(string eventName) => _eventTypes.Single(t => t.Name == eventName); - private string GetEventKey() + public Type GetEventTypeByName(string eventName) => _eventTypes.SingleOrDefault(t => t.Name == eventName); + + public string GetEventKey() { return typeof(T).Name; } diff --git a/src/BuildingBlocks/EventBus/EventBus/SubscriptionInfo.cs b/src/BuildingBlocks/EventBus/EventBus/SubscriptionInfo.cs new file mode 100644 index 000000000..a20b3031c --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus/SubscriptionInfo.cs @@ -0,0 +1,28 @@ +using System; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus +{ + public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager + { + public class SubscriptionInfo + { + public bool IsDynamic { get; } + public Type HandlerType{ get; } + + private SubscriptionInfo(bool isDynamic, Type handlerType) + { + IsDynamic = isDynamic; + HandlerType = handlerType; + } + + public static SubscriptionInfo Dynamic(Type handlerType) + { + return new SubscriptionInfo(true, handlerType); + } + public static SubscriptionInfo Typed(Type handlerType) + { + return new SubscriptionInfo(false, handlerType); + } + } + } +} diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/CommandBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/CommandBusRabbitMQ.cs new file mode 100644 index 000000000..27e67d9ca --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/CommandBusRabbitMQ.cs @@ -0,0 +1,145 @@ +//using Microsoft.eShopOnContainers.BuildingBlocks.CommandBus; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Polly; +using Polly.Retry; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +/* +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ +{ + public class CommandBusRabbitMQ : ICommandBus, IDisposable + { + const string BROKER_NAME = "eshop_command_bus"; + + private readonly IRabbitMQPersistentConnection _persistentConnection; + private readonly ILogger _logger; + + private IModel _consumerChannel; + private string _queueName; + + private readonly Dictionary _handlers; + private readonly Dictionary _typeMappings; + + public CommandBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, + ILogger logger) + { + _logger = logger; + _persistentConnection = persistentConnection; + _handlers = new Dictionary(); + _typeMappings = new Dictionary(); + } + + public void Send(string name, T data) + { + Send(new IntegrationCommand(name, data)); + } + + public void Handle(string name, IIntegrationCommandHandler handler) + { + _handlers.Add(name, handler); + _typeMappings.Add(name, typeof(TC)); + } + + public void Handle(string name, IIntegrationCommandHandler handler) + { + _handlers.Add(name, handler); + } + public void Handle(TI handler) where TI : IIntegrationCommandHandler + { + var name = typeof(TI).Name; + _handlers.Add(name, handler); + _typeMappings.Add(name, typeof(TC)); + } + + private void Send(IntegrationCommand command) + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + var policy = RetryPolicy.Handle() + .Or() + .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + }); + + using (var channel = _persistentConnection.CreateModel()) + { + var commandName = command.Name; + channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); + var message = JsonConvert.SerializeObject(command); + var body = Encoding.UTF8.GetBytes(message); + policy.Execute(() => + { + channel.BasicPublish(exchange: BROKER_NAME, + routingKey: commandName, + basicProperties: null, + body: body); + }); + } + } + + private IModel CreateConsumerChannel() + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + var channel = _persistentConnection.CreateModel(); + + channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); + _queueName = channel.QueueDeclare().QueueName; + var consumer = new EventingBasicConsumer(channel); + consumer.Received += async (model, ea) => + { + var commandName = ea.RoutingKey; + var message = Encoding.UTF8.GetString(ea.Body); + await InvokeHandler(commandName, message); + }; + + channel.BasicConsume(queue: _queueName, + noAck: true, + consumer: consumer); + + channel.CallbackException += (sender, ea) => + { + _consumerChannel.Dispose(); + _consumerChannel = CreateConsumerChannel(); + }; + + return channel; + } + + private Task InvokeHandler(string commandName, string message) + { + if (_handlers.ContainsKey(commandName)) + { + + } + + } + + public void Dispose() + { + if (_consumerChannel != null) + { + _consumerChannel.Dispose(); + } + } + + + + } +} +*/ \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs index adbc52ad1..07a130f22 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs @@ -1,8 +1,10 @@ -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using Autofac; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Polly; using Polly.Retry; using RabbitMQ.Client; @@ -25,17 +27,20 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ private readonly IRabbitMQPersistentConnection _persistentConnection; private readonly ILogger _logger; private readonly IEventBusSubscriptionsManager _subsManager; - + private readonly ILifetimeScope _autofac; + private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; private IModel _consumerChannel; private string _queueName; - public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger logger, IEventBusSubscriptionsManager subsManager) + public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger logger, + ILifetimeScope autofac, IEventBusSubscriptionsManager subsManager) { _persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); _consumerChannel = CreateConsumerChannel(); + _autofac = autofac; _subsManager.OnEventRemoved += SubsManager_OnEventRemoved; } @@ -96,12 +101,25 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ } } - public void Subscribe(Func handler) + public void SubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler + { + DoInternalSubscription(eventName); + _subsManager.AddDynamicSubscription(eventName); + } + + public void Subscribe() where T : IntegrationEvent where TH : IIntegrationEventHandler { - var eventName = typeof(T).Name; - var containsKey = _subsManager.HasSubscriptionsForEvent(); + var eventName = _subsManager.GetEventKey(); + DoInternalSubscription(eventName); + _subsManager.AddSubscription(); + } + + private void DoInternalSubscription(string eventName) + { + var containsKey = _subsManager.HasSubscriptionsForEvent(eventName); if (!containsKey) { if (!_persistentConnection.IsConnected) @@ -116,9 +134,6 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ routingKey: eventName); } } - - _subsManager.AddSubscription(handler); - } public void Unsubscribe() @@ -128,19 +143,13 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ _subsManager.RemoveSubscription(); } - private static Func FindHandlerByType(Type handlerType, IEnumerable> handlers) + public void UnsubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler { - foreach (var func in handlers) - { - if (func.GetMethodInfo().ReturnType == handlerType) - { - return func; - } - } - - return null; + _subsManager.RemoveDynamicSubscription(eventName); } + public void Dispose() { if (_consumerChannel != null) @@ -190,17 +199,29 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ private async Task ProcessEvent(string eventName, string message) { - if (_subsManager.HasSubscriptionsForEvent(eventName)) - { - var eventType = _subsManager.GetEventTypeByName(eventName); - var integrationEvent = JsonConvert.DeserializeObject(message, eventType); - var handlers = _subsManager.GetHandlersForEvent(eventName); - foreach (var handlerfactory in handlers) + if (_subsManager.HasSubscriptionsForEvent(eventName)) + { + using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) { - var handler = handlerfactory.DynamicInvoke(); - var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); - await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); + var subscriptions = _subsManager.GetHandlersForEvent(eventName); + foreach (var subscription in subscriptions) + { + if (subscription.IsDynamic) + { + var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; + dynamic eventData = JObject.Parse(message); + await handler.Handle(eventData); + } + else + { + var eventType = _subsManager.GetEventTypeByName(eventName); + var integrationEvent = JsonConvert.DeserializeObject(message, eventType); + var handler = scope.ResolveOptional(subscription.HandlerType); + var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); + await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); + } + } } } } diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj index 0c2a7a4df..d7e3a58d8 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Basket/BasketCheckout.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Basket/BasketCheckout.cs new file mode 100644 index 000000000..e6b7aaea6 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Basket/BasketCheckout.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace eShopOnContainers.Core.Models.Basket +{ + public class BasketCheckout + { + [Required] + public string City { get; set; } + [Required] + public string Street { get; set; } + [Required] + public string State { get; set; } + [Required] + public string Country { get; set; } + + public string ZipCode { get; set; } + [Required] + public string CardNumber { get; set; } + [Required] + public string CardHolderName { get; set; } + + [Required] + public DateTime CardExpiration { get; set; } + + [Required] + public string CardSecurityNumber { get; set; } + + public int CardTypeId { get; set; } + + public string Buyer { get; set; } + + [Required] + public Guid RequestId { get; set; } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/BasketService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/BasketService.cs index c3607c1e3..b7b36d984 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/BasketService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/BasketService.cs @@ -42,6 +42,15 @@ namespace eShopOnContainers.Core.Services.Basket return result; } + public async Task CheckoutAsync(BasketCheckout basketCheckout, string token) + { + UriBuilder builder = new UriBuilder(GlobalSetting.Instance.BasketEndpoint + "/checkout"); + + string uri = builder.ToString(); + + await _requestProvider.PostAsync(uri, basketCheckout, token); + } + public async Task ClearBasketAsync(string guidUser, string token) { UriBuilder builder = new UriBuilder(GlobalSetting.Instance.BasketEndpoint); diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/IBasketService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/IBasketService.cs index 26b785acc..f77080e6d 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/IBasketService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/IBasketService.cs @@ -7,6 +7,7 @@ namespace eShopOnContainers.Core.Services.Basket { Task GetBasketAsync(string guidUser, string token); Task UpdateBasketAsync(CustomerBasket customerBasket, string token); + Task CheckoutAsync(BasketCheckout basketCheckout, string token); Task ClearBasketAsync(string guidUser, string token); } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/IOrderService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/IOrderService.cs index 9636782ee..f13520463 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/IOrderService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/IOrderService.cs @@ -1,13 +1,15 @@ -using System.Collections.ObjectModel; +using eShopOnContainers.Core.Models.Basket; +using System.Collections.ObjectModel; using System.Threading.Tasks; namespace eShopOnContainers.Core.Services.Order { public interface IOrderService { - Task CreateOrderAsync(Models.Orders.Order newOrder, string token); + //Task CreateOrderAsync(Models.Orders.Order newOrder, string token); Task> GetOrdersAsync(string token); Task GetOrderAsync(int orderId, string token); Task> GetCardTypesAsync(string token); + BasketCheckout MapOrderToBasket(Models.Orders.Order order); } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/OrderMockService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/OrderMockService.cs index 6a2279500..e2287918c 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/OrderMockService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/OrderMockService.cs @@ -1,4 +1,5 @@ using eShopOnContainers.Core.Extensions; +using eShopOnContainers.Core.Models.Basket; using eShopOnContainers.Core.Models.Orders; using eShopOnContainers.Core.Models.User; using System; @@ -64,17 +65,18 @@ namespace eShopOnContainers.Core.Services.Order new CardType { Id = 3, Name = "MasterCard" }, }; - public async Task CreateOrderAsync(Models.Orders.Order newOrder, string token) + private static BasketCheckout MockBasketCheckout = new BasketCheckout() { - await Task.Delay(500); - - if (!string.IsNullOrEmpty(token)) - { - newOrder.OrderNumber = string.Format("{0}", MockOrders.Count + 1); - - MockOrders.Insert(0, newOrder); - } - } + CardExpiration = DateTime.UtcNow, + CardHolderName = "FakeCardHolderName", + CardNumber = "122333423224", + CardSecurityNumber = "1234", + CardTypeId = 1, + City = "FakeCity", + Country = "FakeCountry", + ZipCode = "FakeZipCode", + Street = "FakeStreet" + }; public async Task> GetOrdersAsync(string token) { @@ -111,5 +113,10 @@ namespace eShopOnContainers.Core.Services.Order else return new ObservableCollection(); } + + public BasketCheckout MapOrderToBasket(Models.Orders.Order order) + { + return MockBasketCheckout; + } } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/OrderService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/OrderService.cs index 0005bfd22..2c95c62b6 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/OrderService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Order/OrderService.cs @@ -1,4 +1,5 @@ -using eShopOnContainers.Core.Services.RequestProvider; +using eShopOnContainers.Core.Models.Basket; +using eShopOnContainers.Core.Services.RequestProvider; using System; using System.Collections.ObjectModel; using System.Threading.Tasks; @@ -14,17 +15,6 @@ namespace eShopOnContainers.Core.Services.Order _requestProvider = requestProvider; } - public async Task CreateOrderAsync(Models.Orders.Order newOrder, string token) - { - UriBuilder builder = new UriBuilder(GlobalSetting.Instance.OrdersEndpoint); - - builder.Path = "api/v1/orders/new"; - - string uri = builder.ToString(); - - await _requestProvider.PostAsync(uri, newOrder, token, "x-requestid"); - } - public async Task> GetOrdersAsync(string token) { @@ -82,5 +72,21 @@ namespace eShopOnContainers.Core.Services.Order return new ObservableCollection(); } } + + public BasketCheckout MapOrderToBasket(Models.Orders.Order order) + { + return new BasketCheckout() + { + CardExpiration = order.CardExpiration, + CardHolderName = order.CardHolderName, + CardNumber = order.CardNumber, + CardSecurityNumber = order.CardSecurityNumber, + CardTypeId = order.CardTypeId, + City = order.ShippingCity, + Country = order.ShippingCountry, + ZipCode = order.ShippingZipCode, + Street = order.ShippingStreet + }; + } } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/CheckoutViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/CheckoutViewModel.cs index 4ac73a5c1..5e121e1f4 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/CheckoutViewModel.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/CheckoutViewModel.cs @@ -132,8 +132,10 @@ namespace eShopOnContainers.Core.ViewModels { var authToken = Settings.AuthAccessToken; - // Create new order - await _orderService.CreateOrderAsync(Order, authToken); + var basket = _orderService.MapOrderToBasket(Order); + + // Create basket checkout + await _basketService.CheckoutAsync(basket, authToken); // Clean Basket await _basketService.ClearBasketAsync(_shippingAddress.Id.ToString(), authToken); diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj index 00ba88226..e22341f05 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj @@ -64,6 +64,7 @@ + @@ -262,6 +263,11 @@ MSBuild:UpdateDesignTimeXaml + + + ..\..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.6\Profile\Profile44\System.ComponentModel.Annotations.dll + + diff --git a/src/Services/Basket/Basket.API/Basket.API.csproj b/src/Services/Basket/Basket.API/Basket.API.csproj index 18efb9393..5287dcc96 100644 --- a/src/Services/Basket/Basket.API/Basket.API.csproj +++ b/src/Services/Basket/Basket.API/Basket.API.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Services/Basket/Basket.API/Controllers/BasketController.cs b/src/Services/Basket/Basket.API/Controllers/BasketController.cs index e468a8864..1d00f5f00 100644 --- a/src/Services/Basket/Basket.API/Controllers/BasketController.cs +++ b/src/Services/Basket/Basket.API/Controllers/BasketController.cs @@ -5,6 +5,11 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Basket.API.IntegrationEvents.Events; +using Microsoft.eShopOnContainers.Services.Basket.API.Services; +using Basket.API.Model; namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers { @@ -12,11 +17,17 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers [Authorize] public class BasketController : Controller { - private IBasketRepository _repository; + private readonly IBasketRepository _repository; + private readonly IIdentityService _identitySvc; + private readonly IEventBus _eventBus; - public BasketController(IBasketRepository repository) + public BasketController(IBasketRepository repository, + IIdentityService identityService, + IEventBus eventBus) { _repository = repository; + _identitySvc = identityService; + _eventBus = eventBus; } // GET /id [HttpGet("{id}")] @@ -36,11 +47,38 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers return Ok(basket); } - // DELETE /id + [Route("checkout")] + [HttpPost] + public async Task Checkout([FromBody]BasketCheckout basketCheckout, [FromHeader(Name = "x-requestid")] string requestId) + { + var userId = _identitySvc.GetUserIdentity(); + basketCheckout.RequestId = (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty) ? + guid : basketCheckout.RequestId; + + var basket = await _repository.GetBasketAsync(userId); + var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, basketCheckout.City, basketCheckout.Street, + basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName, + basketCheckout.CardExpiration, basketCheckout.CardSecurityNumber, basketCheckout.CardTypeId, basketCheckout.Buyer, basketCheckout.RequestId, basket); + + // Once basket is checkout, sends an integration event to + // ordering.api to convert basket to order and proceeds with + // order creation process + _eventBus.Publish(eventMessage); + + if (basket == null) + { + return BadRequest(); + } + + return Accepted(); + } + + // DELETE api/values/5 [HttpDelete("{id}")] public void Delete(string id) { _repository.DeleteBasketAsync(id); } + } } diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs index 19ae1b594..f6e7de1b0 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs @@ -1,5 +1,6 @@ using Basket.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using Microsoft.eShopOnContainers.Services.Basket.API.Model; using System; using System.Threading.Tasks; diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs index a32ad0beb..b78d10c05 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs @@ -7,9 +7,9 @@ namespace Basket.API.IntegrationEvents.Events // An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems. public class OrderStartedIntegrationEvent : IntegrationEvent { - public string UserId { get; } + public string UserId { get; set; } - public OrderStartedIntegrationEvent(string userId) => - UserId = userId; + public OrderStartedIntegrationEvent(string userId) + => UserId = userId; } } diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs new file mode 100644 index 000000000..4c02612c5 --- /dev/null +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs @@ -0,0 +1,64 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Microsoft.eShopOnContainers.Services.Basket.API.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Basket.API.IntegrationEvents.Events +{ + public class UserCheckoutAcceptedIntegrationEvent : IntegrationEvent + { + public string UserId { get; } + + public int OrderNumber { get; set; } + + public string City { get; set; } + + public string Street { get; set; } + + public string State { get; set; } + + public string Country { get; set; } + + public string ZipCode { get; set; } + + public string CardNumber { get; set; } + + public string CardHolderName { get; set; } + + public DateTime CardExpiration { get; set; } + + public string CardSecurityNumber { get; set; } + + public int CardTypeId { get; set; } + + public string Buyer { get; set; } + + public Guid RequestId { get; set; } + + public CustomerBasket Basket { get; } + + public UserCheckoutAcceptedIntegrationEvent(string userId, string city, string street, + string state, string country, string zipCode, string cardNumber, string cardHolderName, + DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId, + CustomerBasket basket) + { + UserId = userId; + City = city; + Street = street; + State = state; + Country = country; + ZipCode = zipCode; + CardNumber = cardNumber; + CardHolderName = cardHolderName; + CardExpiration = cardExpiration; + CardSecurityNumber = cardSecurityNumber; + CardTypeId = cardTypeId; + Buyer = buyer; + Basket = basket; + RequestId = requestId; + } + + } +} diff --git a/src/Services/Basket/Basket.API/Model/Basket.cs b/src/Services/Basket/Basket.API/Model/Basket.cs index 7f43b2a4b..3dd034f6c 100644 --- a/src/Services/Basket/Basket.API/Model/Basket.cs +++ b/src/Services/Basket/Basket.API/Model/Basket.cs @@ -10,7 +10,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model public CustomerBasket(string customerId) { BuyerId = customerId; - Items = new List(); + Items = new List(); } } } diff --git a/src/Services/Basket/Basket.API/Model/BasketCheckout.cs b/src/Services/Basket/Basket.API/Model/BasketCheckout.cs new file mode 100644 index 000000000..5241a3672 --- /dev/null +++ b/src/Services/Basket/Basket.API/Model/BasketCheckout.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Basket.API.Model +{ + public class BasketCheckout + { + public string City { get; set; } + + public string Street { get; set; } + + public string State { get; set; } + + public string Country { get; set; } + + public string ZipCode { get; set; } + + public string CardNumber { get; set; } + + public string CardHolderName { get; set; } + + public DateTime CardExpiration { get; set; } + + public string CardSecurityNumber { get; set; } + + public int CardTypeId { get; set; } + + public string Buyer { get; set; } + + public Guid RequestId { get; set; } + } +} + diff --git a/src/Services/Basket/Basket.API/Services/IIdentityService.cs b/src/Services/Basket/Basket.API/Services/IIdentityService.cs new file mode 100644 index 000000000..8cc7bd848 --- /dev/null +++ b/src/Services/Basket/Basket.API/Services/IIdentityService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Basket.API.Services +{ + public interface IIdentityService + { + string GetUserIdentity(); + } +} diff --git a/src/Services/Basket/Basket.API/Services/IdentityService.cs b/src/Services/Basket/Basket.API/Services/IdentityService.cs new file mode 100644 index 000000000..08d1ffffa --- /dev/null +++ b/src/Services/Basket/Basket.API/Services/IdentityService.cs @@ -0,0 +1,24 @@ + +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Basket.API.Services +{ + public class IdentityService : IIdentityService + { + private IHttpContextAccessor _context; + + public IdentityService(IHttpContextAccessor context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public string GetUserIdentity() + { + return _context.HttpContext.User.FindFirst("sub").Value; + } + } +} diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs index b2fb838d6..83b259d5b 100644 --- a/src/Services/Basket/Basket.API/Startup.cs +++ b/src/Services/Basket/Basket.API/Startup.cs @@ -1,8 +1,11 @@ -using Basket.API.Infrastructure.Filters; +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Basket.API.Infrastructure.Filters; using Basket.API.IntegrationEvents.EventHandling; using Basket.API.IntegrationEvents.Events; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; @@ -10,6 +13,7 @@ using Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server; using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Basket.API.Model; +using Microsoft.eShopOnContainers.Services.Basket.API.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.HealthChecks; @@ -17,10 +21,10 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RabbitMQ.Client; using StackExchange.Redis; +using System; using System.Linq; using System.Net; using System.Threading.Tasks; -using System; namespace Microsoft.eShopOnContainers.Services.Basket.API { @@ -39,7 +43,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API public IConfigurationRoot Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) + public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddHealthChecks(checks => @@ -102,9 +106,15 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API .AllowAnyHeader() .AllowCredentials()); }); - + services.AddSingleton(); services.AddTransient(); + services.AddTransient(); RegisterServiceBus(services); + services.AddOptions(); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); } private void RegisterServiceBus(IServiceCollection services) @@ -114,7 +124,6 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API services.AddTransient(); services.AddTransient(); - } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -155,19 +164,9 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API protected virtual void ConfigureEventBus(IApplicationBuilder app) { - var catalogPriceHandler = app.ApplicationServices - .GetService>(); - - var orderStartedHandler = app.ApplicationServices - .GetService>(); - var eventBus = app.ApplicationServices.GetRequiredService(); - - eventBus.Subscribe - (() => app.ApplicationServices.GetRequiredService()); - - eventBus.Subscribe - (() => app.ApplicationServices.GetRequiredService()); + eventBus.Subscribe(); + eventBus.Subscribe(); } } } diff --git a/src/Services/Catalog/Catalog.API/Catalog.API.csproj b/src/Services/Catalog/Catalog.API/Catalog.API.csproj index e965440a6..8f30f7ca3 100644 --- a/src/Services/Catalog/Catalog.API/Catalog.API.csproj +++ b/src/Services/Catalog/Catalog.API/Catalog.API.csproj @@ -2,9 +2,13 @@ netcoreapp1.1 - 1.1.2 + portable + true + Catalog.API Exe + Catalog.API aspnet-Catalog.API-20161122013618 + 1.1.2 $(PackageTargetFallback);dotnet5.6;portable-net45+win8 ..\..\..\..\docker-compose.dcproj @@ -25,12 +29,12 @@ + - @@ -46,7 +50,6 @@ - diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs index 5f2370fb6..9752b46db 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs @@ -70,18 +70,18 @@ { return new List() { - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/1" }, - new CatalogItem() { CatalogTypeId=1,CatalogBrandId=2, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/2" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/3" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/4" }, - new CatalogItem() { CatalogTypeId=3,CatalogBrandId=5, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/5" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/6" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/7" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/8" }, - new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup White Mug", Name = "Cup White Mug", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/9" }, - new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/10" }, - new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup Sheet", Name = "Cup Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/12" } + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/1", AvailableStock = 1}, + new CatalogItem() { CatalogTypeId=1,CatalogBrandId=2, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/2", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/3", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/4", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=3,CatalogBrandId=5, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/5", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/6", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/7", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/8", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup White Mug", Name = "Cup White Mug", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/9", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/10", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup Sheet", Name = "Cup Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11", AvailableStock = 1 }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/12", AvailableStock = 1 } }; } } 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/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs new file mode 100644 index 000000000..0f30a3e0a --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs @@ -0,0 +1,46 @@ +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.IntegrationEvents; + using IntegrationEvents.Events; + + public class OrderStatusChangedToAwaitingValidationIntegrationEventHandler : + IIntegrationEventHandler + { + private readonly CatalogContext _catalogContext; + private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; + + public OrderStatusChangedToAwaitingValidationIntegrationEventHandler(CatalogContext catalogContext, + ICatalogIntegrationEventService catalogIntegrationEventService) + { + _catalogContext = catalogContext; + _catalogIntegrationEventService = catalogIntegrationEventService; + } + + public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent command) + { + var confirmedOrderStockItems = new List(); + + foreach (var orderStockItem in command.OrderStockItems) + { + var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); + var hasStock = catalogItem.AvailableStock >= orderStockItem.Units; + var confirmedOrderStockItem = new ConfirmedOrderStockItem(catalogItem.Id, hasStock); + + confirmedOrderStockItems.Add(confirmedOrderStockItem); + } + + var confirmedIntegrationEvent = confirmedOrderStockItems.Any(c => !c.HasStock) + ? (IntegrationEvent) new OrderStockRejectedIntegrationEvent(command.OrderId, confirmedOrderStockItems) + : new OrderStockConfirmedIntegrationEvent(command.OrderId); + + await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(confirmedIntegrationEvent); + await _catalogIntegrationEventService.PublishThroughEventBusAsync(confirmedIntegrationEvent); + } + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs new file mode 100644 index 000000000..0a45547f9 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs @@ -0,0 +1,31 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling +{ + using BuildingBlocks.EventBus.Abstractions; + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; + + public class OrderStatusChangedToPaidIntegrationEventHandler : + IIntegrationEventHandler + { + private readonly CatalogContext _catalogContext; + + public OrderStatusChangedToPaidIntegrationEventHandler(CatalogContext catalogContext) + { + _catalogContext = catalogContext; + } + + public async Task Handle(OrderStatusChangedToPaidIntegrationEvent command) + { + //we're not blocking stock/inventory + foreach (var orderStockItem in command.OrderStockItems) + { + var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); + + catalogItem.RemoveStock(orderStockItem.Units); + } + + await _catalogContext.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs new file mode 100644 index 000000000..9787aaedd --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs @@ -0,0 +1,30 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events +{ + using BuildingBlocks.EventBus.Events; + using System.Collections.Generic; + + public class OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + public IEnumerable OrderStockItems { get; } + + public OrderStatusChangedToAwaitingValidationIntegrationEvent(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/OrderStatusChangedToPaidIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs new file mode 100644 index 000000000..881aa21fe --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs @@ -0,0 +1,18 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events +{ + using System.Collections.Generic; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + public IEnumerable OrderStockItems { get; } + + public OrderStatusChangedToPaidIntegrationEvent(int orderId, + IEnumerable orderStockItems) + { + OrderId = orderId; + OrderStockItems = orderStockItems; + } + } +} \ 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..b91eaae43 --- /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 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/OrderStockRejectedIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs new file mode 100644 index 000000000..eb74af8dd --- /dev/null +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs @@ -0,0 +1,31 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events +{ + using BuildingBlocks.EventBus.Events; + using System.Collections.Generic; + + public class OrderStockRejectedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public List OrderStockItems { get; } + + public OrderStockRejectedIntegrationEvent(int orderId, + List orderStockItems) + { + OrderId = orderId; + OrderStockItems = orderStockItems; + } + } + + public class ConfirmedOrderStockItem + { + public int ProductId { get; } + public bool HasStock { get; } + + public ConfirmedOrderStockItem(int productId, bool hasStock) + { + ProductId = productId; + HasStock = hasStock; + } + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs index 10b8317da..7c14a07d1 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs @@ -1,10 +1,7 @@ -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events { + using 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. @@ -23,4 +20,4 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Eve OldPrice = oldPrice; } } -} +} \ 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/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index ddb7edd6f..c5784b6a4 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -1,5 +1,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API { + using Autofac; + using Autofac.Extensions.DependencyInjection; using global::Catalog.API.Infrastructure.Filters; using global::Catalog.API.IntegrationEvents; using Microsoft.AspNetCore.Builder; @@ -12,6 +14,8 @@ using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; + using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; + using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.HealthChecks; @@ -46,7 +50,7 @@ Configuration = builder.Build(); } - public void ConfigureServices(IServiceCollection services) + public IServiceProvider ConfigureServices(IServiceCollection services) { // Add framework services. @@ -122,8 +126,11 @@ return new DefaultRabbitMQPersistentConnection(factory, logger); }); - services.AddSingleton(); - services.AddSingleton(); + RegisterServiceBus(services); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) @@ -148,6 +155,8 @@ WaitForSqlAvailabilityAsync(context, loggerFactory, app).Wait(); + ConfigureEventBus(app); + var integrationEventLogContext = new IntegrationEventLogContext( new DbContextOptionsBuilder() .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Catalog.API")) @@ -179,5 +188,26 @@ } ); } + + private void RegisterServiceBus(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient, + OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); + services.AddTransient, + OrderStatusChangedToPaidIntegrationEventHandler>(); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + + eventBus.Subscribe>(); + eventBus.Subscribe>(); + } } } diff --git a/src/Services/GracePeriod/GracePeriodManager/.dockerignore b/src/Services/GracePeriod/GracePeriodManager/.dockerignore new file mode 100644 index 000000000..d8f8175f6 --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/.dockerignore @@ -0,0 +1,3 @@ +* +!obj/Docker/publish/* +!obj/Docker/empty/ diff --git a/src/Services/GracePeriod/GracePeriodManager/Dockerfile b/src/Services/GracePeriod/GracePeriodManager/Dockerfile new file mode 100644 index 000000000..37971fde2 --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/Dockerfile @@ -0,0 +1,5 @@ +FROM microsoft/aspnetcore:1.1.2 +ARG source +WORKDIR /app +COPY ${source:-obj/Docker/publish} . +ENTRYPOINT ["dotnet", "GracePeriodManager.dll"] \ No newline at end of file diff --git a/src/Services/GracePeriod/GracePeriodManager/GracePeriodManager.csproj b/src/Services/GracePeriod/GracePeriodManager/GracePeriodManager.csproj new file mode 100644 index 000000000..d057a6b54 --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/GracePeriodManager.csproj @@ -0,0 +1,36 @@ + + + + Exe + netcoreapp1.1 + 1.1.2 + + + + + + + + + + + + + + + + + + + + + + + + + + Dockerfile + + + + \ No newline at end of file diff --git a/src/Services/GracePeriod/GracePeriodManager/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs b/src/Services/GracePeriod/GracePeriodManager/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..fce04cd24 --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs @@ -0,0 +1,11 @@ +namespace GracePeriodManager.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class GracePeriodConfirmedIntegrationEvent : IntegrationEvent + { + public int OrderId { get;} + + public GracePeriodConfirmedIntegrationEvent(int orderId) => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/GracePeriod/GracePeriodManager/ManagerSettings.cs b/src/Services/GracePeriod/GracePeriodManager/ManagerSettings.cs new file mode 100644 index 000000000..44e0b56f7 --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/ManagerSettings.cs @@ -0,0 +1,13 @@ +namespace GracePeriodManager +{ + public class ManagerSettings + { + public string ConnectionString { get; set; } + + public string EventBusConnection { get; set; } + + public int GracePeriodTime { get; set; } + + public int CheckUpdateTime { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/GracePeriod/GracePeriodManager/Program.cs b/src/Services/GracePeriod/GracePeriodManager/Program.cs new file mode 100644 index 000000000..eecdd80ea --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/Program.cs @@ -0,0 +1,94 @@ +namespace GracePeriodManager +{ + using System.IO; + using System; + using System.Threading.Tasks; + using Autofac.Extensions.DependencyInjection; + using Autofac; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; + 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) => MainAsync().Wait(); + + static async Task MainAsync() + { + StartUp(); + + IServiceCollection services = new ServiceCollection(); + var serviceProvider = ConfigureServices(services); + + var logger = serviceProvider.GetService(); + Configure(logger); + + var gracePeriodManagerService = serviceProvider + .GetRequiredService(); + var checkUpdateTime = serviceProvider + .GetRequiredService>().Value.CheckUpdateTime; + + while (true) + { + gracePeriodManagerService.CheckConfirmedGracePeriodOrders(); + await Task.Delay(checkUpdateTime); + } + } + + 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(sp => + { + var settings = sp.GetRequiredService>().Value; + var logger = sp.GetRequiredService>(); + var factory = new ConnectionFactory() + { + HostName = settings.EventBusConnection + }; + + return new DefaultRabbitMQPersistentConnection(factory, logger); + }); + + RegisterEventBus(services); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); + } + + public static void Configure(ILoggerFactory loggerFactory) + { + loggerFactory + .AddConsole(Configuration.GetSection("Logging")) + .AddConsole(LogLevel.Debug); + } + + private static void RegisterEventBus(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } + } +} \ No newline at end of file diff --git a/src/Services/GracePeriod/GracePeriodManager/Services/IManagerService.cs b/src/Services/GracePeriod/GracePeriodManager/Services/IManagerService.cs new file mode 100644 index 000000000..2362ccd00 --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/Services/IManagerService.cs @@ -0,0 +1,7 @@ +namespace GracePeriodManager.Services +{ + public interface IManagerService + { + void CheckConfirmedGracePeriodOrders(); + } +} \ No newline at end of file diff --git a/src/Services/GracePeriod/GracePeriodManager/Services/ManagerService.cs b/src/Services/GracePeriod/GracePeriodManager/Services/ManagerService.cs new file mode 100644 index 000000000..24207ea2b --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/Services/ManagerService.cs @@ -0,0 +1,62 @@ +namespace GracePeriodManager.Services +{ + using Dapper; + using GracePeriodManager.IntegrationEvents.Events; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using System.Collections.Generic; + using System.Data.SqlClient; + + public class ManagerService : IManagerService + { + private readonly ManagerSettings _settings; + private readonly IEventBus _eventBus; + private readonly ILogger _logger; + + public ManagerService(IOptions settings, + IEventBus eventBus, + ILogger logger) + { + _settings = settings.Value; + _eventBus = eventBus; + _logger = logger; + } + + public void CheckConfirmedGracePeriodOrders() + { + var orderIds = GetConfirmedGracePeriodOrders(); + + foreach (var orderId in orderIds) + { + var confirmGracePeriodEvent = new GracePeriodConfirmedIntegrationEvent(orderId); + _eventBus.Publish(confirmGracePeriodEvent); + } + } + + private IEnumerable GetConfirmedGracePeriodOrders() + { + IEnumerable orderIds = new List(); + using (var conn = new SqlConnection(_settings.ConnectionString)) + { + try + { + _logger.LogInformation("Grace Period Manager Client is trying to connect to database server"); + conn.Open(); + orderIds = conn.Query( + @"SELECT Id FROM [Microsoft.eShopOnContainers.Services.OrderingDb].[ordering].[orders] + WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime + AND [OrderStatusId] = 1", + new { GracePeriodTime = _settings.GracePeriodTime }); + } + catch (SqlException exception) + { + _logger.LogCritical($"FATAL ERROR: Database connections could not be opened: {exception.Message}"); + } + + } + + return orderIds; + } + } +} \ No newline at end of file diff --git a/src/Services/GracePeriod/GracePeriodManager/appsettings.json b/src/Services/GracePeriod/GracePeriodManager/appsettings.json new file mode 100644 index 000000000..273437a8e --- /dev/null +++ b/src/Services/GracePeriod/GracePeriodManager/appsettings.json @@ -0,0 +1,13 @@ +{ + "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;", + "GracePeriodTime": "1", + "CheckUpdateTime": "30000" +} diff --git a/src/Services/Identity/Identity.API/Identity.API.csproj b/src/Services/Identity/Identity.API/Identity.API.csproj index c27a51593..d31664a2b 100644 --- a/src/Services/Identity/Identity.API/Identity.API.csproj +++ b/src/Services/Identity/Identity.API/Identity.API.csproj @@ -10,6 +10,7 @@ + @@ -39,6 +40,7 @@ + diff --git a/src/Services/Identity/Identity.API/Startup.cs b/src/Services/Identity/Identity.API/Startup.cs index c5f72af13..584aefb35 100644 --- a/src/Services/Identity/Identity.API/Startup.cs +++ b/src/Services/Identity/Identity.API/Startup.cs @@ -16,6 +16,9 @@ using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.HealthChecks; +using Identity.API.Certificate; +using Autofac.Extensions.DependencyInjection; +using Autofac; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -47,8 +50,9 @@ namespace eShopOnContainers.Identity public IConfigurationRoot Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { + public IServiceProvider ConfigureServices(IServiceCollection services) + { + // Add framework services. services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); @@ -99,6 +103,10 @@ namespace eShopOnContainers.Identity builder.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly))) .Services.AddTransient(); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommand.cs new file mode 100644 index 000000000..0672807f3 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommand.cs @@ -0,0 +1,21 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Commands +{ + public class CancelOrderCommand : IRequest + { + + [DataMember] + public int OrderNumber { get; private set; } + + public CancelOrderCommand(int orderNumber) + { + OrderNumber = orderNumber; + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommandHandler.cs new file mode 100644 index 000000000..f8ea7ad40 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommandHandler.cs @@ -0,0 +1,46 @@ +using MediatR; +using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Commands +{ + public class CancelOrderCommandIdentifiedHandler : IdentifierCommandHandler + { + public CancelOrderCommandIdentifiedHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for processing order. + } + } + + public class CancelOrderCommandHandler : IAsyncRequestHandler + { + private readonly IOrderRepository _orderRepository; + + public CancelOrderCommandHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + /// + /// Handler which processes the command when + /// customer executes cancel order from app + /// + /// + /// + public async Task Handle(CancelOrderCommand command) + { + var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber); + orderToUpdate.SetCancelledStatus(); + return await _orderRepository.UnitOfWork.SaveEntitiesAsync(); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs index f2cdf6892..980f9d0b6 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs @@ -3,6 +3,7 @@ using MediatR; using System.Collections.Generic; using System.Runtime.Serialization; using System.Collections; +using Ordering.API.Application.Models; namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands { @@ -22,6 +23,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands [DataMember] private readonly List _orderItems; + [DataMember] + public string UserId { get; private set; } + [DataMember] public string City { get; private set; } @@ -52,12 +56,6 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands [DataMember] public int CardTypeId { get; private set; } - [DataMember] - public int PaymentId { get; private set; } - - [DataMember] - public int BuyerId { get; private set; } - [DataMember] public IEnumerable OrderItems => _orderItems; @@ -66,11 +64,12 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands _orderItems = new List(); } - public CreateOrderCommand(List orderItems, string city, string street, string state, string country, string zipcode, + public CreateOrderCommand(List basketItems, string userId, string city, string street, string state, string country, string zipcode, string cardNumber, string cardHolderName, DateTime cardExpiration, - string cardSecurityNumber, int cardTypeId, int paymentId, int buyerId) : this() + string cardSecurityNumber, int cardTypeId) : this() { - _orderItems = orderItems; + _orderItems = MapToOrderItems(basketItems); + UserId = userId; City = city; Street = street; State = state; @@ -82,8 +81,21 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands CardSecurityNumber = cardSecurityNumber; CardTypeId = cardTypeId; CardExpiration = cardExpiration; - PaymentId = paymentId; - BuyerId = buyerId; + } + + private List MapToOrderItems(List basketItems) + { + var result = new List(); + basketItems.ForEach((item) => { + result.Add(new OrderItemDTO() { + ProductId = int.TryParse(item.ProductId, out int id) ? id : -1, + ProductName = item.ProductName, + PictureUrl = item.PictureUrl, + UnitPrice = item.UnitPrice, + Units = item.Quantity + }); + }); + return result; } public class OrderItemDTO diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs index 2a3e8b8c0..938f80a9c 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs @@ -42,8 +42,8 @@ // methods and constructor so validations, invariants and business logic // make sure that consistency is preserved across the whole aggregate var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode); - var order = new Order(address , message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration); - + var order = new Order(message.UserId, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration); + foreach (var item in message.OrderItems) { order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units); diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommand.cs new file mode 100644 index 000000000..08f37007d --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommand.cs @@ -0,0 +1,21 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Commands +{ + public class ShipOrderCommand : IRequest + { + + [DataMember] + public int OrderNumber { get; private set; } + + public ShipOrderCommand(int orderNumber) + { + OrderNumber = orderNumber; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs new file mode 100644 index 000000000..47f1f5d54 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs @@ -0,0 +1,43 @@ +using MediatR; +using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Commands +{ + public class ShipOrderCommandIdentifiedHandler : IdentifierCommandHandler + { + public ShipOrderCommandIdentifiedHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for processing order. + } + } + + public class ShipOrderCommandHandler : IAsyncRequestHandler + { + private readonly IOrderRepository _orderRepository; + + public ShipOrderCommandHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + /// + /// Handler which processes the command when + /// administrator executes ship order from app + /// + /// + /// + public async Task Handle(ShipOrderCommand command) + { + var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber); + orderToUpdate.SetShippedStatus(); + return await _orderRepository.UnitOfWork.SaveEntitiesAsync(); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs index ce55fd0c9..35e14dd68 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs @@ -1,8 +1,8 @@ using MediatR; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.Extensions.Logging; -using Ordering.API.IntegrationEvents; -using Ordering.API.IntegrationEvents.Events; +using Ordering.API.Application.IntegrationEvents; +using Ordering.API.Application.IntegrationEvents.Events; using Ordering.Domain.Events; using System; using System.Threading.Tasks; @@ -12,16 +12,13 @@ namespace Ordering.API.Application.DomainEventHandlers.BuyerAndPaymentMethodVeri public class UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler : IAsyncNotificationHandler { - private readonly IOrderRepository _orderRepository; - private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + private readonly IOrderRepository _orderRepository; private readonly ILoggerFactory _logger; public UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler( - IOrderRepository orderRepository, ILoggerFactory logger, - IOrderingIntegrationEventService orderingIntegrationEventService) + IOrderRepository orderRepository, ILoggerFactory logger) { - _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); - _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); + _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -32,18 +29,7 @@ namespace Ordering.API.Application.DomainEventHandlers.BuyerAndPaymentMethodVeri { var orderToUpdate = await _orderRepository.GetAsync(buyerPaymentMethodVerifiedEvent.OrderId); orderToUpdate.SetBuyerId(buyerPaymentMethodVerifiedEvent.Buyer.Id); - orderToUpdate.SetPaymentId(buyerPaymentMethodVerifiedEvent.Payment.Id); - - var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(buyerPaymentMethodVerifiedEvent.Buyer.IdentityGuid); - - // Using a local transaction to achieve atomicity between original Ordering database operation and - // the IntegrationEventLog. Only saving event if order has been successfully persisted to db - await _orderingIntegrationEventService - .SaveEventAndOrderingContextChangesAsync(orderStartedIntegrationEvent); - - // Publish ordering integration event and mark it as published - await _orderingIntegrationEventService - .PublishThroughEventBusAsync(orderStartedIntegrationEvent); + orderToUpdate.SetPaymentId(buyerPaymentMethodVerifiedEvent.Payment.Id); _logger.CreateLogger(nameof(UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler)) .LogTrace($"Order with Id: {buyerPaymentMethodVerifiedEvent.OrderId} has been successfully updated with a payment method id: { buyerPaymentMethodVerifiedEvent.Payment.Id }"); diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs new file mode 100644 index 000000000..3c0168656 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs @@ -0,0 +1,43 @@ +namespace Ordering.API.Application.DomainEventHandlers.OrderGracePeriodConfirmed +{ + using MediatR; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using Microsoft.Extensions.Logging; + using Domain.Events; + using System; + using System.Threading.Tasks; + using Ordering.API.Application.IntegrationEvents; + using System.Linq; + using Ordering.API.Application.IntegrationEvents.Events; + + public class OrderStatusChangedToAwaitingValidationDomainEventHandler + : IAsyncNotificationHandler + { + private readonly IOrderRepository _orderRepository; + private readonly ILoggerFactory _logger; + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + + public OrderStatusChangedToAwaitingValidationDomainEventHandler( + IOrderRepository orderRepository, ILoggerFactory logger, + IOrderingIntegrationEventService orderingIntegrationEventService) + { + _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _orderingIntegrationEventService = orderingIntegrationEventService; + } + + public async Task Handle(OrderStatusChangedToAwaitingValidationDomainEvent orderStatusChangedToAwaitingValidationDomainEvent) + { + _logger.CreateLogger(nameof(OrderStatusChangedToAwaitingValidationDomainEvent)) + .LogTrace($"Order with Id: {orderStatusChangedToAwaitingValidationDomainEvent.OrderId} has been successfully updated with " + + $"a status order id: {OrderStatus.AwaitingValidation.Id}"); + + var orderStockList = orderStatusChangedToAwaitingValidationDomainEvent.OrderItems + .Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.GetUnits())); + + var orderStatusChangedToAwaitingValidationIntegrationEvent = new OrderStatusChangedToAwaitingValidationIntegrationEvent( + orderStatusChangedToAwaitingValidationDomainEvent.OrderId, orderStockList); + await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToAwaitingValidationIntegrationEvent); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs new file mode 100644 index 000000000..60f56c2e2 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs @@ -0,0 +1,43 @@ +namespace Ordering.API.Application.DomainEventHandlers.OrderPaid +{ + using MediatR; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using Microsoft.Extensions.Logging; + using Domain.Events; + using System; + using System.Threading.Tasks; + using Ordering.API.Application.IntegrationEvents; + using System.Linq; + using Ordering.API.Application.IntegrationEvents.Events; + + public class OrderStatusChangedToPaidDomainEventHandler + : IAsyncNotificationHandler + { + private readonly IOrderRepository _orderRepository; + private readonly ILoggerFactory _logger; + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + + public OrderStatusChangedToPaidDomainEventHandler( + IOrderRepository orderRepository, ILoggerFactory logger, + IOrderingIntegrationEventService orderingIntegrationEventService) + { + _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _orderingIntegrationEventService = orderingIntegrationEventService; + } + + public async Task Handle(OrderStatusChangedToPaidDomainEvent orderStatusChangedToPaidDomainEvent) + { + _logger.CreateLogger(nameof(OrderStatusChangedToPaidDomainEventHandler)) + .LogTrace($"Order with Id: {orderStatusChangedToPaidDomainEvent.OrderId} has been successfully updated with " + + $"a status order id: {OrderStatus.Paid.Id}"); + + var orderStockList = orderStatusChangedToPaidDomainEvent.OrderItems + .Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.GetUnits())); + + var orderStatusChangedToPaidIntegrationEvent = new OrderStatusChangedToPaidIntegrationEvent(orderStatusChangedToPaidDomainEvent.OrderId, + orderStockList); + await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToPaidIntegrationEvent); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs index ada82118e..77714d1b0 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs @@ -26,14 +26,12 @@ namespace Ordering.API.Application.DomainEventHandlers.OrderStartedEvent { var cardTypeId = (orderStartedEvent.CardTypeId != 0) ? orderStartedEvent.CardTypeId : 1; - var userGuid = _identityService.GetUserIdentity(); - - var buyer = await _buyerRepository.FindAsync(userGuid); + var buyer = await _buyerRepository.FindAsync(orderStartedEvent.UserId); bool buyerOriginallyExisted = (buyer == null) ? false : true; if (!buyerOriginallyExisted) { - buyer = new Buyer(userGuid); + buyer = new Buyer(orderStartedEvent.UserId); } buyer.VerifyOrAddPaymentMethod(cardTypeId, diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs new file mode 100644 index 000000000..7ead82c4d --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs @@ -0,0 +1,38 @@ +namespace Ordering.API.Application.DomainEventHandlers.OrderStockConfirmed +{ + using MediatR; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using Microsoft.Extensions.Logging; + using Domain.Events; + using System; + using System.Threading.Tasks; + using Ordering.API.Application.IntegrationEvents; + using Ordering.API.Application.IntegrationEvents.Events; + + public class OrderStatusChangedToStockConfirmedDomainEventHandler + : IAsyncNotificationHandler + { + private readonly IOrderRepository _orderRepository; + private readonly ILoggerFactory _logger; + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + + public OrderStatusChangedToStockConfirmedDomainEventHandler( + IOrderRepository orderRepository, ILoggerFactory logger, + IOrderingIntegrationEventService orderingIntegrationEventService) + { + _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _orderingIntegrationEventService = orderingIntegrationEventService; + } + + public async Task Handle(OrderStatusChangedToStockConfirmedDomainEvent orderStatusChangedToStockConfirmedDomainEvent) + { + _logger.CreateLogger(nameof(OrderStatusChangedToStockConfirmedDomainEventHandler)) + .LogTrace($"Order with Id: {orderStatusChangedToStockConfirmedDomainEvent.OrderId} has been successfully updated with " + + $"a status order id: {OrderStatus.StockConfirmed.Id}"); + + var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(orderStatusChangedToStockConfirmedDomainEvent.OrderId); + await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStatusChangedToStockConfirmedIntegrationEvent); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs new file mode 100644 index 000000000..c51619ff6 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs @@ -0,0 +1,32 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; +using Ordering.API.Application.IntegrationEvents.Events; +using System.Threading.Tasks; + +namespace Ordering.API.Application.IntegrationEvents.EventHandling +{ + public class GracePeriodConfirmedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IOrderRepository _orderRepository; + + public GracePeriodConfirmedIntegrationEventHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + /// + /// Event handler which confirms that the grace period + /// has been completed and order will not initially be cancelled. + /// Therefore, the order process continues for validation. + /// + /// + /// + /// + public async Task Handle(GracePeriodConfirmedIntegrationEvent @event) + { + var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); + orderToUpdate.SetAwaitingValidationStatus(); + await _orderRepository.UnitOfWork.SaveEntitiesAsync(); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs new file mode 100644 index 000000000..259b7ec34 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs @@ -0,0 +1,27 @@ +namespace Ordering.API.Application.IntegrationEvents.EventHandling +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using Ordering.API.Application.IntegrationEvents.Events; + using System.Threading.Tasks; + + public class OrderPaymentFailedIntegrationEventHandler : + IIntegrationEventHandler + { + private readonly IOrderRepository _orderRepository; + + public OrderPaymentFailedIntegrationEventHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task Handle(OrderPaymentFailedIntegrationEvent @event) + { + var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); + + orderToUpdate.SetCancelledStatus(); + + await _orderRepository.UnitOfWork.SaveEntitiesAsync(); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSuccededIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSuccededIntegrationEventHandler.cs new file mode 100644 index 000000000..0e8598dcc --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSuccededIntegrationEventHandler.cs @@ -0,0 +1,27 @@ +namespace Ordering.API.Application.IntegrationEvents.EventHandling +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using Ordering.API.Application.IntegrationEvents.Events; + using System.Threading.Tasks; + + public class OrderPaymentSuccededIntegrationEventHandler : + IIntegrationEventHandler + { + private readonly IOrderRepository _orderRepository; + + public OrderPaymentSuccededIntegrationEventHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task Handle(OrderPaymentSuccededIntegrationEvent @event) + { + var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); + + orderToUpdate.SetPaidStatus(); + + await _orderRepository.UnitOfWork.SaveEntitiesAsync(); + } + } +} \ 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..fa7463041 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs @@ -0,0 +1,27 @@ +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; + + public class OrderStockConfirmedIntegrationEventHandler : + IIntegrationEventHandler + { + private readonly IOrderRepository _orderRepository; + + public OrderStockConfirmedIntegrationEventHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task Handle(OrderStockConfirmedIntegrationEvent @event) + { + var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); + + orderToUpdate.SetStockConfirmedStatus(); + + await _orderRepository.UnitOfWork.SaveEntitiesAsync(); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs new file mode 100644 index 000000000..c70eba187 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs @@ -0,0 +1,31 @@ +namespace Ordering.API.Application.IntegrationEvents.EventHandling +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using System.Threading.Tasks; + using Events; + using System.Linq; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + + public class OrderStockRejectedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IOrderRepository _orderRepository; + + public OrderStockRejectedIntegrationEventHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task Handle(OrderStockRejectedIntegrationEvent @event) + { + var orderToUpdate = await _orderRepository.GetAsync(@event.OrderId); + + var orderStockRejectedItems = @event.OrderStockItems + .FindAll(c => !c.HasStock) + .Select(c => c.ProductId); + + orderToUpdate.SetCancelledStatusWhenStockIsRejected(orderStockRejectedItems); + + await _orderRepository.UnitOfWork.SaveEntitiesAsync(); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs new file mode 100644 index 000000000..e2beff9f3 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs @@ -0,0 +1,58 @@ +using System; +using MediatR; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using System.Threading.Tasks; +using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using Microsoft.Extensions.Logging; +using Ordering.API.Application.IntegrationEvents.Events; + +namespace Ordering.API.Application.IntegrationEvents.EventHandling +{ + public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler + { + private readonly IMediator _mediator; + private readonly ILoggerFactory _logger; + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + + public UserCheckoutAcceptedIntegrationEventHandler(IMediator mediator, + ILoggerFactory logger, IOrderingIntegrationEventService orderingIntegrationEventService) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); + } + + /// + /// Integration event handler which starts the create order process + /// + /// + /// Integration event message which is sent by the + /// basket.api once it has successfully process the + /// order items. + /// + /// + public async Task Handle(UserCheckoutAcceptedIntegrationEvent eventMsg) + { + var result = false; + + // Send Integration event to clean basket once basket is converted to Order and before starting with the order creation process + var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(eventMsg.UserId); + await _orderingIntegrationEventService.PublishThroughEventBusAsync(orderStartedIntegrationEvent); + + if (eventMsg.RequestId != Guid.Empty) + { + var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items, eventMsg.UserId, eventMsg.City, eventMsg.Street, + eventMsg.State, eventMsg.Country, eventMsg.ZipCode, + eventMsg.CardNumber, eventMsg.CardHolderName, eventMsg.CardExpiration, + eventMsg.CardSecurityNumber, eventMsg.CardTypeId); + + var requestCreateOrder = new IdentifiedCommand(createOrderCommand, eventMsg.RequestId); + result = await _mediator.Send(requestCreateOrder); + } + + _logger.CreateLogger(nameof(UserCheckoutAcceptedIntegrationEventHandler)) + .LogTrace(result ? $"UserCheckoutAccepted integration event has been received and a create new order process is started with requestId: {eventMsg.RequestId}" : + $"UserCheckoutAccepted integration event has been received but a new order process has failed with requestId: {eventMsg.RequestId}"); + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..15b0aebb5 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs @@ -0,0 +1,12 @@ +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class GracePeriodConfirmedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public GracePeriodConfirmedIntegrationEvent(int orderId) => + OrderId = orderId; + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderPaymentFailedIntegrationEvent .cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderPaymentFailedIntegrationEvent .cs new file mode 100644 index 000000000..fec066521 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderPaymentFailedIntegrationEvent .cs @@ -0,0 +1,11 @@ +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderPaymentFailedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public OrderPaymentFailedIntegrationEvent(int orderId) => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderPaymentSuccededIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderPaymentSuccededIntegrationEvent.cs new file mode 100644 index 000000000..778aa8114 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderPaymentSuccededIntegrationEvent.cs @@ -0,0 +1,11 @@ +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderPaymentSuccededIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public OrderPaymentSuccededIntegrationEvent(int orderId) => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs similarity index 72% rename from src/Services/Ordering/Ordering.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs rename to src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs index 8184483e5..4a5d7db38 100644 --- a/src/Services/Ordering/Ordering.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs @@ -4,16 +4,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -namespace Ordering.API.IntegrationEvents.Events +namespace Ordering.API.Application.IntegrationEvents.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 OrderStartedIntegrationEvent : IntegrationEvent { - public string UserId { get; } + public string UserId { get; set; } - public OrderStartedIntegrationEvent(string userId) => - UserId = userId; + public OrderStartedIntegrationEvent(string userId) + => UserId = userId; } -} +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs new file mode 100644 index 000000000..63ae02246 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs @@ -0,0 +1,30 @@ +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using System.Collections.Generic; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + public IEnumerable OrderStockItems { get; } + + public OrderStatusChangedToAwaitingValidationIntegrationEvent(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/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs new file mode 100644 index 000000000..115592308 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs @@ -0,0 +1,18 @@ +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using System.Collections.Generic; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + public IEnumerable OrderStockItems { get; } + + public OrderStatusChangedToPaidIntegrationEvent(int orderId, + IEnumerable orderStockItems) + { + OrderId = orderId; + OrderStockItems = orderStockItems; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..d0b1ef2c4 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs @@ -0,0 +1,12 @@ +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStatusChangedToStockConfirmedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public OrderStatusChangedToStockConfirmedIntegrationEvent(int orderId) + => OrderId = orderId; + } +} \ 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/OrderStockRejectedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs new file mode 100644 index 000000000..647970581 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs @@ -0,0 +1,31 @@ +namespace Ordering.API.Application.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + using System.Collections.Generic; + + public class OrderStockRejectedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public List OrderStockItems { get; } + + public OrderStockRejectedIntegrationEvent(int orderId, + List orderStockItems) + { + OrderId = orderId; + OrderStockItems = orderStockItems; + } + } + + public class ConfirmedOrderStockItem + { + public int ProductId { get; } + public bool HasStock { get; } + + public ConfirmedOrderStockItem(int productId, bool hasStock) + { + ProductId = productId; + HasStock = hasStock; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs new file mode 100644 index 000000000..c23e59332 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs @@ -0,0 +1,59 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Ordering.API.Application.Models; +using System; + +namespace Ordering.API.Application.IntegrationEvents.Events +{ + public class UserCheckoutAcceptedIntegrationEvent : IntegrationEvent + { + public string UserId { get; } + + public string City { get; set; } + + public string Street { get; set; } + + public string State { get; set; } + + public string Country { get; set; } + + public string ZipCode { get; set; } + + public string CardNumber { get; set; } + + public string CardHolderName { get; set; } + + public DateTime CardExpiration { get; set; } + + public string CardSecurityNumber { get; set; } + + public int CardTypeId { get; set; } + + public string Buyer { get; set; } + + public Guid RequestId { get; set; } + + public CustomerBasket Basket { get; } + + public UserCheckoutAcceptedIntegrationEvent(string userId, string city, string street, + string state, string country, string zipCode, string cardNumber, string cardHolderName, + DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId, + CustomerBasket basket) + { + UserId = userId; + City = city; + Street = street; + State = state; + Country = country; + ZipCode = zipCode; + CardNumber = cardNumber; + CardHolderName = cardHolderName; + CardExpiration = cardExpiration; + CardSecurityNumber = cardSecurityNumber; + CardTypeId = cardTypeId; + Buyer = buyer; + Basket = basket; + RequestId = requestId; + } + + } +} diff --git a/src/Services/Ordering/Ordering.API/IntegrationEvents/IOrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs similarity index 55% rename from src/Services/Ordering/Ordering.API/IntegrationEvents/IOrderingIntegrationEventService.cs rename to src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs index 6538afbfd..373bafaa5 100644 --- a/src/Services/Ordering/Ordering.API/IntegrationEvents/IOrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs @@ -1,14 +1,10 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -namespace Ordering.API.IntegrationEvents +namespace Ordering.API.Application.IntegrationEvents { public interface IOrderingIntegrationEventService { - Task SaveEventAndOrderingContextChangesAsync(IntegrationEvent evt); Task PublishThroughEventBusAsync(IntegrationEvent evt); } } diff --git a/src/Services/Ordering/Ordering.API/IntegrationEvents/OrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs similarity index 87% rename from src/Services/Ordering/Ordering.API/IntegrationEvents/OrderingIntegrationEventService.cs rename to src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs index 810c07bc9..b3c0201b5 100644 --- a/src/Services/Ordering/Ordering.API/IntegrationEvents/OrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs @@ -4,13 +4,13 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; -using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; using System; using System.Data.Common; +using System.Diagnostics; using System.Threading.Tasks; -namespace Ordering.API.IntegrationEvents +namespace Ordering.API.Application.IntegrationEvents { public class OrderingIntegrationEventService : IOrderingIntegrationEventService { @@ -19,7 +19,7 @@ namespace Ordering.API.IntegrationEvents private readonly OrderingContext _orderingContext; private readonly IIntegrationEventLogService _eventLogService; - public OrderingIntegrationEventService (IEventBus eventBus, OrderingContext orderingContext, + public OrderingIntegrationEventService(IEventBus eventBus, OrderingContext orderingContext, Func integrationEventLogServiceFactory) { _orderingContext = orderingContext ?? throw new ArgumentNullException(nameof(orderingContext)); @@ -30,11 +30,12 @@ namespace Ordering.API.IntegrationEvents public async Task PublishThroughEventBusAsync(IntegrationEvent evt) { + await SaveEventAndOrderingContextChangesAsync(evt); _eventBus.Publish(evt); await _eventLogService.MarkEventAsPublishedAsync(evt); } - public async Task SaveEventAndOrderingContextChangesAsync(IntegrationEvent evt) + private async Task SaveEventAndOrderingContextChangesAsync(IntegrationEvent evt) { //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency diff --git a/src/Services/Ordering/Ordering.API/Application/Models/BasketItem.cs b/src/Services/Ordering/Ordering.API/Application/Models/BasketItem.cs new file mode 100644 index 000000000..698bdea3e --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Models/BasketItem.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Models +{ + public class BasketItem + { + public string Id { get; set; } + public string ProductId { get; set; } + public string ProductName { get; set; } + public decimal UnitPrice { get; set; } + public decimal OldUnitPrice { get; set; } + public int Quantity { get; set; } + public string PictureUrl { get; set; } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Models/CustomerBasket.cs b/src/Services/Ordering/Ordering.API/Application/Models/CustomerBasket.cs new file mode 100644 index 000000000..0275d1f80 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Models/CustomerBasket.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Models +{ + public class CustomerBasket + { + public string BuyerId { get; set; } + public List Items { get; set; } + + public CustomerBasket(string customerId) + { + BuyerId = customerId; + Items = new List(); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs index e51cf04ce..e6f964fed 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs @@ -26,7 +26,7 @@ connection.Open(); var result = await connection.QueryAsync( - @"select o.[Id] as ordernumber,o.OrderDate as date, os.Name as status, + @"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description, os.Name as status, oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl, a.Street as street, a.City as city, a.Country as country, a.State as state, a.ZipCode as zipcode FROM ordering.Orders o @@ -75,6 +75,7 @@ order.ordernumber = result[0].ordernumber; order.date = result[0].date; order.status = result[0].status; + order.description = result[0].description; order.street = result[0].street; order.city = result[0].city; order.zipcode = result[0].zipcode; diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/CancelOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/CancelOrderCommandValidator.cs new file mode 100644 index 000000000..5c619586f --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Validations/CancelOrderCommandValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using Ordering.API.Application.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Validations +{ + public class CancelOrderCommandValidator : AbstractValidator + { + public CancelOrderCommandValidator() + { + RuleFor(order => order.OrderNumber).NotEmpty().WithMessage("No orderId found"); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/ShipOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/ShipOrderCommandValidator.cs new file mode 100644 index 000000000..72a16d319 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Validations/ShipOrderCommandValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using Ordering.API.Application.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Validations +{ + public class ShipOrderCommandValidator : AbstractValidator + { + public ShipOrderCommandValidator() + { + RuleFor(order => order.OrderNumber).NotEmpty().WithMessage("No orderId found"); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index d386ea699..1e40910be 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries; using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; +using Ordering.API.Application.Commands; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -26,21 +27,30 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService)); } - [Route("new")] - [HttpPost] - public async Task CreateOrder([FromBody]CreateOrderCommand command, [FromHeader(Name = "x-requestid")] string requestId) + [Route("cancel")] + [HttpPut] + public async Task CancelOrder([FromBody]CancelOrderCommand command, [FromHeader(Name = "x-requestid")] string requestId) { bool commandResult = false; if (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty) { - var requestCreateOrder = new IdentifiedCommand(command, guid); - commandResult = await _mediator.Send(requestCreateOrder); + var requestCancelOrder = new IdentifiedCommand(command, guid); + commandResult = await _mediator.Send(requestCancelOrder); } - else + + return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest(); + + } + + [Route("ship")] + [HttpPut] + public async Task ShipOrder([FromBody]ShipOrderCommand command, [FromHeader(Name = "x-requestid")] string requestId) + { + bool commandResult = false; + if (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty) { - // If no x-requestid header is found we process the order anyway. This is just temporary to not break existing clients - // that aren't still updated. When all clients were updated this could be removed. - commandResult = await _mediator.Send(command); + var requestShipOrder = new IdentifiedCommand(command, guid); + commandResult = await _mediator.Send(requestShipOrder); } return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest(); diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170511112333_AddOrderDescription.Designer.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170511112333_AddOrderDescription.Designer.cs new file mode 100644 index 000000000..5f0a3d7a0 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170511112333_AddOrderDescription.Designer.cs @@ -0,0 +1,246 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; + +namespace Ordering.API.Migrations +{ + [DbContext(typeof(OrderingContext))] + [Migration("20170511112333_AddOrderDescription")] + partial class AddOrderDescription + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1") + .HasAnnotation("SqlServer:Sequence:.orderitemseq", "'orderitemseq', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:ordering.buyerseq", "'buyerseq', 'ordering', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:ordering.orderseq", "'orderseq', 'ordering', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:ordering.paymentseq", "'paymentseq', 'ordering', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "buyerseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("IdentityGuid") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("IdentityGuid") + .IsUnique(); + + b.ToTable("buyers","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("cardtypes","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "paymentseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Alias") + .IsRequired() + .HasMaxLength(200); + + b.Property("BuyerId"); + + b.Property("CardHolderName") + .IsRequired() + .HasMaxLength(200); + + b.Property("CardNumber") + .IsRequired() + .HasMaxLength(25); + + b.Property("CardTypeId"); + + b.Property("Expiration"); + + b.HasKey("Id"); + + b.HasIndex("BuyerId"); + + b.HasIndex("CardTypeId"); + + b.ToTable("paymentmethods","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("City"); + + b.Property("Country"); + + b.Property("State"); + + b.Property("Street"); + + b.Property("ZipCode"); + + b.HasKey("Id"); + + b.ToTable("address","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "orderseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("AddressId"); + + b.Property("BuyerId"); + + b.Property("Description"); + + b.Property("OrderDate"); + + b.Property("OrderStatusId"); + + b.Property("PaymentMethodId"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("BuyerId"); + + b.HasIndex("OrderStatusId"); + + b.HasIndex("PaymentMethodId"); + + b.ToTable("orders","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "orderitemseq") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Discount"); + + b.Property("OrderId"); + + b.Property("PictureUrl"); + + b.Property("ProductId"); + + b.Property("ProductName") + .IsRequired(); + + b.Property("UnitPrice"); + + b.Property("Units"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("orderItems","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("orderstatus","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency.ClientRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name") + .IsRequired(); + + b.Property("Time"); + + b.HasKey("Id"); + + b.ToTable("requests","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer") + .WithMany("PaymentMethods") + .HasForeignKey("BuyerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType", "CardType") + .WithMany() + .HasForeignKey("CardTypeId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Order", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Address", "Address") + .WithMany() + .HasForeignKey("AddressId"); + + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer") + .WithMany() + .HasForeignKey("BuyerId"); + + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus", "OrderStatus") + .WithMany() + .HasForeignKey("OrderStatusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod") + .WithMany() + .HasForeignKey("PaymentMethodId"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170511112333_AddOrderDescription.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170511112333_AddOrderDescription.cs new file mode 100644 index 000000000..028babb96 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170511112333_AddOrderDescription.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Ordering.API.Migrations +{ + public partial class AddOrderDescription : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Description", + schema: "ordering", + table: "orders", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Description", + schema: "ordering", + table: "orders"); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/OrderingContextModelSnapshot.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/OrderingContextModelSnapshot.cs index f73f1bd04..fa342cf31 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/OrderingContextModelSnapshot.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/OrderingContextModelSnapshot.cs @@ -121,6 +121,8 @@ namespace Ordering.API.Migrations b.Property("BuyerId"); + b.Property("Description"); + b.Property("OrderDate"); b.Property("OrderStatusId"); @@ -183,7 +185,7 @@ namespace Ordering.API.Migrations b.ToTable("orderstatus","ordering"); }); - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.ClientRequest", b => + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency.ClientRequest", b => { b.Property("Id") .ValueGeneratedOnAdd(); diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs b/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs index de3294df7..ebc4778d1 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.Submitted); + context.OrderStatus.Add(OrderStatus.AwaitingValidation); + context.OrderStatus.Add(OrderStatus.StockConfirmed); + 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/Ordering.API.csproj b/src/Services/Ordering/Ordering.API/Ordering.API.csproj index 548c9c14c..96b4f6627 100644 --- a/src/Services/Ordering/Ordering.API/Ordering.API.csproj +++ b/src/Services/Ordering/Ordering.API/Ordering.API.csproj @@ -77,7 +77,6 @@ - diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index cb9a5986a..80b7b2abe 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -3,8 +3,10 @@ using AspNetCore.Http; using Autofac; using Autofac.Extensions.DependencyInjection; + using global::Ordering.API.Application.IntegrationEvents; + using global::Ordering.API.Application.IntegrationEvents.EventHandling; + using global::Ordering.API.Application.IntegrationEvents.Events; using global::Ordering.API.Infrastructure.Middlewares; - using global::Ordering.API.IntegrationEvents; using Infrastructure; using Infrastructure.Auth; using Infrastructure.AutofacModules; @@ -111,7 +113,7 @@ services.AddTransient(); services.AddTransient>( sp => (DbConnection c) => new IntegrationEventLogService(c)); - var serviceProvider = services.BuildServiceProvider(); + services.AddTransient(); services.AddSingleton(sp => @@ -126,9 +128,7 @@ return new DefaultRabbitMQPersistentConnection(factory, logger); }); - services.AddSingleton(); - services.AddSingleton(); - + RegisterServiceBus(services); services.AddOptions(); //configure autofac @@ -142,6 +142,7 @@ return new AutofacServiceProvider(container.Build()); } + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); @@ -150,7 +151,6 @@ app.UseCors("CorsPolicy"); ConfigureAuth(app); - app.UseMvcWithDefaultRoute(); app.UseSwagger() @@ -160,12 +160,32 @@ }); WaitForSqlAvailabilityAsync(loggerFactory, app).Wait(); + ConfigureEventBus(app); var integrationEventLogContext = new IntegrationEventLogContext( new DbContextOptionsBuilder() .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Ordering.API")) .Options); integrationEventLogContext.Database.Migrate(); + + } + + private void RegisterServiceBus(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); } protected virtual void ConfigureAuth(IApplicationBuilder app) diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs index 961d83d3a..5579cf45e 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs @@ -17,6 +17,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O public String ZipCode { get; private set; } + private Address() { } + public Address(string street, string city, string state, string country, string zipcode) { Street = street; diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs index 281cf6ecc..37c17c897 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -1,7 +1,6 @@ -using MediatR; -using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; -using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; +using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; using Ordering.Domain.Events; +using Ordering.Domain.Exceptions; using System; using System.Collections.Generic; using System.Linq; @@ -23,6 +22,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O public OrderStatus OrderStatus { get; private set; } private int _orderStatusId; + private string _description; // DDD Patterns comment // Using a private collection field, better for DDD Aggregate's encapsulation @@ -30,7 +30,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O // but only through the method OrderAggrergateRoot.AddOrderItem() which includes behaviour. private readonly List _orderItems; - public IEnumerable OrderItems => _orderItems.AsReadOnly(); + public IReadOnlyCollection OrderItems => _orderItems; // Using List<>.AsReadOnly() // This will create a read only wrapper around the private list so is protected against "external updates". // It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance) @@ -38,21 +38,21 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O private int? _paymentMethodId; - protected Order() { } + protected Order() { _orderItems = new List(); } - public Order(Address address, int cardTypeId, string cardNumber, string cardSecurityNumber, + public Order(string userId, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber, string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) { _orderItems = new List(); _buyerId = buyerId; _paymentMethodId = paymentMethodId; - _orderStatusId = OrderStatus.InProcess.Id; + _orderStatusId = OrderStatus.Submitted.Id; _orderDate = DateTime.UtcNow; Address = address; // Add the OrderStarterDomainEvent to the domain events collection // to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ] - AddOrderStartedDomainEvent(cardTypeId, cardNumber, + AddOrderStartedDomainEvent(userId, cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); } @@ -94,14 +94,99 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O _buyerId = id; } - private void AddOrderStartedDomainEvent(int cardTypeId, string cardNumber, + public void SetAwaitingValidationStatus() + { + if (_orderStatusId == OrderStatus.Cancelled.Id || + _orderStatusId != OrderStatus.Submitted.Id) + { + StatusChangeException(OrderStatus.AwaitingValidation); + } + + AddDomainEvent(new OrderStatusChangedToAwaitingValidationDomainEvent(Id, _orderItems)); + + _orderStatusId = OrderStatus.AwaitingValidation.Id; + } + + public void SetStockConfirmedStatus() + { + if (_orderStatusId != OrderStatus.AwaitingValidation.Id) + { + StatusChangeException(OrderStatus.StockConfirmed); + } + + AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id)); + + _orderStatusId = OrderStatus.StockConfirmed.Id; + _description = "All the items were confirmed with available stock."; + } + + public void SetPaidStatus() + { + if (_orderStatusId != OrderStatus.StockConfirmed.Id) + { + StatusChangeException(OrderStatus.Paid); + } + + AddDomainEvent(new OrderStatusChangedToPaidDomainEvent(Id, OrderItems)); + + _orderStatusId = OrderStatus.Paid.Id; + _description = "The payment was performed at a simulated \"American Bank checking bank account endinf on XX35071\""; + } + + public void SetShippedStatus() + { + if (_orderStatusId != OrderStatus.Paid.Id) + { + StatusChangeException(OrderStatus.Shipped); + } + + _orderStatusId = OrderStatus.Shipped.Id; + _description = "The order was shipped."; + } + + public void SetCancelledStatus() + { + if (_orderStatusId == OrderStatus.Paid.Id || + _orderStatusId == OrderStatus.Shipped.Id) + { + StatusChangeException(OrderStatus.Cancelled); + } + + _orderStatusId = OrderStatus.Cancelled.Id; + _description = $"The order was cancelled."; + } + + public void SetCancelledStatusWhenStockIsRejected(IEnumerable orderStockRejectedItems) + { + if (_orderStatusId != OrderStatus.AwaitingValidation.Id) + { + StatusChangeException(OrderStatus.Cancelled); + } + + _orderStatusId = OrderStatus.Cancelled.Id; + + var itemsStockRejectedProductNames = OrderItems + .Where(c => orderStockRejectedItems.Contains(c.ProductId)) + .Select(c => c.GetOrderItemProductName()); + + var itemsStockRejectedDescription = string.Join(", ", itemsStockRejectedProductNames); + _description = $"The product items don't have stock: ({itemsStockRejectedDescription})."; + } + + private void AddOrderStartedDomainEvent(string userId, int cardTypeId, string cardNumber, string cardSecurityNumber, string cardHolderName, DateTime cardExpiration) { var orderStartedDomainEvent = new OrderStartedDomainEvent( - this, cardTypeId, cardNumber, cardSecurityNumber, + this, userId, cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); this.AddDomainEvent(orderStartedDomainEvent); } + + private void StatusChangeException(OrderStatus orderStatusToChange) + { + throw new OrderingDomainException($"Not possible to change order status from {OrderStatus.Name} to {orderStatusToChange.Name}."); + } } } + diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs index 5d011981a..fd65fb3ca 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs @@ -54,6 +54,13 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O return _discount; } + public int GetUnits() + { + return _units; + } + + public string GetOrderItemProductName() => _productName; + 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..bc07635ae 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 Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant()); + public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant()); + public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).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[] { Submitted, AwaitingValidation, StockConfirmed, Paid, Shipped, Cancelled }; public static OrderStatus FromName(string name) { diff --git a/src/Services/Ordering/Ordering.Domain/Events/OrderStartedDomainEvent.cs b/src/Services/Ordering/Ordering.Domain/Events/OrderStartedDomainEvent.cs index 699361428..38de4601a 100644 --- a/src/Services/Ordering/Ordering.Domain/Events/OrderStartedDomainEvent.cs +++ b/src/Services/Ordering/Ordering.Domain/Events/OrderStartedDomainEvent.cs @@ -12,6 +12,7 @@ namespace Ordering.Domain.Events public class OrderStartedDomainEvent : INotification { + public string UserId { get; private set; } public int CardTypeId { get; private set; } public string CardNumber { get; private set; } public string CardSecurityNumber { get; private set; } @@ -19,12 +20,13 @@ namespace Ordering.Domain.Events public DateTime CardExpiration { get; private set; } public Order Order { get; private set; } - public OrderStartedDomainEvent(Order order, + public OrderStartedDomainEvent(Order order, string userId, int cardTypeId, string cardNumber, string cardSecurityNumber, string cardHolderName, DateTime cardExpiration) { Order = order; + UserId = userId; CardTypeId = cardTypeId; CardNumber = cardNumber; CardSecurityNumber = cardSecurityNumber; diff --git a/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToAwaitingValidationDomainEvent.cs b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToAwaitingValidationDomainEvent.cs new file mode 100644 index 000000000..b4dd7aa82 --- /dev/null +++ b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToAwaitingValidationDomainEvent.cs @@ -0,0 +1,23 @@ +namespace Ordering.Domain.Events +{ + using MediatR; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using System.Collections.Generic; + + /// + /// Event used when the grace period order is confirmed + /// + public class OrderStatusChangedToAwaitingValidationDomainEvent + : INotification + { + public int OrderId { get; } + public IEnumerable OrderItems { get; } + + public OrderStatusChangedToAwaitingValidationDomainEvent(int orderId, + IEnumerable orderItems) + { + OrderId = orderId; + OrderItems = orderItems; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs new file mode 100644 index 000000000..f315bab60 --- /dev/null +++ b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs @@ -0,0 +1,23 @@ +namespace Ordering.Domain.Events +{ + using MediatR; + using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; + using System.Collections.Generic; + + /// + /// Event used when the order is paid + /// + public class OrderStatusChangedToPaidDomainEvent + : INotification + { + public int OrderId { get; } + public IEnumerable OrderItems { get; } + + public OrderStatusChangedToPaidDomainEvent(int orderId, + IEnumerable orderItems) + { + OrderId = orderId; + OrderItems = orderItems; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToStockConfirmedDomainEvent.cs b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToStockConfirmedDomainEvent.cs new file mode 100644 index 000000000..b16bebbcc --- /dev/null +++ b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToStockConfirmedDomainEvent.cs @@ -0,0 +1,16 @@ +namespace Ordering.Domain.Events +{ + using MediatR; + + /// + /// Event used when the order stock items are confirmed + /// + public class OrderStatusChangedToStockConfirmedDomainEvent + : INotification + { + public int OrderId { get; } + + public OrderStatusChangedToStockConfirmedDomainEvent(int orderId) + => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index 18d66534b..0c69fe566 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -36,6 +36,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure public OrderingContext(DbContextOptions options, IMediator mediator) : base(options) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + + + System.Diagnostics.Debug.WriteLine("OrderingContext::ctor ->" + this.GetHashCode()); } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -153,6 +156,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure orderConfiguration.Property("BuyerId").IsRequired(false); orderConfiguration.Property("OrderStatusId").IsRequired(); orderConfiguration.Property("PaymentMethodId").IsRequired(false); + orderConfiguration.Property("Description").IsRequired(false); var navigation = orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems)); // DDD Patterns comment: diff --git a/src/Services/Ordering/Ordering.Infrastructure/Repositories/OrderRepository.cs b/src/Services/Ordering/Ordering.Infrastructure/Repositories/OrderRepository.cs index 53f0ad92a..4aaf738af 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/Repositories/OrderRepository.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/Repositories/OrderRepository.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; +using Ordering.Domain.Exceptions; using System; using System.Threading.Tasks; @@ -32,7 +33,18 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositor public async Task GetAsync(int orderId) { - return await _context.Orders.FindAsync(orderId); + var order = await _context.Orders.FindAsync(orderId); + if (order != null) + { + await _context.Entry(order) + .Collection(i => i.OrderItems).LoadAsync(); + await _context.Entry(order) + .Reference(i => i.OrderStatus).LoadAsync(); + await _context.Entry(order) + .Reference(i => i.Address).LoadAsync(); + } + + return order; } public void Update(Order order) diff --git a/src/Services/Payment/Payment.API/.dockerignore b/src/Services/Payment/Payment.API/.dockerignore new file mode 100644 index 000000000..d8f8175f6 --- /dev/null +++ b/src/Services/Payment/Payment.API/.dockerignore @@ -0,0 +1,3 @@ +* +!obj/Docker/publish/* +!obj/Docker/empty/ diff --git a/src/Services/Payment/Payment.API/Controllers/HomeController.cs b/src/Services/Payment/Payment.API/Controllers/HomeController.cs new file mode 100644 index 000000000..1372ea720 --- /dev/null +++ b/src/Services/Payment/Payment.API/Controllers/HomeController.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +// For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace Payment.API.Controllers +{ + public class HomeController : Controller + { + // GET: // + public IActionResult Index() + { + return new RedirectResult("~/swagger/ui"); + } + } +} diff --git a/src/Services/Payment/Payment.API/Controllers/PaymentController.cs b/src/Services/Payment/Payment.API/Controllers/PaymentController.cs new file mode 100644 index 000000000..60ccca2a6 --- /dev/null +++ b/src/Services/Payment/Payment.API/Controllers/PaymentController.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace Payment.API.Controllers +{ + [Route("api/v1/[controller]")] + public class PaymentController : Controller + { + // GET: api/values + [HttpGet] + public IEnumerable Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public string Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody]string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody]string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } +} diff --git a/src/Services/Payment/Payment.API/Dockerfile b/src/Services/Payment/Payment.API/Dockerfile new file mode 100644 index 000000000..0bb3473d9 --- /dev/null +++ b/src/Services/Payment/Payment.API/Dockerfile @@ -0,0 +1,6 @@ +FROM microsoft/aspnetcore:1.1 +ARG source +WORKDIR /app +EXPOSE 80 +COPY ${source:-obj/Docker/publish} . +ENTRYPOINT ["dotnet", "Payment.API.dll"] diff --git a/src/Services/Payment/Payment.API/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs b/src/Services/Payment/Payment.API/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs new file mode 100644 index 000000000..dfdba0068 --- /dev/null +++ b/src/Services/Payment/Payment.API/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs @@ -0,0 +1,37 @@ +namespace Payment.API.IntegrationEvents.EventHandling +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + using System.Threading.Tasks; + using Payment.API.IntegrationEvents.Events; + using Microsoft.Extensions.Options; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStatusChangedToStockConfirmedIntegrationEventHandler : + IIntegrationEventHandler + { + private readonly IEventBus _eventBus; + private readonly PaymentSettings _settings; + + public OrderStatusChangedToStockConfirmedIntegrationEventHandler(IEventBus eventBus, + IOptionsSnapshot settings) + { + _eventBus = eventBus; + _settings = settings.Value; + } + + public async Task Handle(OrderStatusChangedToStockConfirmedIntegrationEvent @event) + { + IntegrationEvent orderPaymentIntegrationEvent; + if(_settings.PaymentSucceded) + { + orderPaymentIntegrationEvent = new OrderPaymentSuccededIntegrationEvent(@event.OrderId); + } + else + { + orderPaymentIntegrationEvent = new OrderPaymentFailedIntegrationEvent(@event.OrderId); + } + + _eventBus.Publish(orderPaymentIntegrationEvent); + } + } +} \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderPaymentFailedIntegrationEvent.cs b/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderPaymentFailedIntegrationEvent.cs new file mode 100644 index 000000000..d51c518c4 --- /dev/null +++ b/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderPaymentFailedIntegrationEvent.cs @@ -0,0 +1,11 @@ +namespace Payment.API.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderPaymentFailedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public OrderPaymentFailedIntegrationEvent(int orderId) => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderPaymentSuccededIntegrationEvent.cs b/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderPaymentSuccededIntegrationEvent.cs new file mode 100644 index 000000000..d672ff9d4 --- /dev/null +++ b/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderPaymentSuccededIntegrationEvent.cs @@ -0,0 +1,11 @@ +namespace Payment.API.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderPaymentSuccededIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public OrderPaymentSuccededIntegrationEvent(int orderId) => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs b/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..88f60e0d6 --- /dev/null +++ b/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs @@ -0,0 +1,12 @@ +namespace Payment.API.IntegrationEvents.Events +{ + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; + + public class OrderStatusChangedToStockConfirmedIntegrationEvent : IntegrationEvent + { + public int OrderId { get; } + + public OrderStatusChangedToStockConfirmedIntegrationEvent(int orderId) + => OrderId = orderId; + } +} \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/Payment.API.csproj b/src/Services/Payment/Payment.API/Payment.API.csproj new file mode 100644 index 000000000..dc8aad80d --- /dev/null +++ b/src/Services/Payment/Payment.API/Payment.API.csproj @@ -0,0 +1,37 @@ + + + + netcoreapp1.1 + ..\..\..\..\docker-compose.dcproj + portable-net45+win8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Payment/Payment.API/PaymentSettings.cs b/src/Services/Payment/Payment.API/PaymentSettings.cs new file mode 100644 index 000000000..e31acb52e --- /dev/null +++ b/src/Services/Payment/Payment.API/PaymentSettings.cs @@ -0,0 +1,8 @@ +namespace Payment.API +{ + public class PaymentSettings + { + public bool PaymentSucceded { get; set; } + public string EventBusConnection { get; set; } + } +} diff --git a/src/Services/Payment/Payment.API/Program.cs b/src/Services/Payment/Payment.API/Program.cs new file mode 100644 index 000000000..505dd2aee --- /dev/null +++ b/src/Services/Payment/Payment.API/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Payment.API +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseApplicationInsights() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Services/Payment/Payment.API/Properties/launchSettings.json b/src/Services/Payment/Payment.API/Properties/launchSettings.json new file mode 100644 index 000000000..faf310f83 --- /dev/null +++ b/src/Services/Payment/Payment.API/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3330/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Payment.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:3331" + } + } +} diff --git a/src/Services/Payment/Payment.API/Startup.cs b/src/Services/Payment/Payment.API/Startup.cs new file mode 100644 index 000000000..32d0fc8c4 --- /dev/null +++ b/src/Services/Payment/Payment.API/Startup.cs @@ -0,0 +1,103 @@ +using Autofac; +using Autofac.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +using RabbitMQ.Client; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using Payment.API.IntegrationEvents.Events; +using Payment.API.IntegrationEvents.EventHandling; + +namespace Payment.API +{ + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public IServiceProvider ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc(); + services.Configure(Configuration); + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + var factory = new ConnectionFactory() + { + HostName = Configuration["EventBusConnection"] + }; + + return new DefaultRabbitMQPersistentConnection(factory, logger); + }); + + RegisterServiceBus(services); + + services.AddSwaggerGen(options => + { + options.DescribeAllEnumsAsStrings(); + options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info + { + Title = "eShopOnContainers - Payment HTTP API", + Version = "v1", + Description = "The Payment Microservice HTTP API. This is a Data-Driven/CRUD microservice sample", + TermsOfService = "Terms Of Service" + }); + }); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + app.UseMvcWithDefaultRoute(); + + + app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + ConfigureEventBus(app); + } + + private void RegisterServiceBus(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient, + OrderStatusChangedToStockConfirmedIntegrationEventHandler>(); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe>(); + } + } +} \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/appsettings.Development.json b/src/Services/Payment/Payment.API/appsettings.Development.json new file mode 100644 index 000000000..fa8ce71a9 --- /dev/null +++ b/src/Services/Payment/Payment.API/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Services/Payment/Payment.API/appsettings.json b/src/Services/Payment/Payment.API/appsettings.json new file mode 100644 index 000000000..601eaa246 --- /dev/null +++ b/src/Services/Payment/Payment.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + "PaymentSucceded": "true" +} diff --git a/src/Web/WebMVC/Controllers/OrderController.cs b/src/Web/WebMVC/Controllers/OrderController.cs index 83152d697..8f34c2282 100644 --- a/src/Web/WebMVC/Controllers/OrderController.cs +++ b/src/Web/WebMVC/Controllers/OrderController.cs @@ -8,6 +8,7 @@ using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Microsoft.AspNetCore.Authorization; using System.Net.Http; using Polly.CircuitBreaker; +using WebMVC.Models; namespace Microsoft.eShopOnContainers.WebMVC.Controllers { @@ -36,14 +37,16 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers } [HttpPost] - public async Task Create(Order model, string action) + public async Task Checkout(Order model) { try { if (ModelState.IsValid) { var user = _appUserParser.Parse(HttpContext.User); - await _orderSvc.CreateOrder(model); + var basket = _orderSvc.MapOrderToBasket(model); + + await _basketSvc.Checkout(basket); //Redirect to historic list. return RedirectToAction("Index"); @@ -56,6 +59,14 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers return View(model); } + public async Task Cancel(string orderId) + { + await _orderSvc.CancelOrder(orderId); + + //Redirect to historic list. + return RedirectToAction("Index"); + } + public async Task Detail(string orderId) { var user = _appUserParser.Parse(HttpContext.User); diff --git a/src/Web/WebMVC/Controllers/OrderManagementController.cs b/src/Web/WebMVC/Controllers/OrderManagementController.cs new file mode 100644 index 000000000..abd0b21ab --- /dev/null +++ b/src/Web/WebMVC/Controllers/OrderManagementController.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using WebMVC.Models; +using Microsoft.eShopOnContainers.WebMVC.Services; +using Microsoft.eShopOnContainers.WebMVC.ViewModels; +using Microsoft.AspNetCore.Authorization; + +namespace WebMVC.Controllers +{ + [Authorize] + public class OrderManagementController : Controller + { + private IOrderingService _orderSvc; + private readonly IIdentityParser _appUserParser; + public OrderManagementController(IOrderingService orderSvc, IIdentityParser appUserParser) + { + _appUserParser = appUserParser; + _orderSvc = orderSvc; + } + + public async Task Index() + { + var user = _appUserParser.Parse(HttpContext.User); + var vm = await _orderSvc.GetMyOrders(user); + + return View(vm); + } + + [HttpPost] + public async Task OrderProcess(string orderId, string actionCode) + { + if (OrderProcessAction.Ship.Code == actionCode) + { + await _orderSvc.ShipOrder(orderId); + } + + return RedirectToAction("Index"); + } + } +} diff --git a/src/Web/WebMVC/Infrastructure/API.cs b/src/Web/WebMVC/Infrastructure/API.cs index c837b8067..fc170ffcc 100644 --- a/src/Web/WebMVC/Infrastructure/API.cs +++ b/src/Web/WebMVC/Infrastructure/API.cs @@ -14,6 +14,11 @@ return baseUri; } + public static string CheckoutBasket(string baseUri) + { + return $"{baseUri}/checkout"; + } + public static string CleanBasket(string baseUri, string basketId) { return $"{baseUri}/{basketId}"; @@ -36,6 +41,16 @@ { return $"{baseUri}/new"; } + + public static string CancelOrder(string baseUri) + { + return $"{baseUri}/cancel"; + } + + public static string ShipOrder(string baseUri) + { + return $"{baseUri}/ship"; + } } public static class Catalog diff --git a/src/Web/WebMVC/Models/BasketDTO.cs b/src/Web/WebMVC/Models/BasketDTO.cs new file mode 100644 index 000000000..4609c8533 --- /dev/null +++ b/src/Web/WebMVC/Models/BasketDTO.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace WebMVC.Models +{ + public class BasketDTO + { + [Required] + public string City { get; set; } + [Required] + public string Street { get; set; } + [Required] + public string State { get; set; } + [Required] + public string Country { get; set; } + + public string ZipCode { get; set; } + [Required] + public string CardNumber { get; set; } + [Required] + public string CardHolderName { get; set; } + + [Required] + public DateTime CardExpiration { get; set; } + + [Required] + public string CardSecurityNumber { get; set; } + + public int CardTypeId { get; set; } + + public string Buyer { get; set; } + + [Required] + public Guid RequestId { get; set; } + } +} + diff --git a/src/Web/WebMVC/Models/OrderDTO.cs b/src/Web/WebMVC/Models/OrderDTO.cs new file mode 100644 index 000000000..13646ea38 --- /dev/null +++ b/src/Web/WebMVC/Models/OrderDTO.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace WebMVC.Models +{ + public class OrderDTO + { + [Required] + public string OrderNumber { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Models/OrderProcessAction.cs b/src/Web/WebMVC/Models/OrderProcessAction.cs new file mode 100644 index 000000000..bd746bb36 --- /dev/null +++ b/src/Web/WebMVC/Models/OrderProcessAction.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace WebMVC.Models +{ + public class OrderProcessAction + { + public string Code { get; private set; } + public string Name { get; private set; } + + public static OrderProcessAction Ship = new OrderProcessAction(nameof(Ship).ToLowerInvariant(), "Ship"); + + protected OrderProcessAction() + { + } + + public OrderProcessAction(string code, string name) + { + Code = code; + Name = name; + } + } +} diff --git a/src/Web/WebMVC/Services/BasketService.cs b/src/Web/WebMVC/Services/BasketService.cs index bd418ea26..55cec1bb9 100644 --- a/src/Web/WebMVC/Services/BasketService.cs +++ b/src/Web/WebMVC/Services/BasketService.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using System.Collections.Generic; using System.Threading.Tasks; using WebMVC.Infrastructure; +using WebMVC.Models; namespace Microsoft.eShopOnContainers.WebMVC.Services { @@ -54,6 +55,16 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services return basket; } + public async Task Checkout(BasketDTO basket) + { + var token = await GetUserTokenAsync(); + var updateBasketUri = API.Basket.CheckoutBasket(_remoteServiceBaseUrl); + + var response = await _apiClient.PostAsync(updateBasketUri, basket, token); + + response.EnsureSuccessStatusCode(); + } + public async Task SetQuantities(ApplicationUser user, Dictionary quantities) { var basket = await GetBasket(user); @@ -109,18 +120,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services basket.Items.Add(product); await UpdateBasket(basket); - } - - public async Task CleanBasket(ApplicationUser user) - { - var token = await GetUserTokenAsync(); - var cleanBasketUri = API.Basket.CleanBasket(_remoteServiceBaseUrl, user.Id); - - var response = await _apiClient.DeleteAsync(cleanBasketUri, token); - - //CCE: response status code... - - } + } async Task GetUserTokenAsync() { diff --git a/src/Web/WebMVC/Services/IBasketService.cs b/src/Web/WebMVC/Services/IBasketService.cs index 114246d5e..13921909a 100644 --- a/src/Web/WebMVC/Services/IBasketService.cs +++ b/src/Web/WebMVC/Services/IBasketService.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using WebMVC.Models; namespace Microsoft.eShopOnContainers.WebMVC.Services { @@ -11,8 +12,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services Task GetBasket(ApplicationUser user); Task AddItemToBasket(ApplicationUser user, BasketItem product); Task UpdateBasket(Basket basket); + Task Checkout(BasketDTO basket); Task SetQuantities(ApplicationUser user, Dictionary quantities); Order MapBasketToOrder(Basket basket); - Task CleanBasket(ApplicationUser user); } } diff --git a/src/Web/WebMVC/Services/IOrderingService.cs b/src/Web/WebMVC/Services/IOrderingService.cs index e1a7e6f83..1de2c631c 100644 --- a/src/Web/WebMVC/Services/IOrderingService.cs +++ b/src/Web/WebMVC/Services/IOrderingService.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using WebMVC.Models; namespace Microsoft.eShopOnContainers.WebMVC.Services { @@ -10,8 +11,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services { Task> GetMyOrders(ApplicationUser user); Task GetOrder(ApplicationUser user, string orderId); - Task CreateOrder(Order order); + Task CancelOrder(string orderId); + Task ShipOrder(string orderId); Order MapUserInfoIntoOrder(ApplicationUser user, Order order); + BasketDTO MapOrderToBasket(Order order); void OverrideUserInfoIntoOrder(Order original, Order destination); } } diff --git a/src/Web/WebMVC/Services/OrderingService.cs b/src/Web/WebMVC/Services/OrderingService.cs index 8f198fbed..d9eba7392 100644 --- a/src/Web/WebMVC/Services/OrderingService.cs +++ b/src/Web/WebMVC/Services/OrderingService.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using WebMVC.Infrastructure; +using WebMVC.Models; namespace Microsoft.eShopOnContainers.WebMVC.Services { @@ -65,22 +66,41 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services return order; } - async public Task CreateOrder(Order order) + async public Task CancelOrder(string orderId) { var token = await GetUserTokenAsync(); - var requestId = order.RequestId.ToString(); - var addNewOrderUri = API.Order.AddNewOrder(_remoteServiceBaseUrl); + var order = new OrderDTO() + { + OrderNumber = orderId + }; - order.CardTypeId = 1; - order.CardExpirationApiFormat(); + var cancelOrderUri = API.Order.CancelOrder(_remoteServiceBaseUrl); + + var response = await _apiClient.PutAsync(cancelOrderUri, order, token, Guid.NewGuid().ToString()); + + if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + { + throw new Exception("Error cancelling order, try later."); + } + + response.EnsureSuccessStatusCode(); + } + + async public Task ShipOrder(string orderId) + { + var token = await GetUserTokenAsync(); + var order = new OrderDTO() + { + OrderNumber = orderId + }; - SetFakeIdToProducts(order); + var shipOrderUri = API.Order.ShipOrder(_remoteServiceBaseUrl); - var response = await _apiClient.PostAsync(addNewOrderUri, order, token, requestId); + var response = await _apiClient.PutAsync(shipOrderUri, order, token, Guid.NewGuid().ToString()); if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) { - throw new Exception("Error creating order, try later."); + throw new Exception("Error in ship order process, try later."); } response.EnsureSuccessStatusCode(); @@ -100,6 +120,27 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services destination.CardSecurityNumber = original.CardSecurityNumber; } + public BasketDTO MapOrderToBasket(Order order) + { + order.CardExpirationApiFormat(); + + return new BasketDTO() + { + City = order.City, + Street = order.Street, + State = order.State, + Country = order.Country, + ZipCode = order.ZipCode, + CardNumber = order.CardNumber, + CardHolderName = order.CardHolderName, + CardExpiration = order.CardExpiration, + CardSecurityNumber = order.CardSecurityNumber, + CardTypeId = 1, + Buyer = order.Buyer, + RequestId = order.RequestId + }; + } + void SetFakeIdToProducts(Order order) { var id = 1; @@ -111,6 +152,6 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services var context = _httpContextAccesor.HttpContext; return await context.Authentication.GetTokenAsync("access_token"); - } + } } } diff --git a/src/Web/WebMVC/ViewModels/Order.cs b/src/Web/WebMVC/ViewModels/Order.cs index 785b29119..28dbe9968 100644 --- a/src/Web/WebMVC/ViewModels/Order.cs +++ b/src/Web/WebMVC/ViewModels/Order.cs @@ -1,4 +1,5 @@ -using Microsoft.eShopOnContainers.WebMVC.ViewModels.Annotations; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.eShopOnContainers.WebMVC.ViewModels.Annotations; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -6,6 +7,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using WebMVC.Models; namespace Microsoft.eShopOnContainers.WebMVC.ViewModels { @@ -19,6 +21,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.ViewModels public decimal Total {get;set;} + public string Description { get; set; } + [Required] public string City { get; set; } [Required] @@ -49,6 +53,9 @@ namespace Microsoft.eShopOnContainers.WebMVC.ViewModels public string Buyer { get; set; } + public List ActionCodeSelectList => + GetActionCodesByCurrentState(); + // See the property initializer syntax below. This // initializes the compiler generated field for this // auto-implemented property. @@ -70,6 +77,25 @@ namespace Microsoft.eShopOnContainers.WebMVC.ViewModels CardExpiration = new DateTime(int.Parse(year), int.Parse(month), 1); } + + private List GetActionCodesByCurrentState() + { + var actions = new List(); + switch (Status?.ToLower()) + { + case "paid": + actions.Add(OrderProcessAction.Ship); + break; + } + + var result = new List(); + actions.ForEach(action => + { + result.Add(new SelectListItem { Text = action.Name, Value = action.Code }); + }); + + return result; + } } public enum CardType diff --git a/src/Web/WebMVC/Views/Cart/Index.cshtml b/src/Web/WebMVC/Views/Cart/Index.cshtml index 1111df0cf..d9386a1fd 100644 --- a/src/Web/WebMVC/Views/Cart/Index.cshtml +++ b/src/Web/WebMVC/Views/Cart/Index.cshtml @@ -10,7 +10,8 @@
- @Html.Partial("_Header", new Header(){ Controller = "Catalog", Text = "Back to catalog" }) + @Html.Partial("_Header", new List
() { + new Header() { Controller = "Catalog", Text = "Back to catalog" } }) @await Component.InvokeAsync("CartList", new { user = UserManager.Parse(User) })
diff --git a/src/Web/WebMVC/Views/Order/Create.cshtml b/src/Web/WebMVC/Views/Order/Create.cshtml index e05d3a7b7..24a987fa9 100644 --- a/src/Web/WebMVC/Views/Order/Create.cshtml +++ b/src/Web/WebMVC/Views/Order/Create.cshtml @@ -6,10 +6,10 @@ ViewData["Title"] = "New Order"; } -@Html.Partial("_Header", new Header() { Controller = "Cart", Text = "Back to cart" }) - +@Html.Partial("_Header", new List
() { + new Header() { Controller = "Cart", Text = "Back to cart" } })
- +
@foreach (var error in ViewData.ModelState.Values.SelectMany(err => err.Errors)) { diff --git a/src/Web/WebMVC/Views/Order/Detail.cshtml b/src/Web/WebMVC/Views/Order/Detail.cshtml index c3a371275..c17ffb2a9 100644 --- a/src/Web/WebMVC/Views/Order/Detail.cshtml +++ b/src/Web/WebMVC/Views/Order/Detail.cshtml @@ -7,7 +7,8 @@ }
- @Html.Partial("_Header", new Header() { Controller = "Order", Text = "Back to list" }) + @Html.Partial("_Header", new List
() { + new Header() { Controller = "Catalog", Text = "Back to catalog" } })
@@ -21,8 +22,18 @@
@Model.OrderNumber
@Model.Date
-
$ @Model.Total
-
@Model.Status
+
$@Model.Total
+
@Model.Status
+
+
+ +
+
+
Description
+
+ +
+
@Model.Description
diff --git a/src/Web/WebMVC/Views/Order/Index.cshtml b/src/Web/WebMVC/Views/Order/Index.cshtml index 83cc8b992..16e5a2003 100644 --- a/src/Web/WebMVC/Views/Order/Index.cshtml +++ b/src/Web/WebMVC/Views/Order/Index.cshtml @@ -7,7 +7,9 @@ }
- @Html.Partial("_Header", new Header() { Controller = "Catalog", Text = "Back to catalog" }) + @Html.Partial("_Header", new List
() { + new Header() { Controller = "Catalog", Text = "Back to catalog" }, + new Header() { Controller = "OrderManagement", Text = "Orders Management" } })
@@ -25,9 +27,15 @@
@Html.DisplayFor(modelItem => item.Date)
$ @Html.DisplayFor(modelItem => item.Total)
@Html.DisplayFor(modelItem => item.Status)
-
+
Detail
+
+ @if (item.Status.ToLower() == "submitted") + { + Cancel + } +
}
diff --git a/src/Web/WebMVC/Views/OrderManagement/Index.cshtml b/src/Web/WebMVC/Views/OrderManagement/Index.cshtml new file mode 100644 index 000000000..d108f2281 --- /dev/null +++ b/src/Web/WebMVC/Views/OrderManagement/Index.cshtml @@ -0,0 +1,43 @@ +@using Microsoft.eShopOnContainers.WebMVC.ViewModels + +@model IEnumerable + +@{ + ViewData["Title"] = "My Orders"; +} + +
+ @Html.Partial("_Header", new List
() { + new Header() { Controller = "Catalog", Text = "Back to catalog" } }) + +
+
+
Order number
+
Date
+
Total
+
Status
+
+
+ + @foreach (var item in Model) + { +
+
@Html.DisplayFor(modelItem => item.OrderNumber)
+
@Html.DisplayFor(modelItem => item.Date)
+
$ @Html.DisplayFor(modelItem => item.Total)
+
@Html.DisplayFor(modelItem => item.Status)
+
+ + + + +
+
+ } +
+
\ No newline at end of file diff --git a/src/Web/WebMVC/Views/Shared/_Header.cshtml b/src/Web/WebMVC/Views/Shared/_Header.cshtml index 894a92af2..9c06eabd3 100644 --- a/src/Web/WebMVC/Views/Shared/_Header.cshtml +++ b/src/Web/WebMVC/Views/Shared/_Header.cshtml @@ -1,7 +1,11 @@ -@model Microsoft.eShopOnContainers.WebMVC.ViewModels.Header + +@model IEnumerable
- @Model.Text + @foreach (var header in @Model) + { + @header.Text + }
diff --git a/src/Web/WebMVC/wwwroot/css/shared/components/header/header.css b/src/Web/WebMVC/wwwroot/css/shared/components/header/header.css index 89fd203bd..737c18dbb 100644 --- a/src/Web/WebMVC/wwwroot/css/shared/components/header/header.css +++ b/src/Web/WebMVC/wwwroot/css/shared/components/header/header.css @@ -3,6 +3,20 @@ height: 4rem; } +.esh-header-title { + color: rgba(255, 255, 255, 0.5) !important; + line-height: 4rem; + text-transform: uppercase; + text-decoration: none; + transition: color 0.35s; + margin-right: 15px; +} + + .esh-header-title:hover { + color: #FFFFFF !important; + transition: color 0.35s; + } + .esh-header-back { color: rgba(255, 255, 255, 0.5) !important; line-height: 4rem; diff --git a/src/Web/WebSPA/Client/modules/basket/basket.service.ts b/src/Web/WebSPA/Client/modules/basket/basket.service.ts index b7fbb60d5..5aa03c182 100644 --- a/src/Web/WebSPA/Client/modules/basket/basket.service.ts +++ b/src/Web/WebSPA/Client/modules/basket/basket.service.ts @@ -4,7 +4,9 @@ import { Router } from '@angular/router'; import { DataService } from '../shared/services/data.service'; import { SecurityService } from '../shared/services/security.service'; -import { IBasket } from '../shared/models/basket.model'; +import { IBasket } from '../shared/models/basket.model'; +import { IOrder } from '../shared/models/order.model'; +import { IBasketCheckout } from '../shared/models/basketCheckout.model'; import { IBasketItem } from '../shared/models/basketItem.model'; import { BasketWrapperService } from '../shared/services/basket.wrapper.service'; import { ConfigurationService } from '../shared/services/configuration.service'; @@ -49,10 +51,6 @@ export class BasketService { } } } - - this.basketEvents.orderCreated$.subscribe(x => { - this.dropBasket(); - }); } addItemToBasket(item): Observable { @@ -67,6 +65,12 @@ export class BasketService { }); } + setBasketCheckout(basketCheckout): Observable { + return this.service.postWithId(this.basketUrl + '/checkout', basketCheckout).map((response: Response) => { + return true; + }); + } + getBasket(): Observable { return this.service.get(this.basketUrl + '/' + this.basket.buyerId).map((response: Response) => { if (response.status === 204) { @@ -75,13 +79,26 @@ export class BasketService { return response.json(); }); - } + } - dropBasket() { - this.basket.items = []; - this.service.delete(this.basketUrl + '/' + this.basket.buyerId); - this.basketDropedSource.next(); - } + mapBasketInfoCheckout(order: IOrder): IBasketCheckout { + let basketCheckout = {}; + + basketCheckout.street = order.street + basketCheckout.city = order.city; + basketCheckout.country = order.country; + basketCheckout.state = order.state; + basketCheckout.zipcode = order.zipcode; + basketCheckout.cardexpiration = order.cardexpiration; + basketCheckout.cardnumber = order.cardnumber; + basketCheckout.cardsecuritynumber = order.cardsecuritynumber; + basketCheckout.cardtypeid = order.cardtypeid; + basketCheckout.cardholdername = order.cardholdername; + basketCheckout.total = 0; + basketCheckout.expiration = order.expiration; + + return basketCheckout; + } private loadData() { this.getBasket().subscribe(basket => { diff --git a/src/Web/WebSPA/Client/modules/orders/orders-detail/orders-detail.component.html b/src/Web/WebSPA/Client/modules/orders/orders-detail/orders-detail.component.html index 62f6c8431..db88ed7a3 100644 --- a/src/Web/WebSPA/Client/modules/orders/orders-detail/orders-detail.component.html +++ b/src/Web/WebSPA/Client/modules/orders/orders-detail/orders-detail.component.html @@ -17,6 +17,16 @@
{{order.status}}
+ +
+
+
Description
+
+ +
+
{{order.description}}
+
+
diff --git a/src/Web/WebSPA/Client/modules/orders/orders-new/orders-new.component.ts b/src/Web/WebSPA/Client/modules/orders/orders-new/orders-new.component.ts index 4f6f82a9f..b1e4e49da 100644 --- a/src/Web/WebSPA/Client/modules/orders/orders-new/orders-new.component.ts +++ b/src/Web/WebSPA/Client/modules/orders/orders-new/orders-new.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { OrdersService } from '../orders.service'; +import { OrdersService } from '../orders.service'; +import { BasketService } from '../../basket/basket.service'; import { IOrder } from '../../shared/models/order.model'; import { BasketWrapperService } from '../../shared/services/basket.wrapper.service'; @@ -18,9 +19,9 @@ export class OrdersNewComponent implements OnInit { errorReceived: boolean; order: IOrder; - constructor(private service: OrdersService, fb: FormBuilder, private router: Router) { + constructor(private orderService: OrdersService, private basketService: BasketService, fb: FormBuilder, private router: Router) { // Obtain user profile information - this.order = service.mapBasketAndIdentityInfoNewOrder(); + this.order = orderService.mapOrderAndIdentityInfoNewOrder(); this.newOrderForm = fb.group({ 'street': [this.order.street, Validators.required], 'city': [this.order.city, Validators.required], @@ -36,7 +37,7 @@ export class OrdersNewComponent implements OnInit { ngOnInit() { } - submitForm(value: any) { + submitForm(value: any) { this.order.street = this.newOrderForm.controls['street'].value; this.order.city = this.newOrderForm.controls['city'].value; this.order.state = this.newOrderForm.controls['state'].value; @@ -46,8 +47,8 @@ export class OrdersNewComponent implements OnInit { this.order.cardholdername = this.newOrderForm.controls['cardholdername'].value; this.order.cardexpiration = new Date(20 + this.newOrderForm.controls['expirationdate'].value.split('/')[1], this.newOrderForm.controls['expirationdate'].value.split('/')[0]); this.order.cardsecuritynumber = this.newOrderForm.controls['securitycode'].value; - - this.service.postOrder(this.order) + let basketCheckout = this.basketService.mapBasketInfoCheckout(this.order); + this.basketService.setBasketCheckout(basketCheckout) .catch((errMessage) => { this.errorReceived = true; this.isOrderProcessing = false; diff --git a/src/Web/WebSPA/Client/modules/orders/orders.module.ts b/src/Web/WebSPA/Client/modules/orders/orders.module.ts index 1ecd0e9f5..9b43eb207 100644 --- a/src/Web/WebSPA/Client/modules/orders/orders.module.ts +++ b/src/Web/WebSPA/Client/modules/orders/orders.module.ts @@ -5,12 +5,13 @@ import { SharedModule } from '../shared/shared.module'; import { OrdersComponent } from './orders.component'; import { OrdersDetailComponent } from './orders-detail/orders-detail.component'; import { OrdersNewComponent } from './orders-new/orders-new.component'; -import { OrdersService } from './orders.service'; +import { OrdersService } from './orders.service'; +import { BasketService } from '../basket/basket.service'; import { Header } from '../shared/components/header/header'; @NgModule({ imports: [BrowserModule, SharedModule], declarations: [OrdersComponent, OrdersDetailComponent, OrdersNewComponent], - providers: [OrdersService] + providers: [OrdersService, BasketService] }) export class OrdersModule { } diff --git a/src/Web/WebSPA/Client/modules/orders/orders.service.ts b/src/Web/WebSPA/Client/modules/orders/orders.service.ts index 355670cdd..5eda7c8ce 100644 --- a/src/Web/WebSPA/Client/modules/orders/orders.service.ts +++ b/src/Web/WebSPA/Client/modules/orders/orders.service.ts @@ -44,13 +44,7 @@ export class OrdersService { }); } - postOrder(item): Observable { - return this.service.postWithId(this.ordersUrl + '/api/v1/orders/new', item).map((response: Response) => { - return true; - }); - } - - mapBasketAndIdentityInfoNewOrder(): IOrder { + mapOrderAndIdentityInfoNewOrder(): IOrder { let order = {}; let basket = this.basketService.basket; let identityInfo = this.identityService.UserData; diff --git a/src/Web/WebSPA/Client/modules/shared/models/basketCheckout.model.ts b/src/Web/WebSPA/Client/modules/shared/models/basketCheckout.model.ts new file mode 100644 index 000000000..2f7cfbf62 --- /dev/null +++ b/src/Web/WebSPA/Client/modules/shared/models/basketCheckout.model.ts @@ -0,0 +1,16 @@ +export interface IBasketCheckout { + city: number; + street: string; + state: string; + country: number; + zipcode: string; + cardnumber: string; + cardexpiration: Date; + expiration: string; + cardsecuritynumber: string; + cardholdername: string; + cardtypeid: number; + buyer: string; + ordernumber: string; + total: number; +} \ No newline at end of file diff --git a/src/Web/WebSPA/Client/modules/shared/models/order-detail.model.ts b/src/Web/WebSPA/Client/modules/shared/models/order-detail.model.ts index 25a869f9c..5631d8807 100644 --- a/src/Web/WebSPA/Client/modules/shared/models/order-detail.model.ts +++ b/src/Web/WebSPA/Client/modules/shared/models/order-detail.model.ts @@ -3,6 +3,7 @@ import {IOrderItem} from './orderItem.model'; export interface IOrderDetail { ordernumber: string; status: string; + description: string; street: string; date: Date; city: number; diff --git a/src/Web/WebSPA/Client/modules/shared/services/data.service.ts b/src/Web/WebSPA/Client/modules/shared/services/data.service.ts index 3b121f4e7..64288e127 100644 --- a/src/Web/WebSPA/Client/modules/shared/services/data.service.ts +++ b/src/Web/WebSPA/Client/modules/shared/services/data.service.ts @@ -39,6 +39,10 @@ export class DataService { return this.doPost(url, data, false, params); } + putWithId(url: string, data: any, params?: any): Observable { + return this.doPut(url, data, true, params); + } + private doPost(url: string, data: any, needId: boolean, params?: any): Observable { let options: RequestOptionsArgs = {}; @@ -57,6 +61,24 @@ export class DataService { }).catch(this.handleError); } + private doPut(url: string, data: any, needId: boolean, params?: any): Observable { + let options: RequestOptionsArgs = {}; + + options.headers = new Headers(); + if (this.securityService) { + options.headers.append('Authorization', 'Bearer ' + this.securityService.GetToken()); + } + if (needId) { + let guid = Guid.newGuid(); + options.headers.append('x-requestid', guid); + } + + return this.http.put(url, data, options).map( + (res: Response) => { + return res; + }).catch(this.handleError); + } + delete(url: string, params?: any) { let options: RequestOptionsArgs = {}; diff --git a/test/Services/FunctionalTests/Services/Basket/BasketScenariosBase.cs b/test/Services/FunctionalTests/Services/Basket/BasketScenariosBase.cs index 5de55abb3..2f3a22d10 100644 --- a/test/Services/FunctionalTests/Services/Basket/BasketScenariosBase.cs +++ b/test/Services/FunctionalTests/Services/Basket/BasketScenariosBase.cs @@ -29,6 +29,7 @@ namespace FunctionalTests.Services.Basket public static class Post { public static string CreateBasket = "/"; + public static string Checkout = "/checkout"; } } } diff --git a/test/Services/FunctionalTests/Services/Basket/BasketTestsStartup.cs b/test/Services/FunctionalTests/Services/Basket/BasketTestsStartup.cs index a0e625d96..2beb73bd1 100644 --- a/test/Services/FunctionalTests/Services/Basket/BasketTestsStartup.cs +++ b/test/Services/FunctionalTests/Services/Basket/BasketTestsStartup.cs @@ -1,10 +1,7 @@ -using Microsoft.eShopOnContainers.Services.Basket.API; -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.AspNetCore.Hosting; +using FunctionalTests.Middleware; using Microsoft.AspNetCore.Builder; -using FunctionalTests.Middleware; +using Microsoft.AspNetCore.Hosting; +using Microsoft.eShopOnContainers.Services.Basket.API; namespace FunctionalTests.Services.Basket { diff --git a/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs b/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs index 5b2424114..57d107033 100644 --- a/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs +++ b/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs @@ -1,5 +1,6 @@ using FunctionalTests.Extensions; -using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using FunctionalTests.Services.Basket; +using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Newtonsoft.Json; using System; @@ -8,71 +9,128 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using WebMVC.Models; using Xunit; -using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; namespace FunctionalTests.Services.Ordering { public class OrderingScenarios : OrderingScenariosBase - { + { [Fact] - public async Task Create_order_and_return_the_order_by_id() + public async Task Cancel_basket_and_check_order_status_cancelled() { - using (var server = CreateServer()) + using (var orderServer = new OrderingScenariosBase().CreateServer()) + using (var basketServer = new BasketScenariosBase().CreateServer()) { - var client = server.CreateIdempotentClient(); - - // GIVEN an order is created - await client.PostAsync(Post.AddNewOrder, new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json")); - - var ordersResponse = await client.GetAsync(Get.Orders); - var responseBody = await ordersResponse.Content.ReadAsStringAsync(); - var orders = JsonConvert.DeserializeObject>(responseBody); - string orderId = orders.OrderByDescending(o => o.Date).First().OrderNumber; - - //WHEN we request the order bit its id - var order= await client.GetAsync(Get.OrderBy(int.Parse(orderId))); - var orderBody = await order.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(orderBody); - - //THEN the requested order is returned - Assert.Equal(orderId, result.OrderNumber); - Assert.Equal("inprocess", result.Status); - Assert.Equal(1, result.OrderItems.Count); - Assert.Equal(10, result.OrderItems[0].UnitPrice); + // Expected data + var cityExpected = $"city-{Guid.NewGuid()}"; + var orderStatusExpected = "cancelled"; + + var basketClient = basketServer.CreateIdempotentClient(); + var orderClient = orderServer.CreateIdempotentClient(); + + // GIVEN a basket is created + var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); + await basketClient.PostAsync(BasketScenariosBase.Post.CreateBasket, contentBasket); + + // AND basket checkout is sent + await basketClient.PostAsync(BasketScenariosBase.Post.Checkout, new StringContent(BuildCheckout(cityExpected), UTF8Encoding.UTF8, "application/json")); + + // WHEN Order is created in Ordering.api + var newOrder = await TryGetNewOrderCreated(cityExpected, orderClient); + + // AND Order is cancelled in Ordering.api + await orderClient.PutAsync(OrderingScenariosBase.Put.CancelOrder, new StringContent(BuildCancelOrder(newOrder.OrderNumber), UTF8Encoding.UTF8, "application/json")); + + // AND the requested order is retrieved + var order = await TryGetNewOrderCreated(cityExpected, orderClient); + + // THEN check status + Assert.Equal(orderStatusExpected, order.Status); } } - - string BuildOrder() + + private async Task TryGetNewOrderCreated(string city, HttpClient orderClient) + { + var counter = 0; + Order order = null; + + while (counter < 20) + { + //get the orders and verify that the new order has been created + var ordersGetResponse = await orderClient.GetStringAsync(OrderingScenariosBase.Get.Orders); + var orders = JsonConvert.DeserializeObject>(ordersGetResponse); + + if (orders == null || orders.Count == 0) { + counter++; + await Task.Delay(100); + continue; + } + + var lastOrder = orders.OrderByDescending(o => o.Date).First(); + int.TryParse(lastOrder.OrderNumber, out int id); + var orderDetails = await orderClient.GetStringAsync(OrderingScenariosBase.Get.OrderBy(id)); + order = JsonConvert.DeserializeObject(orderDetails); + + if (IsOrderCreated(order, city)) + { + break; + } + } + + return order; + } + + private bool IsOrderCreated(Order order, string city) { - List orderItemsList = new List(); - orderItemsList.Add(new OrderItemDTO() - { - ProductId = 1, - Discount = 8M, - UnitPrice = 10, - Units = 1, - ProductName = "Some name" - } - ); - - var order = new CreateOrderCommand( - orderItemsList, - cardExpiration: DateTime.UtcNow.AddYears(1), - cardNumber: "5145-555-5555", - cardHolderName: "Jhon Senna", - cardSecurityNumber: "232", - cardTypeId: 1, - city: "Redmon", - country: "USA", - state: "WA", - street: "One way", - zipcode: "zipcode", - paymentId: 1, - buyerId: 3 - ); + return order.City == city; + } + string BuildBasket() + { + var order = new CustomerBasket("1234"); + order.Items = new List() + { + new Microsoft.eShopOnContainers.Services.Basket.API.Model.BasketItem() + { + Id = "1", + ProductName = "ProductName", + ProductId = "1", + UnitPrice = 10, + Quantity = 1 + } + }; return JsonConvert.SerializeObject(order); - } + } + + string BuildCancelOrder(string orderId) + { + var order = new OrderDTO() + { + OrderNumber = orderId + }; + return JsonConvert.SerializeObject(order); + } + + string BuildCheckout(string cityExpected) + { + var checkoutBasket = new BasketDTO() + { + City = cityExpected, + Street = "street", + State = "state", + Country = "coutry", + ZipCode = "zipcode", + CardNumber = "CardNumber", + CardHolderName = "CardHolderName", + CardExpiration = DateTime.Now.AddYears(1), + CardSecurityNumber = "1234", + CardTypeId = 1, + Buyer = "Buyer", + RequestId = Guid.NewGuid() + }; + + return JsonConvert.SerializeObject(checkoutBasket); + } } } diff --git a/test/Services/FunctionalTests/Services/Ordering/OrderingScenariosBase.cs b/test/Services/FunctionalTests/Services/Ordering/OrderingScenariosBase.cs index 8c3c554e5..93391121d 100644 --- a/test/Services/FunctionalTests/Services/Ordering/OrderingScenariosBase.cs +++ b/test/Services/FunctionalTests/Services/Ordering/OrderingScenariosBase.cs @@ -32,5 +32,18 @@ namespace FunctionalTests.Services.Ordering { public static string AddNewOrder = "api/v1/orders/new"; } + + public static class Put + { + public static string CancelOrder = "api/v1/orders/cancel"; + } + + public static class Delete + { + public static string OrderBy(int id) + { + return $"api/v1/orders/{id}"; + } + } } } diff --git a/test/Services/IntegrationTests/IntegrationTests.csproj b/test/Services/IntegrationTests/IntegrationTests.csproj index 6b3ee1948..d02e1b312 100644 --- a/test/Services/IntegrationTests/IntegrationTests.csproj +++ b/test/Services/IntegrationTests/IntegrationTests.csproj @@ -18,6 +18,9 @@ + + PreserveNewest + PreserveNewest @@ -38,6 +41,7 @@ + diff --git a/test/Services/IntegrationTests/Services/Basket/BasketScenarioBase.cs b/test/Services/IntegrationTests/Services/Basket/BasketScenarioBase.cs new file mode 100644 index 000000000..cca3bf306 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Basket/BasketScenarioBase.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.eShopOnContainers.Services.Basket.API; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace IntegrationTests.Services.Basket +{ + public class BasketScenarioBase + { + public TestServer CreateServer() + { + var webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\basket"); + webHostBuilder.UseStartup(); + + return new TestServer(webHostBuilder); + } + + public static class Get + { + public static string GetBasket(int id) + { + return $"{id}"; + } + } + + public static class Post + { + public static string Basket = ""; + public static string CheckoutOrder = "checkout"; + } + } +} diff --git a/test/Services/IntegrationTests/Services/Basket/BasketScenarios.cs b/test/Services/IntegrationTests/Services/Basket/BasketScenarios.cs new file mode 100644 index 000000000..451b1fbf7 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Basket/BasketScenarios.cs @@ -0,0 +1,85 @@ +using IntegrationTests.Services.Extensions; +using Microsoft.eShopOnContainers.Services.Basket.API.Model; +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using WebMVC.Models; +using Xunit; + +namespace IntegrationTests.Services.Basket +{ + public class BasketScenarios + : BasketScenarioBase + { + [Fact] + public async Task Post_basket_and_response_ok_status_code() + { + using (var server = CreateServer()) + { + var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); + var response = await server.CreateClient() + .PostAsync(Post.Basket, content); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Get_basket_and_response_ok_status_code() + { + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.GetBasket(1)); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Send_Checkout_basket_and_response_ok_status_code() + { + using (var server = CreateServer()) + { + var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); + await server.CreateClient() + .PostAsync(Post.Basket, contentBasket); + + var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json"); + var response = await server.CreateIdempotentClient() + .PostAsync(Post.CheckoutOrder, contentCheckout); + + response.EnsureSuccessStatusCode(); + } + } + + string BuildBasket() + { + var order = new CustomerBasket("1234"); + return JsonConvert.SerializeObject(order); + } + + string BuildCheckout() + { + var checkoutBasket = new BasketDTO() + { + City = "city", + Street = "street", + State = "state", + Country = "coutry", + ZipCode = "zipcode", + CardNumber = "CardNumber", + CardHolderName = "CardHolderName", + CardExpiration = DateTime.UtcNow, + CardSecurityNumber = "1234", + CardTypeId = 1, + Buyer = "Buyer", + RequestId = Guid.NewGuid() + }; + + return JsonConvert.SerializeObject(checkoutBasket); + } + } +} diff --git a/test/Services/IntegrationTests/Services/Basket/BasketTestsStartup.cs b/test/Services/IntegrationTests/Services/Basket/BasketTestsStartup.cs new file mode 100644 index 000000000..788c9621d --- /dev/null +++ b/test/Services/IntegrationTests/Services/Basket/BasketTestsStartup.cs @@ -0,0 +1,26 @@ +using IntegrationTests.Middleware; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.eShopOnContainers.Services.Basket.API; + +namespace IntegrationTests.Services.Basket +{ + public class BasketTestsStartup : Startup + { + public BasketTestsStartup(IHostingEnvironment env) : base(env) + { + } + + protected override void ConfigureAuth(IApplicationBuilder app) + { + if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) + { + app.UseMiddleware(); + } + else + { + base.ConfigureAuth(app); + } + } + } +} diff --git a/test/Services/IntegrationTests/Services/Basket/appsettings.json b/test/Services/IntegrationTests/Services/Basket/appsettings.json new file mode 100644 index 000000000..0d84f580e --- /dev/null +++ b/test/Services/IntegrationTests/Services/Basket/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "IdentityUrl": "http://localhost:5105", + "ConnectionString": "127.0.0.1", + "isTest": "true", + "EventBusConnection": "localhost" +} + + diff --git a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarioBase.cs b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarioBase.cs index b4909e5a6..a815886e8 100644 --- a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarioBase.cs +++ b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarioBase.cs @@ -26,9 +26,10 @@ } } - public static class Post + public static class Put { - public static string AddNewOrder = "api/v1/orders/new"; + public static string CancelOrder = "api/v1/orders/cancel"; + public static string ShipOrder = "api/v1/orders/ship"; } } } diff --git a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs index 52119997d..d1381a92f 100644 --- a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs +++ b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs @@ -1,17 +1,13 @@ namespace IntegrationTests.Services.Ordering { using IntegrationTests.Services.Extensions; - using Microsoft.AspNetCore.TestHost; - using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Newtonsoft.Json; - using System; + using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; + using WebMVC.Models; using Xunit; - using System.Collections; - using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; - using System.Collections.Generic; public class OrderingScenarios : OrderingScenarioBase @@ -29,86 +25,38 @@ } [Fact] - public async Task AddNewOrder_add_new_order_and_response_ok_status_code() + public async Task Cancel_order_no_order_created_response_bad_status_code() { using (var server = CreateServer()) - { + { var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"); var response = await server.CreateIdempotentClient() - .PostAsync(Post.AddNewOrder, content); + .PutAsync(Put.CancelOrder, content); - response.EnsureSuccessStatusCode(); + Assert.Equal(response.StatusCode, HttpStatusCode.InternalServerError); } } [Fact] - public async Task AddNewOrder_response_bad_request_if_card_expiration_is_invalid() + public async Task Ship_order_no_order_created_response_bad_status_code() { using (var server = CreateServer()) { - var content = new StringContent(BuildOrderWithInvalidExperationTime(), UTF8Encoding.UTF8, "application/json"); - + var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"); var response = await server.CreateIdempotentClient() - .PostAsync(Post.AddNewOrder, content); + .PutAsync(Put.ShipOrder, content); - Assert.True(response.StatusCode == System.Net.HttpStatusCode.BadRequest); + Assert.Equal(response.StatusCode, HttpStatusCode.InternalServerError); } } - //public CreateOrderCommand(string city, string street, string state, string country, string zipcode, - // string cardNumber, string cardHolderName, DateTime cardExpiration, - // string cardSecurityNumber, int cardTypeId, int paymentId, int buyerId) : this() - string BuildOrder() { - List orderItemsList = new List(); - orderItemsList.Add(new OrderItemDTO() - { - ProductId = 1, - Discount = 10M, - UnitPrice = 10, - Units = 1, - ProductName = "Some name" - } - ); - - var order = new CreateOrderCommand( - orderItemsList, - cardExpiration: DateTime.UtcNow.AddYears(1), - cardNumber: "5145-555-5555", - cardHolderName: "Jhon Senna", - cardSecurityNumber: "232", - cardTypeId: 1, - city: "Redmon", - country: "USA", - state: "WA", - street: "One way", - zipcode: "zipcode", - paymentId: 1, - buyerId: 1 - ); - - return JsonConvert.SerializeObject(order); - } - string BuildOrderWithInvalidExperationTime() - { - var order = new CreateOrderCommand( - new List(), - cardExpiration: DateTime.UtcNow.AddYears(-1), - cardNumber: "5145-555-5555", - cardHolderName: "Jhon Senna", - cardSecurityNumber: "232", - cardTypeId: 1, - city: "Redmon", - country: "USA", - state: "WA", - street: "One way", - zipcode: "zipcode", - buyerId: 1, - paymentId:1 - ); - + var order = new OrderDTO() + { + OrderNumber = "1" + }; return JsonConvert.SerializeObject(order); - } + } } } diff --git a/test/Services/UnitTest/Basket/Application/BasketWebApiTest.cs b/test/Services/UnitTest/Basket/Application/BasketWebApiTest.cs index 6c5f116bd..7e17a9980 100644 --- a/test/Services/UnitTest/Basket/Application/BasketWebApiTest.cs +++ b/test/Services/UnitTest/Basket/Application/BasketWebApiTest.cs @@ -1,20 +1,29 @@ -using Microsoft.AspNetCore.Mvc; +using Basket.API.IntegrationEvents.Events; +using Basket.API.Model; +using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.Services.Basket.API.Controllers; using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Moq; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; +using IBasketIdentityService = Microsoft.eShopOnContainers.Services.Basket.API.Services.IIdentityService; namespace UnitTest.Basket.Application { public class BasketWebApiTest { private readonly Mock _basketRepositoryMock; + private readonly Mock _identityServiceMock; + private readonly Mock _serviceBusMock; public BasketWebApiTest() { _basketRepositoryMock = new Mock(); + _identityServiceMock = new Mock(); + _serviceBusMock = new Mock(); } [Fact] @@ -26,9 +35,12 @@ namespace UnitTest.Basket.Application _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) .Returns(Task.FromResult(fakeCustomerBasket)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + _serviceBusMock.Setup(x => x.Publish(It.IsAny())); //Act - var basketController = new BasketController(_basketRepositoryMock.Object); + var basketController = new BasketController( + _basketRepositoryMock.Object, _identityServiceMock.Object, _serviceBusMock.Object); var actionResult = await basketController.Get(fakeCustomerId) as OkObjectResult; //Assert @@ -45,9 +57,12 @@ namespace UnitTest.Basket.Application _basketRepositoryMock.Setup(x => x.UpdateBasketAsync(It.IsAny())) .Returns(Task.FromResult(fakeCustomerBasket)); - + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + _serviceBusMock.Setup(x => x.Publish(It.IsAny())); //Act - var basketController = new BasketController(_basketRepositoryMock.Object); + var basketController = new BasketController( + _basketRepositoryMock.Object, _identityServiceMock.Object, _serviceBusMock.Object); + var actionResult = await basketController.Post(fakeCustomerBasket) as OkObjectResult; //Assert @@ -55,6 +70,38 @@ namespace UnitTest.Basket.Application Assert.Equal(((CustomerBasket)actionResult.Value).BuyerId, fakeCustomerId); } + [Fact] + public async Task Doing_Checkout_Without_Basket_Should_Return_Bad_Request() + { + var fakeCustomerId = "2"; + _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) + .Returns(Task.FromResult((CustomerBasket)null)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + //Act + var basketController = new BasketController( + _basketRepositoryMock.Object, _identityServiceMock.Object, _serviceBusMock.Object); + + var result = await basketController.Checkout(new BasketCheckout(), Guid.NewGuid().ToString()) as BadRequestResult; + Assert.NotNull(result); + } + + [Fact] + public async Task Doing_Checkout_Wit_Basket_Should_Publish_UserCheckoutAccepted_Integration_Event() + { + var fakeCustomerId = "1"; + var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); + _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) + .Returns(Task.FromResult(fakeCustomerBasket)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + //Act + var basketController = new BasketController( + _basketRepositoryMock.Object, _identityServiceMock.Object, _serviceBusMock.Object); + + var result = await basketController.Checkout(new BasketCheckout(), Guid.NewGuid().ToString()) as AcceptedResult; + _serviceBusMock.Verify(mock => mock.Publish(It.IsAny()), Times.Once); + Assert.NotNull(result); + } + private CustomerBasket GetCustomerBasketFake(string fakeCustomerId) { return new CustomerBasket(fakeCustomerId) diff --git a/test/Services/UnitTest/Ordering/Application/IdentifierCommandHandlerTest.cs b/test/Services/UnitTest/Ordering/Application/IdentifierCommandHandlerTest.cs index 76db4b4aa..fcddb53a8 100644 --- a/test/Services/UnitTest/Ordering/Application/IdentifierCommandHandlerTest.cs +++ b/test/Services/UnitTest/Ordering/Application/IdentifierCommandHandlerTest.cs @@ -4,6 +4,7 @@ using System.Text; namespace UnitTest.Ordering.Application { + using global::Ordering.API.Application.Models; using MediatR; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; @@ -70,7 +71,8 @@ namespace UnitTest.Ordering.Application private CreateOrderCommand FakeOrderRequest(Dictionary args = null) { return new CreateOrderCommand( - null, + new List(), + userId: args != null && args.ContainsKey("userId") ? (string)args["userId"] : null, city: args != null && args.ContainsKey("city") ? (string)args["city"] : null, street: args != null && args.ContainsKey("street") ? (string)args["street"] : null, state: args != null && args.ContainsKey("state") ? (string)args["state"] : null, @@ -80,9 +82,7 @@ namespace UnitTest.Ordering.Application cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue, cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123", cardHolderName: args != null && args.ContainsKey("cardHolderName") ? (string)args["cardHolderName"] : "XXX", - cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0, - paymentId: args != null && args.ContainsKey("paymentId") ? (int)args["paymentId"] : 0, - buyerId: args != null && args.ContainsKey("buyerId") ? (int)args["buyerId"] : 0); + cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0); } } } diff --git a/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs b/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs index 1615bcd0b..67929b02a 100644 --- a/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs +++ b/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; namespace UnitTest.Ordering.Application { + using global::Ordering.API.Application.Models; using MediatR; using System.Collections; using System.Collections.Generic; @@ -68,13 +69,14 @@ namespace UnitTest.Ordering.Application private Order FakeOrder() { - return new Order(new Address("street", "city", "state", "country", "zipcode"), 1, "12", "111", "fakeName", DateTime.Now.AddYears(1)); + return new Order("1", new Address("street", "city", "state", "country", "zipcode"), 1, "12", "111", "fakeName", DateTime.Now.AddYears(1)); } private CreateOrderCommand FakeOrderRequestWithBuyer(Dictionary args = null) { return new CreateOrderCommand( - new List(), + new List(), + userId: args != null && args.ContainsKey("userId") ? (string)args["userId"] : null, city: args != null && args.ContainsKey("city") ? (string)args["city"] : null, street: args != null && args.ContainsKey("street") ? (string)args["street"] : null, state: args != null && args.ContainsKey("state") ? (string)args["state"] : null, @@ -84,9 +86,7 @@ namespace UnitTest.Ordering.Application cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue, cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123", cardHolderName: args != null && args.ContainsKey("cardHolderName") ? (string)args["cardHolderName"] : "XXX", - cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0, - paymentId: args != null && args.ContainsKey("paymentId") ? (int)args["paymentId"] : 0, - buyerId: args != null && args.ContainsKey("buyerId") ? (int)args["buyerId"] : 0); + cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0); } } } diff --git a/test/Services/UnitTest/Ordering/Application/OrderControllerTest.cs b/test/Services/UnitTest/Ordering/Application/OrderControllerTest.cs index 27c524010..12f2be395 100644 --- a/test/Services/UnitTest/Ordering/Application/OrderControllerTest.cs +++ b/test/Services/UnitTest/Ordering/Application/OrderControllerTest.cs @@ -93,53 +93,7 @@ namespace UnitTest.Ordering.Application //Assert var viewResult = Assert.IsType(actionResult); Assert.IsAssignableFrom(viewResult.ViewData.Model); - } - - [Fact] - public async Task Post_create_order_success() - { - //Arrange - var fakeOrder = GetFakeOrder(); - - _basketServiceMock.Setup(x => x.CleanBasket(It.IsAny())) - .Returns(Task.FromResult(1)); - - _orderServiceMock.Setup(x => x.CreateOrder(It.IsAny())) - .Returns(Task.FromResult(1)); - - //Act - var orderController = new OrderController(_orderServiceMock.Object, _basketServiceMock.Object, _identityParserMock.Object); - orderController.ControllerContext.HttpContext = _contextMock.Object; - var actionResult = await orderController.Create(fakeOrder, "fakeAction"); - - //Assert - var redirectToActionResult = Assert.IsType(actionResult); - Assert.Null(redirectToActionResult.ControllerName); - Assert.Equal("Index", redirectToActionResult.ActionName); - } - - [Fact] - public async Task Post_create_order_fail() - { - //Arrange - var fakeOrder = GetFakeOrder(); - - _basketServiceMock.Setup(x => x.CleanBasket(It.IsAny())) - .Returns(Task.FromResult(1)); - - _orderServiceMock.Setup(x => x.CreateOrder(It.IsAny())) - .Returns(Task.FromResult(1)); - - //Act - var orderController = new OrderController(_orderServiceMock.Object, _basketServiceMock.Object, _identityParserMock.Object); - orderController.ControllerContext.HttpContext = _contextMock.Object; - orderController.ModelState.AddModelError("fakeError", "fakeError"); - var actionResult = await orderController.Create(fakeOrder, "action"); - - //Assert - var viewResult = Assert.IsType(actionResult); - Assert.IsAssignableFrom(viewResult.ViewData.Model); - } + } private BasketModel GetFakeBasket(string buyerId) { diff --git a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs index c3fb0d95e..8210deb7c 100644 --- a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs +++ b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs @@ -5,8 +5,10 @@ using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries; using Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; using Moq; +using Ordering.API.Application.Commands; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -27,6 +29,37 @@ namespace UnitTest.Ordering.Application [Fact] public async Task Create_order_with_requestId_success() + { + //Arrange + _mediatorMock.Setup(x => x.Send(It.IsAny>(), default(CancellationToken))) + .Returns(Task.FromResult(true)); + + //Act + var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object); + var actionResult = await orderController.CancelOrder(new CancelOrderCommand(1), Guid.NewGuid().ToString()) as OkResult; + + //Assert + Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.OK); + + } + + [Fact] + public async Task Cancel_order_bad_request() + { + //Arrange + _mediatorMock.Setup(x => x.Send(It.IsAny>(), default(CancellationToken))) + .Returns(Task.FromResult(true)); + + //Act + var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object); + var actionResult = await orderController.CancelOrder(new CancelOrderCommand(1), String.Empty) as BadRequestResult; + + //Assert + Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Ship_order_with_requestId_success() { //Arrange _mediatorMock.Setup(x => x.Send(It.IsAny>(), default(System.Threading.CancellationToken))) @@ -34,7 +67,7 @@ namespace UnitTest.Ordering.Application //Act var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object); - var actionResult = await orderController.CreateOrder(new CreateOrderCommand(), Guid.NewGuid().ToString()) as OkResult; + var actionResult = await orderController.ShipOrder(new ShipOrderCommand(1), Guid.NewGuid().ToString()) as OkResult; //Assert Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.OK); @@ -42,7 +75,7 @@ namespace UnitTest.Ordering.Application } [Fact] - public async Task Create_order_bad_request() + public async Task Ship_order_bad_request() { //Arrange _mediatorMock.Setup(x => x.Send(It.IsAny>(), default(System.Threading.CancellationToken))) @@ -50,7 +83,7 @@ namespace UnitTest.Ordering.Application //Act var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object); - var actionResult = await orderController.CreateOrder(new CreateOrderCommand(), String.Empty) as BadRequestResult; + var actionResult = await orderController.ShipOrder(new ShipOrderCommand(1), String.Empty) as BadRequestResult; //Assert Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.BadRequest); diff --git a/test/Services/UnitTest/Ordering/Domain/OrderAggregateTest.cs b/test/Services/UnitTest/Ordering/Domain/OrderAggregateTest.cs index 40bc66431..dfa31eb62 100644 --- a/test/Services/UnitTest/Ordering/Domain/OrderAggregateTest.cs +++ b/test/Services/UnitTest/Ordering/Domain/OrderAggregateTest.cs @@ -96,7 +96,8 @@ public class OrderAggregateTest [Fact] public void Add_new_Order_raises_new_event() { - //Arrange + //Arrange + var userId = new Guid(); var street = "fakeStreet"; var city = "FakeCity"; var state = "fakeState"; @@ -110,7 +111,7 @@ public class OrderAggregateTest var expectedResult = 1; //Act - var fakeOrder = new Order(new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); + var fakeOrder = new Order("1", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); //Assert Assert.Equal(fakeOrder.DomainEvents.Count, expectedResult); @@ -119,7 +120,8 @@ public class OrderAggregateTest [Fact] public void Add_event_Order_explicitly_raises_new_event() { - //Arrange + //Arrange + var userId = new Guid(); var street = "fakeStreet"; var city = "FakeCity"; var state = "fakeState"; @@ -133,8 +135,8 @@ public class OrderAggregateTest var expectedResult = 2; //Act - var fakeOrder = new Order(new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); - fakeOrder.AddDomainEvent(new OrderStartedDomainEvent(fakeOrder,cardTypeId,cardNumber,cardSecurityNumber,cardHolderName,cardExpiration)); + var fakeOrder = new Order("1", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); + fakeOrder.AddDomainEvent(new OrderStartedDomainEvent(fakeOrder, "1", cardTypeId,cardNumber,cardSecurityNumber,cardHolderName,cardExpiration)); //Assert Assert.Equal(fakeOrder.DomainEvents.Count, expectedResult); } @@ -143,6 +145,7 @@ public class OrderAggregateTest public void Remove_event_Order_explicitly() { //Arrange + var userId = new Guid(); var street = "fakeStreet"; var city = "FakeCity"; var state = "fakeState"; @@ -153,8 +156,8 @@ public class OrderAggregateTest var cardSecurityNumber = "123"; var cardHolderName = "FakeName"; var cardExpiration = DateTime.Now.AddYears(1); - var fakeOrder = new Order(new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); - var @fakeEvent = new OrderStartedDomainEvent(fakeOrder, cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); + var fakeOrder = new Order("1", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); + var @fakeEvent = new OrderStartedDomainEvent(fakeOrder, "1", cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); var expectedResult = 1; //Act diff --git a/test/Services/UnitTest/UnitTest.csproj b/test/Services/UnitTest/UnitTest.csproj index 8955e4a45..2cf0dfbb0 100644 --- a/test/Services/UnitTest/UnitTest.csproj +++ b/test/Services/UnitTest/UnitTest.csproj @@ -10,6 +10,7 @@ +