@ -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, | |||||
}; | |||||
} | |||||
} | |||||
} |
@ -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> |
@ -0,0 +1,6 @@ | |||||
namespace Coupon.API | |||||
{ | |||||
public class CouponSettings | |||||
{ | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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"] |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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) | |||||
{ } | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,9 @@ | |||||
namespace Coupon.API.Infrastructure.Filters | |||||
{ | |||||
public class JsonErrorResponse | |||||
{ | |||||
public string[] Messages { get; set; } | |||||
public object DeveloperMessage { get; set; } | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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" } | |||||
} | |||||
}; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,6 @@ | |||||
namespace Coupon.API.Middlewares | |||||
{ | |||||
public class FailingOptions | |||||
{ | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} |
@ -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 | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,7 @@ | |||||
namespace Coupon.API.Services | |||||
{ | |||||
public interface IIdentityService | |||||
{ | |||||
string GetUserIdentity(); | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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>(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,8 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft.AspNetCore": "Information" | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,13 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft.AspNetCore": "Warning" | |||||
} | |||||
}, | |||||
"AllowedHosts": "*", | |||||
"ConnectionString": "mongodb://localhost:27017", | |||||
"DatabaseName": "eshop", | |||||
"CouponsCollectionName": "coupons", | |||||
"SubscriptionClientName": "Coupon" | |||||
} |
@ -0,0 +1,5 @@ | |||||
{ | |||||
"version": "1.0", | |||||
"defaultProvider": "cdnjs", | |||||
"libraries": [] | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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 | ||||
} | } |
@ -0,0 +1,5 @@ | |||||
export interface ICoupon { | |||||
discount: number; | |||||
code: string; | |||||
ordernumber: string; | |||||
} |
@ -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> |
@ -0,0 +1,3 @@ | |||||
{ | |||||
"lockfileVersion": 1 | |||||
} |