Browse Source

Initial version [need refactoring]

pull/2022/head
Dzmitry Ausiyevich 2 years ago
parent
commit
4fd5530a0d
64 changed files with 2105 additions and 22889 deletions
  1. +1
    -1
      src/Services/Basket/Basket.API/Controllers/BasketController.cs
  2. +4
    -1
      src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs
  3. +2
    -0
      src/Services/Basket/Basket.API/Model/BasketCheckout.cs
  4. +36
    -0
      src/Services/Coupon/Coupon.API/Controllers/CouponController.cs
  5. +44
    -0
      src/Services/Coupon/Coupon.API/Coupon.API.csproj
  6. +6
    -0
      src/Services/Coupon/Coupon.API/CouponSettings.cs
  7. +40
    -0
      src/Services/Coupon/Coupon.API/CustomExtensionMethods.cs
  8. +11
    -0
      src/Services/Coupon/Coupon.API/DTOs/CouponDto.cs
  9. +59
    -0
      src/Services/Coupon/Coupon.API/Dockerfile
  10. +13
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs
  11. +30
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/CouponRepository.cs
  12. +16
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/Exceptions/CouponDomainException.cs
  13. +54
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs
  14. +9
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/Filters/JsonErrorResponse.cs
  15. +11
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/ICouponRepository.cs
  16. +20
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/Models/Coupon.cs
  17. +11
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/MongoDbSettings.cs
  18. +58
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs
  19. +12
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs
  20. +12
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs
  21. +14
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs
  22. +34
    -0
      src/Services/Coupon/Coupon.API/Middlewares/AuthorizeCheckOperationFilter.cs
  23. +6
    -0
      src/Services/Coupon/Coupon.API/Middlewares/FailingOptions.cs
  24. +14
    -0
      src/Services/Coupon/Coupon.API/Middlewares/WebHostBuildertExtensions.cs
  25. +108
    -0
      src/Services/Coupon/Coupon.API/Program.cs
  26. +37
    -0
      src/Services/Coupon/Coupon.API/Properties/launchSettings.json
  27. +7
    -0
      src/Services/Coupon/Coupon.API/Services/IIdentityService.cs
  28. +17
    -0
      src/Services/Coupon/Coupon.API/Services/IdentityService.cs
  29. +307
    -0
      src/Services/Coupon/Coupon.API/Startup.cs
  30. +8
    -0
      src/Services/Coupon/Coupon.API/appsettings.Development.json
  31. +13
    -0
      src/Services/Coupon/Coupon.API/appsettings.json
  32. +5
    -0
      src/Services/Coupon/Coupon.API/libman.json
  33. +5
    -1
      src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs
  34. +1
    -1
      src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs
  35. +10
    -4
      src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs
  36. +46
    -0
      src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponConfirmedIntegrationEventHandler.cs
  37. +37
    -0
      src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponRejectedIntegrationEventHandler.cs
  38. +5
    -2
      src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs
  39. +10
    -0
      src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs
  40. +10
    -0
      src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs
  41. +12
    -0
      src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs
  42. +4
    -1
      src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs
  43. +4
    -2
      src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs
  44. +2
    -0
      src/Services/Ordering/Ordering.API/Application/Queries/OrderViewModel.cs
  45. +9
    -1
      src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20190808132242_Change_Relation_Of_Orders.cs
  46. +8
    -0
      src/Services/Ordering/Ordering.API/Ordering.API.csproj
  47. +4
    -0
      src/Services/Ordering/Ordering.API/Startup.cs
  48. +4
    -1
      src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs
  49. +4
    -0
      src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj
  50. +666
    -22807
      src/Web/WebSPA/Client/package-lock.json
  51. +12
    -1
      src/Web/WebSPA/Client/src/modules/orders/orders-detail/orders-detail.component.html
  52. +33
    -1
      src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.html
  53. +13
    -0
      src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.ts
  54. +1
    -0
      src/Web/WebSPA/Client/src/modules/shared/models/basketCheckout.model.ts
  55. +1
    -0
      src/Web/WebSPA/Client/src/modules/shared/models/configuration.model.ts
  56. +5
    -0
      src/Web/WebSPA/Client/src/modules/shared/models/coupon.model.ts
  57. +2
    -0
      src/Web/WebSPA/Client/src/modules/shared/models/order-detail.model.ts
  58. +14
    -0
      src/Web/WebSPA/Dockerfile
  59. +68
    -58
      src/Web/WebSPA/WebSPA.csproj
  60. +1
    -0
      src/Web/WebSPA/appsettings.json
  61. +3
    -0
      src/Web/WebSPA/package-lock.json
  62. +24
    -3
      src/docker-compose.override.yml
  63. +10
    -0
      src/docker-compose.yml
  64. +58
    -4
      src/eShopOnContainers-ServicesAndWebApps.sln

+ 1
- 1
src/Services/Basket/Basket.API/Controllers/BasketController.cs View File

@ -60,7 +60,7 @@ public class BasketController : ControllerBase
var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street, var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street,
basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName, basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName,
basketCheckout.CardExpiration, basketCheckout.CardSecurityNumber, basketCheckout.CardTypeId, basketCheckout.Buyer, basketCheckout.RequestId, basket);
basketCheckout.CardExpiration, basketCheckout.CardSecurityNumber, basketCheckout.CardTypeId, basketCheckout.Buyer, basketCheckout.RequestId, basket, basketCheckout.CouponCode);
// Once basket is checkout, sends an integration event to // Once basket is checkout, sends an integration event to
// ordering.api to convert basket to order and proceeds with // ordering.api to convert basket to order and proceeds with


+ 4
- 1
src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs View File

@ -34,10 +34,12 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
public CustomerBasket Basket { get; } public CustomerBasket Basket { get; }
public string CouponCode { get; init; }
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street, public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
string state, string country, string zipCode, string cardNumber, string cardHolderName, string state, string country, string zipCode, string cardNumber, string cardHolderName,
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId, DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
CustomerBasket basket)
CustomerBasket basket, string couponCode = null)
{ {
UserId = userId; UserId = userId;
UserName = userName; UserName = userName;
@ -54,6 +56,7 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
Buyer = buyer; Buyer = buyer;
Basket = basket; Basket = basket;
RequestId = requestId; RequestId = requestId;
CouponCode = couponCode;
} }
} }

+ 2
- 0
src/Services/Basket/Basket.API/Model/BasketCheckout.cs View File

@ -25,4 +25,6 @@ public class BasketCheckout
public string Buyer { get; set; } public string Buyer { get; set; }
public Guid RequestId { get; set; } public Guid RequestId { get; set; }
public string CouponCode { get; set; }
} }

+ 36
- 0
src/Services/Coupon/Coupon.API/Controllers/CouponController.cs View File

@ -0,0 +1,36 @@
using Coupon.API.DTOs;
using Coupon.API.Infrastructure;
using Microsoft.AspNetCore.Mvc;
namespace Coupon.API.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class CouponController: ControllerBase
{
private ICouponRepository _couponRepository;
public CouponController(ICouponRepository couponeRepository) =>
_couponRepository = couponeRepository;
[HttpGet("value/{code}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CouponDto>> GetCouponByCodeAsync(string code)
{
var coupon = await _couponRepository.FindByCodeAsync(code);
if (coupon is null || coupon.Consumed)
{
return NotFound();
}
return new CouponDto
{
Code = code,
Consumed = coupon.Consumed,
};
}
}
}

+ 44
- 0
src/Services/Coupon/Coupon.API/Coupon.API.csproj View File

@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..\..</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<None Remove="Middlewares\AuthorizeCheckOperationFilter.cs~RF427f162.TMP" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" />
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="6.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="6.0.4" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0-preview.1" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
<PackageReference Include="Azure.Identity" Version="1.9.0-beta.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.21.0" />
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
<PackageReference Include="MongoDB.Driver" Version="2.18.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageReference Include="Serilog.Sinks.Http" Version="8.0.0-beta.9" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.2.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" />
</ItemGroup>
</Project>

+ 6
- 0
src/Services/Coupon/Coupon.API/CouponSettings.cs View File

@ -0,0 +1,6 @@
namespace Coupon.API
{
public class CouponSettings
{
}
}

+ 40
- 0
src/Services/Coupon/Coupon.API/CustomExtensionMethods.cs View File

@ -0,0 +1,40 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Coupon.API
{
public static class CustomExtensionMethods
{
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
{
var hcBuilder = services.AddHealthChecks();
hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy());
hcBuilder
.AddRedis(
configuration["ConnectionString"],
name: "nosqldata-check",
tags: new string[] { "nosqldata" });
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
hcBuilder
.AddAzureServiceBusTopic(
configuration["EventBusConnection"],
topicName: "eshop_event_bus",
name: "coupon-servicebus-check",
tags: new string[] { "servicebus" });
}
else
{
hcBuilder
.AddRabbitMQ(
$"amqp://{configuration["EventBusConnection"]}",
name: "coupon-rabbitmqbus-check",
tags: new string[] { "rabbitmqbus" });
}
return services;
}
}
}

+ 11
- 0
src/Services/Coupon/Coupon.API/DTOs/CouponDto.cs View File

@ -0,0 +1,11 @@
namespace Coupon.API.DTOs
{
public record CouponDto
{
public int Discount { get; init; }
public string Code { get; init; }
public bool Consumed { get; init; }
}
}

+ 59
- 0
src/Services/Coupon/Coupon.API/Dockerfile View File

@ -0,0 +1,59 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles
# to take advantage of Docker's build cache, to speed up local container builds
COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWebApps.sln"
COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj"
COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj"
COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj"
COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj"
COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj"
COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj"
COPY "BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj" "BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj"
COPY "BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj" "BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj"
COPY "BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj" "BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj"
COPY "Services/Basket/Basket.API/Basket.API.csproj" "Services/Basket/Basket.API/Basket.API.csproj"
COPY "Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj" "Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj"
COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj"
COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj"
COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj"
COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj"
COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj"
COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj"
COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj"
COPY "Services/Ordering/Ordering.Domain/Ordering.Domain.csproj" "Services/Ordering/Ordering.Domain/Ordering.Domain.csproj"
COPY "Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj" "Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj"
COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"
COPY "Web/WebMVC/WebMVC.csproj" "Web/WebMVC/WebMVC.csproj"
COPY "Web/WebSPA/WebSPA.csproj" "Web/WebSPA/WebSPA.csproj"
COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj"
COPY "docker-compose.dcproj" "docker-compose.dcproj"
COPY "NuGet.config" "NuGet.config"
RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln"
COPY . .
WORKDIR /src/Services/Coupon/Coupon.API
RUN dotnet publish --no-restore -c Release -o /app
FROM build AS publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "Coupon.API.dll"]

+ 13
- 0
src/Services/Coupon/Coupon.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
namespace Coupon.API.Infrastructure.ActionResults
{
public class InternalServerErrorObjectResult : ObjectResult
{
public InternalServerErrorObjectResult(object error)
: base(error)
{
StatusCode = StatusCodes.Status500InternalServerError;
}
}
}

+ 30
- 0
src/Services/Coupon/Coupon.API/Infrastructure/CouponRepository.cs View File

@ -0,0 +1,30 @@
using Microsoft.Extensions.Options;
using MongoDB.Driver;
namespace Coupon.API.Infrastructure
{
public class CouponRepository: ICouponRepository
{
private readonly IMongoCollection<Models.Coupon> _couponsCollection;
public CouponRepository()
{
var mongoClient = new MongoClient("mongodb://nosqldata:27017");
var mongoDatabase = mongoClient.GetDatabase("eshop");
_couponsCollection = mongoDatabase.GetCollection<Models.Coupon>("coupons");
}
public async Task<Models.Coupon> FindByCodeAsync(string code) =>
await _couponsCollection
.Find(x => string.Equals(x.Code, code))
.FirstOrDefaultAsync();
public async Task AddAsync(Models.Coupon coupon) =>
await _couponsCollection.InsertOneAsync(coupon);
public async Task UpdateAsync(Models.Coupon updatedCoupon) =>
await _couponsCollection.ReplaceOneAsync(x => x.Id == updatedCoupon.Id, updatedCoupon);
}
}

+ 16
- 0
src/Services/Coupon/Coupon.API/Infrastructure/Exceptions/CouponDomainException.cs View File

@ -0,0 +1,16 @@
namespace Coupon.API.Infrastructure.Exceptions
{
public class CouponDomainException : Exception
{
public CouponDomainException()
{ }
public CouponDomainException(string message)
: base(message)
{ }
public CouponDomainException(string message, Exception innerException)
: base(message, innerException)
{ }
}
}

+ 54
- 0
src/Services/Coupon/Coupon.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs View File

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using Coupon.API.Infrastructure.Exceptions;
using Coupon.API.Infrastructure.ActionResults;
namespace Coupon.API.Infrastructure.Filters
{
public partial class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly IWebHostEnvironment env;
private readonly ILogger<HttpGlobalExceptionFilter> logger;
public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
{
this.env = env;
this.logger = logger;
}
public void OnException(ExceptionContext context)
{
logger.LogError(new EventId(context.Exception.HResult),
context.Exception,
context.Exception.Message);
if (context.Exception.GetType() == typeof(CouponDomainException))
{
var json = new JsonErrorResponse
{
Messages = new[] { context.Exception.Message }
};
context.Result = new BadRequestObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
else
{
var json = new JsonErrorResponse
{
Messages = new[] { "An error occurred. Try it again." }
};
if (env.IsDevelopment())
{
json.DeveloperMessage = context.Exception;
}
context.Result = new InternalServerErrorObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
context.ExceptionHandled = true;
}
}
}

+ 9
- 0
src/Services/Coupon/Coupon.API/Infrastructure/Filters/JsonErrorResponse.cs View File

@ -0,0 +1,9 @@
namespace Coupon.API.Infrastructure.Filters
{
public class JsonErrorResponse
{
public string[] Messages { get; set; }
public object DeveloperMessage { get; set; }
}
}

+ 11
- 0
src/Services/Coupon/Coupon.API/Infrastructure/ICouponRepository.cs View File

@ -0,0 +1,11 @@
namespace Coupon.API.Infrastructure
{
public interface ICouponRepository
{
Task<Models.Coupon> FindByCodeAsync(string code);
Task AddAsync(Models.Coupon coupon);
Task UpdateAsync(Models.Coupon updatedCoupon);
}
}

+ 20
- 0
src/Services/Coupon/Coupon.API/Infrastructure/Models/Coupon.cs View File

@ -0,0 +1,20 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson;
namespace Coupon.API.Infrastructure.Models
{
public class Coupon
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public int Discount { get; set; }
public string Code { get; set; }
public bool Consumed { get; set; }
public int OrderId { get; set; }
}
}

+ 11
- 0
src/Services/Coupon/Coupon.API/Infrastructure/MongoDbSettings.cs View File

@ -0,0 +1,11 @@
namespace Coupon.API.Infrastructure
{
public class MongoDbSettings
{
public string ConnectionString { get; set; }
public string DatabaseName { get; set; }
public string CouponsCollectionName { get; set; }
}
}

+ 58
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs View File

@ -0,0 +1,58 @@
using Coupon.API.Infrastructure;
using Coupon.API.IntegrationEvents.Events;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
namespace Coupon.API.IntegrationEvents.EventHandling
{
public class OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler
: IIntegrationEventHandler<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent>
{
private readonly ILogger<OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler> _logger;
private readonly ICouponRepository _repository;
private readonly IEventBus _eventBus;
public OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler(
ILogger<OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler> logger,
ICouponRepository repository,
IEventBus eventBus)
{
_logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
_repository = repository ?? throw new System.ArgumentNullException(nameof(repository));
_eventBus = eventBus ?? throw new System.ArgumentNullException(nameof(eventBus));
}
public async Task Handle(OrderStatusChangedToAwaitingCouponValidationIntegrationEvent @event)
{
var coupon = await _repository.FindByCodeAsync(@event.CouponCode);
//add validation of coupon as string
if (coupon == null)
{
var newCoupon = new Infrastructure.Models.Coupon
{
Code = @event.CouponCode,
Consumed = true,
Discount = int.Parse(@event.CouponCode.Split("-").Last()),
OrderId = @event.OrderId
};
await _repository.AddAsync(newCoupon);
var orderCouponConfirmedIntegrationEvent = new OrderCouponConfirmedIntegrationEvent(@event.OrderId);
_eventBus.Publish(orderCouponConfirmedIntegrationEvent);
}
else if (coupon.Consumed)
{
var orderCouponRejectedIntegrationEvent = new OrderCouponRejectedIntegrationEvent(@event.OrderId);
_eventBus.Publish(orderCouponRejectedIntegrationEvent);
}
else
{
coupon.Consumed = true;
coupon.OrderId = @event.OrderId;
await _repository.UpdateAsync(coupon);
var orderCouponConfirmedIntegrationEvent = new OrderCouponConfirmedIntegrationEvent(@event.OrderId);
_eventBus.Publish(orderCouponConfirmedIntegrationEvent);
}
}
}
}

+ 12
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs View File

@ -0,0 +1,12 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
namespace Coupon.API.IntegrationEvents.Events
{
public record OrderCouponConfirmedIntegrationEvent : IntegrationEvent
{
public int OrderId { get; set; }
public OrderCouponConfirmedIntegrationEvent(int orderId)
=> (OrderId) = (orderId);
}
}

+ 12
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs View File

@ -0,0 +1,12 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
namespace Coupon.API.IntegrationEvents.Events
{
public record OrderCouponRejectedIntegrationEvent : IntegrationEvent
{
public int OrderId { get; set; }
public OrderCouponRejectedIntegrationEvent(int orderId)
=> (OrderId) = (orderId);
}
}

+ 14
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs View File

@ -0,0 +1,14 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
namespace Coupon.API.IntegrationEvents.Events
{
public record OrderStatusChangedToAwaitingCouponValidationIntegrationEvent: IntegrationEvent
{
public int OrderId { get; set; }
public string CouponCode { get; init; }
public OrderStatusChangedToAwaitingCouponValidationIntegrationEvent(int orderId, string couponCode)
=> (OrderId, CouponCode) = (orderId, couponCode);
}
}

+ 34
- 0
src/Services/Coupon/Coupon.API/Middlewares/AuthorizeCheckOperationFilter.cs View File

@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Coupon.API.Middlewares
{
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Check for authorize attribute
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
if (!hasAuthorize) return;
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new()
{
[ oAuthScheme ] = new [] { "basketapi" }
}
};
}
}
}

+ 6
- 0
src/Services/Coupon/Coupon.API/Middlewares/FailingOptions.cs View File

@ -0,0 +1,6 @@
namespace Coupon.API.Middlewares
{
public class FailingOptions
{
}
}

+ 14
- 0
src/Services/Coupon/Coupon.API/Middlewares/WebHostBuildertExtensions.cs View File

@ -0,0 +1,14 @@
namespace Coupon.API.Middlewares
{
public static class WebHostBuildertExtensions
{
public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, Action<FailingOptions> options)
{
builder.ConfigureServices(services =>
{
//services.AddSingleton<IStartupFilter>(new FailingStartupFilter(options));
});
return builder;
}
}
}

+ 108
- 0
src/Services/Coupon/Coupon.API/Program.cs View File

@ -0,0 +1,108 @@
using Azure.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore;
using System.Net;
using Serilog;
using Coupon.API;
using Azure.Identity;
var configuration = GetConfiguration();
Log.Logger = CreateSerilogLogger(configuration);
try
{
Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName);
var host = BuildWebHost(configuration, args);
Log.Information("Starting web host ({ApplicationContext})...", Program.AppName);
host.Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName);
return 1;
}
finally
{
Log.CloseAndFlush();
}
IWebHost BuildWebHost(IConfiguration configuration, string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(false)
.ConfigureKestrel(options =>
{
var ports = GetDefinedPorts(configuration);
options.Listen(IPAddress.Any, ports.httpPort, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
});
options.Listen(IPAddress.Any, ports.grpcPort, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
});
})
.ConfigureAppConfiguration(x => x.AddConfiguration(configuration))
//.UseFailing(options =>
//{
// options.ConfigPath = "/Failing";
// options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" });
//})
.UseStartup<Startup>()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseSerilog()
.Build();
Serilog.ILogger CreateSerilogLogger(IConfiguration configuration)
{
var seqServerUrl = configuration["Serilog:SeqServerUrl"];
var logstashUrl = configuration["Serilog:LogstashgUrl"];
return new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.WithProperty("ApplicationContext", Program.AppName)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl)
.WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl)
.ReadFrom.Configuration(configuration)
.CreateLogger();
}
IConfiguration GetConfiguration()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
var config = builder.Build();
if (config.GetValue<bool>("UseVault", false))
{
TokenCredential credential = new ClientSecretCredential(
config["Vault:TenantId"],
config["Vault:ClientId"],
config["Vault:ClientSecret"]);
builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential);
}
return builder.Build();
}
(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config)
{
var grpcPort = config.GetValue("GRPC_PORT", 5001);
var port = config.GetValue("PORT", 80);
return (port, grpcPort);
}
public partial class Program
{
public static string Namespace = typeof(Startup).Namespace;
public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1);
}

+ 37
- 0
src/Services/Coupon/Coupon.API/Properties/launchSettings.json View File

@ -0,0 +1,37 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:37877",
"sslPort": 0
}
},
"profiles": {
"Coupon.API": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5171",
"dotnetRunMessages": true
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"publishAllPorts": true
}
}
}

+ 7
- 0
src/Services/Coupon/Coupon.API/Services/IIdentityService.cs View File

@ -0,0 +1,7 @@
namespace Coupon.API.Services
{
public interface IIdentityService
{
string GetUserIdentity();
}
}

+ 17
- 0
src/Services/Coupon/Coupon.API/Services/IdentityService.cs View File

@ -0,0 +1,17 @@
namespace Coupon.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;
}
}
}

+ 307
- 0
src/Services/Coupon/Coupon.API/Startup.cs View File

@ -0,0 +1,307 @@
using Autofac;
using Coupon.API.Controllers;
using Coupon.API.Infrastructure;
using Coupon.API.Infrastructure.Filters;
using Coupon.API.Middlewares;
using Coupon.API.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using RabbitMQ.Client;
using System.IdentityModel.Tokens.Jwt;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using HealthChecks.UI.Client;
using Coupon.API.IntegrationEvents.EventHandling;
using Coupon.API.IntegrationEvents.Events;
namespace Coupon.API
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
{
//services.AddGrpc(options =>
//{
// options.EnableDetailedErrors = true;
//});
RegisterAppInsights(services);
services.AddControllers(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
//options.Filters.Add(typeof(ValidateModelStateFilter));
}) // Added for functional tests
.AddApplicationPart(typeof(CouponController).Assembly)
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "eShopOnContainers - Coupon HTTP API",
Version = "v1",
Description = "The Coupon Service HTTP API"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows()
{
Implicit = new OpenApiOAuthFlow()
{
AuthorizationUrl = new Uri($"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
TokenUrl = new Uri($"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
Scopes = new Dictionary<string, string>()
{
{ "basket", "Basket API" }
}
}
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
ConfigureAuthService(services);
services.AddCustomHealthCheck(Configuration);
services.Configure<CouponSettings>(Configuration);
//By connecting here we are making sure that our service
//cannot start until redis is ready. This might slow down startup,
//but given that there is a delay on resolving the ip address
//and then creating the connection it seems reasonable to move
//that cost to startup instead of having the first request pay the
//penalty.
//services.AddSingleton<ConnectionMultiplexer>(sp =>
//{
// var settings = sp.GetRequiredService<IOptions<CouponSettings>>().Value;
// var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
// return ConnectionMultiplexer.Connect(configuration);
//});
if (Configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
services.AddSingleton<IServiceBusPersisterConnection>(sp =>
{
var serviceBusConnectionString = Configuration["EventBusConnection"];
return new DefaultServiceBusPersisterConnection(serviceBusConnectionString);
});
}
else
{
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new ConnectionFactory()
{
HostName = Configuration["EventBusConnection"],
DispatchConsumersAsync = true
};
if (!string.IsNullOrEmpty(Configuration["EventBusUserName"]))
{
factory.UserName = Configuration["EventBusUserName"];
}
if (!string.IsNullOrEmpty(Configuration["EventBusPassword"]))
{
factory.Password = Configuration["EventBusPassword"];
}
var retryCount = 5;
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
}
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
});
}
RegisterEventBus(services);
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.SetIsOriginAllowed((host) => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<ICouponRepository, CouponRepository>();
services.AddTransient<IIdentityService, IdentityService>();
services.AddOptions();
var container = new ContainerBuilder();
container.Populate(services);
return new AutofacServiceProvider(container.Build());
}
private void RegisterAppInsights(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(Configuration);
services.AddApplicationInsightsKubernetesEnricher();
}
private void ConfigureAuthService(IServiceCollection services)
{
// prevent from mapping "sub" claim to nameidentifier.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
var identityUrl = Configuration.GetValue<string>("IdentityUrl");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "basket";
});
}
private void RegisterEventBus(IServiceCollection services)
{
if (Configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
{
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
string subscriptionName = Configuration["SubscriptionClientName"];
return new EventBusServiceBus(serviceBusPersisterConnection, logger,
eventBusSubscriptionsManager, iLifetimeScope, subscriptionName);
});
}
else
{
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
{
var subscriptionClientName = Configuration["SubscriptionClientName"];
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
var retryCount = 5;
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
}
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubscriptionsManager, subscriptionClientName, retryCount);
});
}
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
//services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
services.AddTransient<OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
//loggerFactory.AddAzureWebAppDiagnostics();
//loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
app.UsePathBase(pathBase);
}
app.UseSwagger()
.UseSwaggerUI(setup =>
{
setup.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "1Basket.API V1");
setup.OAuthClientId("basketswaggerui");
setup.OAuthAppName("Basket Swagger UI");
});
app.UseRouting();
app.UseCors("CorsPolicy");
ConfigureAuth(app);
app.UseStaticFiles();
app.UseEndpoints(endpoints =>
{
//endpoints.MapGrpcService<BasketService>();
endpoints.MapDefaultControllerRoute();
endpoints.MapControllers();
//endpoints.MapGet("/_proto/", async ctx =>
//{
// ctx.Response.ContentType = "text/plain";
// using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read);
// using var sr = new StreamReader(fs);
// while (!sr.EndOfStream)
// {
// var line = await sr.ReadLineAsync();
// if (line != "/* >>" || line != "<< */")
// {
// await ctx.Response.WriteAsync(line);
// }
// }
//});
endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
{
Predicate = r => r.Name.Contains("self")
});
});
ConfigureEventBus(app);
}
protected virtual void ConfigureAuth(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseAuthorization();
}
private void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
//eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent, OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>();
}
}
}

+ 8
- 0
src/Services/Coupon/Coupon.API/appsettings.Development.json View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information"
}
}
}

+ 13
- 0
src/Services/Coupon/Coupon.API/appsettings.json View File

@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "eshop",
"CouponsCollectionName": "coupons",
"SubscriptionClientName": "Coupon"
}

+ 5
- 0
src/Services/Coupon/Coupon.API/libman.json View File

@ -0,0 +1,5 @@
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": []
}

+ 5
- 1
src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs View File

@ -54,6 +54,9 @@ public class CreateOrderCommand
[DataMember] [DataMember]
public int CardTypeId { get; private set; } public int CardTypeId { get; private set; }
[DataMember]
public string CouponCode { get; private set; }
[DataMember] [DataMember]
public IEnumerable<OrderItemDTO> OrderItems => _orderItems; public IEnumerable<OrderItemDTO> OrderItems => _orderItems;
@ -64,7 +67,7 @@ public class CreateOrderCommand
public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode, public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,
string cardNumber, string cardHolderName, DateTime cardExpiration, string cardNumber, string cardHolderName, DateTime cardExpiration,
string cardSecurityNumber, int cardTypeId) : this()
string cardSecurityNumber, int cardTypeId, string couponCode = null) : this()
{ {
_orderItems = basketItems.ToOrderItemsDTO().ToList(); _orderItems = basketItems.ToOrderItemsDTO().ToList();
UserId = userId; UserId = userId;
@ -79,6 +82,7 @@ public class CreateOrderCommand
CardExpiration = cardExpiration; CardExpiration = cardExpiration;
CardSecurityNumber = cardSecurityNumber; CardSecurityNumber = cardSecurityNumber;
CardTypeId = cardTypeId; CardTypeId = cardTypeId;
CouponCode = couponCode;
} }


+ 1
- 1
src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs View File

@ -37,7 +37,7 @@ public class CreateOrderCommandHandler
// methods and constructor so validations, invariants and business logic // methods and constructor so validations, invariants and business logic
// make sure that consistency is preserved across the whole aggregate // 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 address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration, couponCode: message.CouponCode);
foreach (var item in message.OrderItems) foreach (var item in message.OrderItems)
{ {


+ 10
- 4
src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs View File

@ -1,4 +1,6 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStockConfirmed;
using Ordering.API.Application.IntegrationEvents.Events;
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStockConfirmed;
public class OrderStatusChangedToStockConfirmedDomainEventHandler public class OrderStatusChangedToStockConfirmedDomainEventHandler
: INotificationHandler<OrderStatusChangedToStockConfirmedDomainEvent> : INotificationHandler<OrderStatusChangedToStockConfirmedDomainEvent>
@ -27,9 +29,13 @@ public class OrderStatusChangedToStockConfirmedDomainEventHandler
orderStatusChangedToStockConfirmedDomainEvent.OrderId, nameof(OrderStatus.StockConfirmed), OrderStatus.StockConfirmed.Id); orderStatusChangedToStockConfirmedDomainEvent.OrderId, nameof(OrderStatus.StockConfirmed), OrderStatus.StockConfirmed.Id);
var order = await _orderRepository.GetAsync(orderStatusChangedToStockConfirmedDomainEvent.OrderId); var order = await _orderRepository.GetAsync(orderStatusChangedToStockConfirmedDomainEvent.OrderId);
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
//var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
var orderStatusChangedToAwaitingCouponValidationIntegrationEvent =
new OrderStatusChangedToAwaitingCouponValidationIntegrationEvent(order.Id, order.CouponCode);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToAwaitingCouponValidationIntegrationEvent);
var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToStockConfirmedIntegrationEvent);
//var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
//await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToStockConfirmedIntegrationEvent);
} }
} }

+ 46
- 0
src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponConfirmedIntegrationEventHandler.cs View File

@ -0,0 +1,46 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
using Ordering.API.Application.IntegrationEvents.Events;
namespace Ordering.API.Application.IntegrationEvents.EventHandling
{
public class OrderCouponConfirmedIntegrationEventHandler : IIntegrationEventHandler<OrderCouponConfirmedIntegrationEvent>
{
private readonly IOrderRepository _orderRepository;
private readonly IBuyerRepository _buyerRepository;
private readonly ILoggerFactory _logger;
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
private readonly IEventBus _eventBus;
public OrderCouponConfirmedIntegrationEventHandler(
IOrderRepository orderRepository,
IBuyerRepository buyerRepository,
ILoggerFactory logger,
IOrderingIntegrationEventService orderingIntegrationEventService,
IEventBus eventBus)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_orderingIntegrationEventService = orderingIntegrationEventService;
_eventBus = eventBus ?? throw new ArgumentNullException();
}
public async Task Handle(OrderCouponConfirmedIntegrationEvent @event)
{
// Add new statuses here
//_logger.CreateLogger<OrderStatusChangedToStockConfirmedDomainEventHandler>()
//.LogTrace("Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})",
// orderStatusChangedToStockConfirmedDomainEvent.OrderId, nameof(OrderStatus.StockConfirmed), OrderStatus.StockConfirmed.Id);
var order = await _orderRepository.GetAsync(@event.OrderId);
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
_eventBus.Publish(orderStatusChangedToStockConfirmedIntegrationEvent);
//fix it
//await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToStockConfirmedIntegrationEvent);
}
}
}

+ 37
- 0
src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderCouponRejectedIntegrationEventHandler.cs View File

@ -0,0 +1,37 @@
using Ordering.API.Application.IntegrationEvents.Events;
namespace Ordering.API.Application.IntegrationEvents.EventHandling
{
public class OrderCouponRejectedIntegrationEventHandler : IIntegrationEventHandler<OrderCouponRejectedIntegrationEvent>
{
private readonly IMediator _mediator;
private readonly ILogger<OrderCouponRejectedIntegrationEventHandler> _logger;
public OrderCouponRejectedIntegrationEventHandler(
IMediator mediator,
ILogger<OrderCouponRejectedIntegrationEventHandler> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task Handle(OrderCouponRejectedIntegrationEvent @event)
{
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
{
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
var command = new CancelOrderCommand(@event.OrderId);
_logger.LogInformation(
"----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",
command.GetGenericTypeName(),
nameof(command.OrderNumber),
command.OrderNumber,
command);
await _mediator.Send(command);
}
}
}
}

+ 5
- 2
src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs View File

@ -1,4 +1,7 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling;
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
using Ordering.API.Application.IntegrationEvents.Events;
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling;
public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent> public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>
{ {
@ -37,7 +40,7 @@ public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHand
var createOrderCommand = new CreateOrderCommand(@event.Basket.Items, @event.UserId, @event.UserName, @event.City, @event.Street, var createOrderCommand = new CreateOrderCommand(@event.Basket.Items, @event.UserId, @event.UserName, @event.City, @event.Street,
@event.State, @event.Country, @event.ZipCode, @event.State, @event.Country, @event.ZipCode,
@event.CardNumber, @event.CardHolderName, @event.CardExpiration, @event.CardNumber, @event.CardHolderName, @event.CardExpiration,
@event.CardSecurityNumber, @event.CardTypeId);
@event.CardSecurityNumber, @event.CardTypeId, @event.CouponCode);
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand, bool>(createOrderCommand, @event.RequestId); var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand, bool>(createOrderCommand, @event.RequestId);


+ 10
- 0
src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs View File

@ -0,0 +1,10 @@
namespace Ordering.API.Application.IntegrationEvents.Events
{
public record OrderCouponConfirmedIntegrationEvent: IntegrationEvent
{
public int OrderId { get; set; }
public OrderCouponConfirmedIntegrationEvent(int orderId)
=> (OrderId) = (orderId);
}
}

+ 10
- 0
src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs View File

@ -0,0 +1,10 @@
namespace Ordering.API.Application.IntegrationEvents.Events
{
public record OrderCouponRejectedIntegrationEvent: IntegrationEvent
{
public int OrderId { get; set; }
public OrderCouponRejectedIntegrationEvent(int orderId)
=> (OrderId) = (orderId);
}
}

+ 12
- 0
src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs View File

@ -0,0 +1,12 @@
namespace Ordering.API.Application.IntegrationEvents.Events
{
public record OrderStatusChangedToAwaitingCouponValidationIntegrationEvent : IntegrationEvent
{
public int OrderId { get; set; }
public string CouponCode { get; init; }
public OrderStatusChangedToAwaitingCouponValidationIntegrationEvent(int orderId, string couponCode)
=> (OrderId, CouponCode) = (orderId, couponCode);
}
}

+ 4
- 1
src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs View File

@ -32,10 +32,12 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
public CustomerBasket Basket { get; } public CustomerBasket Basket { get; }
public string CouponCode { get; set; }
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street, public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
string state, string country, string zipCode, string cardNumber, string cardHolderName, string state, string country, string zipCode, string cardNumber, string cardHolderName,
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId, DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
CustomerBasket basket)
CustomerBasket basket, string couponCode = null)
{ {
UserId = userId; UserId = userId;
City = city; City = city;
@ -52,6 +54,7 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
Basket = basket; Basket = basket;
RequestId = requestId; RequestId = requestId;
UserName = userName; UserName = userName;
CouponCode = couponCode;
} }
} }

+ 4
- 2
src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs View File

@ -19,7 +19,7 @@ public class OrderQueries
var result = await connection.QueryAsync<dynamic>( var result = await connection.QueryAsync<dynamic>(
@"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description, @"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description,
o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode, o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode,
os.Name as status,
os.Name as status, o.CouponCode as couponcode,
oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl
FROM ordering.Orders o FROM ordering.Orders o
LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid
@ -70,7 +70,9 @@ public class OrderQueries
zipcode = result[0].zipcode, zipcode = result[0].zipcode,
country = result[0].country, country = result[0].country,
orderitems = new List<Orderitem>(), orderitems = new List<Orderitem>(),
total = 0
total = 0,
couponcode = result[0].couponcode,
discount = result[0].couponcode == null ? 0 : int.Parse((result[0].couponcode as string).Split("-").Last()),
}; };
foreach (dynamic item in result) foreach (dynamic item in result)


+ 2
- 0
src/Services/Ordering/Ordering.API/Application/Queries/OrderViewModel.cs View File

@ -20,6 +20,8 @@ public record Order
public string country { get; init; } public string country { get; init; }
public List<Orderitem> orderitems { get; set; } public List<Orderitem> orderitems { get; set; }
public decimal total { get; set; } public decimal total { get; set; }
public decimal discount { get; set; }
public string couponcode { get; set; }
} }
public record OrderSummary public record OrderSummary


+ 9
- 1
src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20190808132242_Change_Relation_Of_Orders.cs View File

@ -6,11 +6,19 @@ namespace Ordering.API.Infrastructure.Migrations
{ {
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.AddColumn<string>(
name: "CouponCode",
schema: "ordering",
table: "orders",
nullable: true);
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropColumn(
name: "CouponCode",
schema: "ordering",
table: "orders");
} }
} }
} }

+ 8
- 0
src/Services/Ordering/Ordering.API/Ordering.API.csproj View File

@ -23,6 +23,8 @@
<Protobuf Include="Proto\ordering.proto" GrpcServices="Server" Generator="MSBuild:Compile" /> <Protobuf Include="Proto\ordering.proto" GrpcServices="Server" Generator="MSBuild:Compile" />
<Content Include="Proto\ordering.proto" /> <Content Include="Proto\ordering.proto" />
<None Remove="@(Protobuf)" /> <None Remove="@(Protobuf)" />
<None Remove="Application\IntegrationEvents\EventHandling\OrderCouponConfirmedIntegrationEventHandler.cs~RF159e4744.TMP" />
<None Remove="Application\IntegrationEvents\EventHandling\UserCheckoutAcceptedIntegrationEventHandler.cs~RFaf93a3b.TMP" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -44,6 +46,8 @@
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.1" /> <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.1" />
<PackageReference Include="Azure.Identity" Version="1.4.0" /> <PackageReference Include="Azure.Identity" Version="1.4.0" />
<PackageReference Include="Dapper" Version="2.0.78" /> <PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="FluentMigrator" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner.SqlServer" Version="3.3.2" />
<PackageReference Include="FluentValidation.AspNetCore" Version="9.3.0" /> <PackageReference Include="FluentValidation.AspNetCore" Version="9.3.0" />
<PackageReference Include="Google.Protobuf" Version="3.15.0" /> <PackageReference Include="Google.Protobuf" Version="3.15.0" />
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.34.0" /> <PackageReference Include="Grpc.AspNetCore.Server" Version="2.34.0" />
@ -56,6 +60,10 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" /> <PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="6.0.0" />
<PackageReference Include="Microsoft.NETCore.Platforms" Version="6.0.0" /> <PackageReference Include="Microsoft.NETCore.Platforms" Version="6.0.0" />
<PackageReference Include="Polly" Version="7.2.1" /> <PackageReference Include="Polly" Version="7.2.1" />


+ 4
- 0
src/Services/Ordering/Ordering.API/Startup.cs View File

@ -1,3 +1,5 @@
using Ordering.API.Application.IntegrationEvents.Events;
namespace Microsoft.eShopOnContainers.Services.Ordering.API; namespace Microsoft.eShopOnContainers.Services.Ordering.API;
public class Startup public class Startup
@ -106,6 +108,8 @@ public class Startup
eventBus.Subscribe<OrderStockRejectedIntegrationEvent, IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>>(); eventBus.Subscribe<OrderStockRejectedIntegrationEvent, IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>>();
eventBus.Subscribe<OrderPaymentFailedIntegrationEvent, IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>>(); eventBus.Subscribe<OrderPaymentFailedIntegrationEvent, IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>>();
eventBus.Subscribe<OrderPaymentSucceededIntegrationEvent, IIntegrationEventHandler<OrderPaymentSucceededIntegrationEvent>>(); eventBus.Subscribe<OrderPaymentSucceededIntegrationEvent, IIntegrationEventHandler<OrderPaymentSucceededIntegrationEvent>>();
eventBus.Subscribe<OrderCouponConfirmedIntegrationEvent, IIntegrationEventHandler<OrderCouponConfirmedIntegrationEvent>>();
eventBus.Subscribe<OrderCouponRejectedIntegrationEvent, IIntegrationEventHandler<OrderCouponRejectedIntegrationEvent>>();
} }
protected virtual void ConfigureAuth(IApplicationBuilder app) protected virtual void ConfigureAuth(IApplicationBuilder app)


+ 4
- 1
src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs View File

@ -13,6 +13,8 @@ public class Order
// Address is a Value Object pattern example persisted as EF Core 2.0 owned entity // Address is a Value Object pattern example persisted as EF Core 2.0 owned entity
public Address Address { get; private set; } public Address Address { get; private set; }
public string CouponCode { get; private set; }
public int? GetBuyerId => _buyerId; public int? GetBuyerId => _buyerId;
private int? _buyerId; private int? _buyerId;
@ -49,13 +51,14 @@ public class Order
} }
public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber, public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) : this()
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null, string couponCode = null) : this()
{ {
_buyerId = buyerId; _buyerId = buyerId;
_paymentMethodId = paymentMethodId; _paymentMethodId = paymentMethodId;
_orderStatusId = OrderStatus.Submitted.Id; _orderStatusId = OrderStatus.Submitted.Id;
_orderDate = DateTime.UtcNow; _orderDate = DateTime.UtcNow;
Address = address; Address = address;
CouponCode = couponCode;
// Add the OrderStarterDomainEvent to the domain events collection // Add the OrderStarterDomainEvent to the domain events collection
// to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ] // to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ]


+ 4
- 0
src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj View File

@ -11,6 +11,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="10.0.1" /> <PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />


+ 666
- 22807
src/Web/WebSPA/Client/package-lock.json
File diff suppressed because it is too large
View File


+ 12
- 1
src/Web/WebSPA/Client/src/modules/orders/orders-detail/orders-detail.component.html View File

@ -34,9 +34,20 @@
</div> </div>
</article> </article>
<div *ngIf="order.couponcode">
<div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase">
<div>Subtotal</div>
<div class="ml-3">${{order.total | number:'.2-2'}}</div>
</div>
<div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase">
<div>Coupon</div>
<div class="ml-3">{{order.couponcode}}</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase u-text-xl"> <div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase u-text-xl">
<div>Total</div> <div>Total</div>
<div class="ml-3">${{order.total | number:'.2-2'}}</div>
<div class="ml-3">${{order.total - order.discount | number:'.2-2'}}</div>
</div> </div>
<aside class="d-flex justify-content-end mt-5"> <aside class="d-flex justify-content-end mt-5">


+ 33
- 1
src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.html View File

@ -84,9 +84,41 @@
</div> </div>
</article> </article>
<div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase">
<div>Subtotal</div>
<div class="ml-3">${{order.total | number:'.2-2'}}</div>
</div>
<div class="d-flex flex-nowrap justify-content-between align-items-center mb-3 mt-3">
<div>
<div *ngIf="!coupon">
<div class="u-text-uppercase">Have a discount code?</div>
<div class="d-flex flex-nowrap justify-content-between align-items-center mt-1">
<input #discountcode
class="esh-orders_new-coupon mr-2 form-control"
type="text"
placeholder="Coupon number"
(keydown)="keyDownValidationCoupon($event, discountcode.value)">
<button type="button"
(click)="checkValidationCoupon(discountcode.value)"
class="btn btn-secondary u-minwidth-unset">
Apply
</button>
</div>
<div class="mt-1" *ngIf="couponValidationMessage">{{couponValidationMessage}}</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-end text-uppercase">
<div *ngIf="coupon?.code">{{coupon?.code}}</div>
<div>
<div class="text-right ml-3" *ngIf="coupon?.discount">-${{coupon?.discount | number:'.2-2'}}</div>
</div>
</div>
</div>
<div class="divider d-flex align-items-center justify-content-end mb-4 pt-4 text-uppercase u-text-xl"> <div class="divider d-flex align-items-center justify-content-end mb-4 pt-4 text-uppercase u-text-xl">
<div>Total</div> <div>Total</div>
<div class="ml-3">${{ order.total | number:'.2-2'}}</div>
<div class="ml-3">${{ order.total - coupon?.discount | number:'.2-2'}}</div>
</div> </div>
<div class="esh-orders_new-buttons d-flex justify-content-end align-items-center"> <div class="esh-orders_new-buttons d-flex justify-content-end align-items-center">


+ 13
- 0
src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.ts View File

@ -9,6 +9,8 @@ import { BasketWrapperService } from '../../shared/services/
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ICoupon } from '../../shared/models/coupon.model';
import { DataService } from '../../shared/services/data.service';
@Component({ @Component({
selector: 'esh-orders_new .esh-orders_new .mb-5', selector: 'esh-orders_new .esh-orders_new .mb-5',
@ -20,6 +22,7 @@ export class OrdersNewComponent implements OnInit {
isOrderProcessing: boolean; isOrderProcessing: boolean;
errorReceived: boolean; errorReceived: boolean;
order: IOrder; order: IOrder;
coupon: ICoupon;
constructor(private orderService: OrdersService, private basketService: BasketService, fb: FormBuilder, private router: Router) { constructor(private orderService: OrdersService, private basketService: BasketService, fb: FormBuilder, private router: Router) {
// Obtain user profile information // Obtain user profile information
@ -39,6 +42,13 @@ export class OrdersNewComponent implements OnInit {
ngOnInit() { ngOnInit() {
} }
checkValidationCoupon(value: string)
{
this.coupon = <ICoupon>{};
this.coupon.code = value;
this.coupon.discount = +value.split('-')[1];
}
submitForm(value: any) { submitForm(value: any) {
this.order.street = this.newOrderForm.controls['street'].value; this.order.street = this.newOrderForm.controls['street'].value;
this.order.city = this.newOrderForm.controls['city'].value; this.order.city = this.newOrderForm.controls['city'].value;
@ -50,6 +60,9 @@ export class OrdersNewComponent implements OnInit {
this.order.cardexpiration = new Date(20 + this.newOrderForm.controls['expirationdate'].value.split('/')[1], this.newOrderForm.controls['expirationdate'].value.split('/')[0]); 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.order.cardsecuritynumber = this.newOrderForm.controls['securitycode'].value;
let basketCheckout = this.basketService.mapBasketInfoCheckout(this.order); let basketCheckout = this.basketService.mapBasketInfoCheckout(this.order);
if (this.coupon) {
basketCheckout.couponCode = this.coupon.code;
}
this.basketService.setBasketCheckout(basketCheckout) this.basketService.setBasketCheckout(basketCheckout)
.pipe(catchError((errMessage) => { .pipe(catchError((errMessage) => {
this.errorReceived = true; this.errorReceived = true;


+ 1
- 0
src/Web/WebSPA/Client/src/modules/shared/models/basketCheckout.model.ts View File

@ -13,4 +13,5 @@
buyer: string; buyer: string;
ordernumber: string; ordernumber: string;
total: number; total: number;
couponCode: string;
} }

+ 1
- 0
src/Web/WebSPA/Client/src/modules/shared/models/configuration.model.ts View File

@ -1,6 +1,7 @@
export interface IConfiguration { export interface IConfiguration {
identityUrl: string, identityUrl: string,
purchaseUrl: string, purchaseUrl: string,
couponUrl: string,
signalrHubUrl: string, signalrHubUrl: string,
activateCampaignDetailFunction: boolean activateCampaignDetailFunction: boolean
} }

+ 5
- 0
src/Web/WebSPA/Client/src/modules/shared/models/coupon.model.ts View File

@ -0,0 +1,5 @@
export interface ICoupon {
discount: number;
code: string;
ordernumber: string;
}

+ 2
- 0
src/Web/WebSPA/Client/src/modules/shared/models/order-detail.model.ts View File

@ -12,4 +12,6 @@ export interface IOrderDetail {
country: number; country: number;
total: number; total: number;
orderitems: IOrderItem[]; orderitems: IOrderItem[];
couponcode: string;
discount: number;
} }

+ 14
- 0
src/Web/WebSPA/Dockerfile View File

@ -3,6 +3,13 @@
ARG NODE_IMAGE=node:12.0 ARG NODE_IMAGE=node:12.0
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app WORKDIR /app
RUN apt-get update
RUN apt-get -y install curl gnupg
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
RUN apt-get -y install nodejs
RUN npm install
RUN npm -v
RUN npm install -g @angular/cli@11.2.14
EXPOSE 80 EXPOSE 80
FROM ${NODE_IMAGE} as node-build FROM ${NODE_IMAGE} as node-build
@ -60,6 +67,13 @@ RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln"
COPY . . COPY . .
COPY --from=node-build /web/wwwroot /src/Web/WebSPA/wwwroot/ COPY --from=node-build /web/wwwroot /src/Web/WebSPA/wwwroot/
WORKDIR /src/Web/WebSPA WORKDIR /src/Web/WebSPA
RUN apt-get update
RUN apt-get -y install curl gnupg
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
RUN apt-get -y install nodejs
RUN npm install
RUN npm -v
RUN npm install -g @angular/cli@11.2.14
RUN dotnet publish --no-restore -c Release -o /app RUN dotnet publish --no-restore -c Release -o /app
FROM build AS publish FROM build AS publish


+ 68
- 58
src/Web/WebSPA/WebSPA.csproj View File

@ -1,64 +1,74 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<UserSecretsId>aspnetcorespa-c23d27a4-eb88-4b18-9b77-2a93f3b15119</UserSecretsId>
<TypeScriptCompileOnSaveEnabled>false</TypeScriptCompileOnSaveEnabled>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<GeneratedItemPatterns>wwwroot/dist/**</GeneratedItemPatterns>
<DefaultItemExcludes>$(DefaultItemExcludes);$(GeneratedItemPatterns)</DefaultItemExcludes>
<TypeScriptToolsVersion>4.2</TypeScriptToolsVersion>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..\..</DockerfileContext>
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
<SpaRoot>Client\</SpaRoot>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<UserSecretsId>aspnetcorespa-c23d27a4-eb88-4b18-9b77-2a93f3b15119</UserSecretsId>
<TypeScriptCompileOnSaveEnabled>false</TypeScriptCompileOnSaveEnabled>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<GeneratedItemPatterns>wwwroot/dist/**</GeneratedItemPatterns>
<DefaultItemExcludes>$(DefaultItemExcludes);$(GeneratedItemPatterns)</DefaultItemExcludes>
<TypeScriptToolsVersion>4.2</TypeScriptToolsVersion>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..\..</DockerfileContext>
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
<SpaRoot>Client\</SpaRoot>
</PropertyGroup>
<ItemGroup>
<Content Include="Setup\images.zip">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="appsettings.json;">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="web.config;">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\**\*;">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Remove="Client\src\modules\shared\models\coupon.model.ts" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="5.0.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.17.0" />
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.17.0" />
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="2.0.1" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0-dev-00834" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.0.1" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<ProjectExtensions>
<VisualStudio><UserProperties package_1json__JSONSchema="http://json.schemastore.org/project-1.0.0-beta4" /></VisualStudio>
</ProjectExtensions>
<ItemGroup>
<Content Include="Setup\images.zip">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="appsettings.json;">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="web.config;">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\**\*;">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="5.0.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.17.0" />
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.17.0" />
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="2.0.1" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0-dev-00834" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="Client\src\modules\shared\models\coupon.model.ts" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<ProjectExtensions>
<VisualStudio>
<UserProperties package_1json__JSONSchema="http://json.schemastore.org/project-1.0.0-beta4" />
</VisualStudio>
</ProjectExtensions>
</Project> </Project>

+ 1
- 0
src/Web/WebSPA/appsettings.json View File

@ -6,6 +6,7 @@
"PurchaseUrlHC": "http://host.docker.internal:5202/hc", "PurchaseUrlHC": "http://host.docker.internal:5202/hc",
"IdentityUrlHC": "http://host.docker.internal:5105/hc", "IdentityUrlHC": "http://host.docker.internal:5105/hc",
"SignalrHubUrl": "http://host.docker.internal:5112", "SignalrHubUrl": "http://host.docker.internal:5112",
"CouponUrl" : "http://host.docker.internal:5203",
"UseCustomizationData": true, "UseCustomizationData": true,
"IsClusterEnv": "False", "IsClusterEnv": "False",
"ActivateCampaignDetailFunction": false, "ActivateCampaignDetailFunction": false,


+ 3
- 0
src/Web/WebSPA/package-lock.json View File

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

+ 24
- 3
src/docker-compose.override.yml View File

@ -79,6 +79,27 @@ services:
- "5103:80" - "5103:80"
- "9103:81" - "9103:81"
coupon-api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=${ESHOP_AZURE_MONGO_COUPON_DB}
- identityUrl=http://identity-api
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
- AzureServiceBusEnabled=False
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
- OrchestratorType=${ORCHESTRATOR_TYPE}
- UseLoadTest=${USE_LOADTEST:-False}
- PATH_BASE=/coupon-api
- GRPC_PORT=81
- PORT=80
ports:
- "5203:80"
- "9203:81"
catalog-api: catalog-api:
environment: environment:
- ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_ENVIRONMENT=Development
@ -98,8 +119,8 @@ services:
- PORT=80 - PORT=80
- PATH_BASE=/catalog-api - PATH_BASE=/catalog-api
ports: ports:
- "5101:80"
- "9101:81"
- "5201:80"
- "9201:81"
ordering-api: ordering-api:
environment: environment:
@ -272,7 +293,7 @@ services:
webspa: webspa:
environment: environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80 - ASPNETCORE_URLS=http://0.0.0.0:80
- IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 - IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
- PurchaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5202 - PurchaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5202


+ 10
- 0
src/docker-compose.yml View File

@ -78,6 +78,16 @@ services:
depends_on: depends_on:
- sqldata - sqldata
coupon-api:
image: ${REGISTRY:-eshop}/coupon.api:${PLATFORM:-linux}-${TAG:-latest}
build:
context: .
dockerfile: Services/Coupon/Coupon.API/Dockerfile
depends_on:
- rabbitmq
- nosqldata
- identity-api
mobileshoppingapigw: mobileshoppingapigw:
image: envoyproxy/envoy:v1.11.1 image: envoyproxy/envoy:v1.11.1


+ 58
- 4
src/eShopOnContainers-ServicesAndWebApps.sln View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29020.237
# Visual Studio Version 17
VisualStudioVersion = 17.2.32526.322
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{FEA0C318-FFED-4D39-8781-265718CA43DD}" Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{FEA0C318-FFED-4D39-8781-265718CA43DD}"
EndProject EndProject
@ -124,6 +124,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{373D8AA1
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{95D735BE-2899-4495-BE3F-2600E93B4E3C}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{95D735BE-2899-4495-BE3F-2600E93B4E3C}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Coupon", "Coupon", "{44844635-633D-49B3-A70E-E44E8E2992EE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coupon.API", "Services\Coupon\Coupon.API\Coupon.API.csproj", "{D9651304-75EB-4EA7-8314-86C06C253EA8}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
@ -450,8 +454,8 @@ Global
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x64.Build.0 = Release|Any CPU {F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x64.Build.0 = Release|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x86.ActiveCfg = Release|Any CPU {F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x86.ActiveCfg = Release|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x86.Build.0 = Release|Any CPU {F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x86.Build.0 = Release|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.Build.0 = Release|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|ARM.ActiveCfg = Debug|Any CPU {F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|ARM.ActiveCfg = Debug|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|ARM.Build.0 = Debug|Any CPU {F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|ARM.Build.0 = Debug|Any CPU
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|iPhone.ActiveCfg = Debug|Any CPU {F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|iPhone.ActiveCfg = Debug|Any CPU
@ -1530,6 +1534,54 @@ Global
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x64.Build.0 = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x64.Build.0 = Release|Any CPU
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.ActiveCfg = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.ActiveCfg = Release|Any CPU
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.Build.0 = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.Build.0 = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|ARM.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|ARM.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|iPhone.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|x64.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|x64.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|x86.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|x86.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|ARM.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|ARM.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|iPhone.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|x64.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|x64.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|x86.ActiveCfg = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|x86.Build.0 = Debug|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|Any CPU.Build.0 = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|ARM.ActiveCfg = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|ARM.Build.0 = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|iPhone.ActiveCfg = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|iPhone.Build.0 = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|x64.ActiveCfg = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|x64.Build.0 = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|x86.ActiveCfg = Release|Any CPU
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -1588,6 +1640,8 @@ Global
{B62E859F-825E-4C8B-93EC-5966EACFD026} = {798BFC44-2CCD-45FA-B37A-5173B03C2B30} {B62E859F-825E-4C8B-93EC-5966EACFD026} = {798BFC44-2CCD-45FA-B37A-5173B03C2B30}
{373D8AA1-36BE-49EC-89F0-6CB736666285} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} {373D8AA1-36BE-49EC-89F0-6CB736666285} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F}
{95D735BE-2899-4495-BE3F-2600E93B4E3C} = {373D8AA1-36BE-49EC-89F0-6CB736666285} {95D735BE-2899-4495-BE3F-2600E93B4E3C} = {373D8AA1-36BE-49EC-89F0-6CB736666285}
{44844635-633D-49B3-A70E-E44E8E2992EE} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8}
{D9651304-75EB-4EA7-8314-86C06C253EA8} = {44844635-633D-49B3-A70E-E44E8E2992EE}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9} SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9}


Loading…
Cancel
Save