@ -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 { | |||
identityUrl: string, | |||
purchaseUrl: string, | |||
couponUrl: string, | |||
signalrHubUrl: string, | |||
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"> | |||
<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> |
@ -0,0 +1,3 @@ | |||
{ | |||
"lockfileVersion": 1 | |||
} |