Initial version [need refactoring]
This commit is contained in:
parent
4596d7aa99
commit
4fd5530a0d
@ -60,7 +60,7 @@ public class BasketController : ControllerBase
|
||||
|
||||
var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street,
|
||||
basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName,
|
||||
basketCheckout.CardExpiration, basketCheckout.CardSecurityNumber, basketCheckout.CardTypeId, basketCheckout.Buyer, basketCheckout.RequestId, basket);
|
||||
basketCheckout.CardExpiration, basketCheckout.CardSecurityNumber, basketCheckout.CardTypeId, basketCheckout.Buyer, basketCheckout.RequestId, basket, basketCheckout.CouponCode);
|
||||
|
||||
// Once basket is checkout, sends an integration event to
|
||||
// ordering.api to convert basket to order and proceeds with
|
||||
|
@ -34,10 +34,12 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
||||
|
||||
public CustomerBasket Basket { get; }
|
||||
|
||||
public string CouponCode { get; init; }
|
||||
|
||||
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
|
||||
string state, string country, string zipCode, string cardNumber, string cardHolderName,
|
||||
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
|
||||
CustomerBasket basket)
|
||||
CustomerBasket basket, string couponCode = null)
|
||||
{
|
||||
UserId = userId;
|
||||
UserName = userName;
|
||||
@ -54,6 +56,7 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
||||
Buyer = buyer;
|
||||
Basket = basket;
|
||||
RequestId = requestId;
|
||||
CouponCode = couponCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,4 +25,6 @@ public class BasketCheckout
|
||||
public string Buyer { get; set; }
|
||||
|
||||
public Guid RequestId { get; set; }
|
||||
|
||||
public string CouponCode { get; set; }
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
using Coupon.API.DTOs;
|
||||
using Coupon.API.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Coupon.API.Controllers
|
||||
{
|
||||
[Route("api/v1/[controller]")]
|
||||
[ApiController]
|
||||
public class CouponController: ControllerBase
|
||||
{
|
||||
private ICouponRepository _couponRepository;
|
||||
|
||||
public CouponController(ICouponRepository couponeRepository) =>
|
||||
_couponRepository = couponeRepository;
|
||||
|
||||
[HttpGet("value/{code}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CouponDto>> GetCouponByCodeAsync(string code)
|
||||
{
|
||||
var coupon = await _couponRepository.FindByCodeAsync(code);
|
||||
|
||||
if (coupon is null || coupon.Consumed)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return new CouponDto
|
||||
{
|
||||
Code = code,
|
||||
Consumed = coupon.Consumed,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
44
src/Services/Coupon/Coupon.API/Coupon.API.csproj
Normal file
44
src/Services/Coupon/Coupon.API/Coupon.API.csproj
Normal file
@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..\..</DockerfileContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Middlewares\AuthorizeCheckOperationFilter.cs~RF427f162.TMP" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="6.0.2" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="6.0.4" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0-preview.1" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.9.0-beta.1" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.21.0" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.18.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Http" Version="8.0.0-beta.9" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="5.2.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
6
src/Services/Coupon/Coupon.API/CouponSettings.cs
Normal file
6
src/Services/Coupon/Coupon.API/CouponSettings.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Coupon.API
|
||||
{
|
||||
public class CouponSettings
|
||||
{
|
||||
}
|
||||
}
|
40
src/Services/Coupon/Coupon.API/CustomExtensionMethods.cs
Normal file
40
src/Services/Coupon/Coupon.API/CustomExtensionMethods.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Coupon.API
|
||||
{
|
||||
public static class CustomExtensionMethods
|
||||
{
|
||||
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var hcBuilder = services.AddHealthChecks();
|
||||
|
||||
hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy());
|
||||
|
||||
hcBuilder
|
||||
.AddRedis(
|
||||
configuration["ConnectionString"],
|
||||
name: "nosqldata-check",
|
||||
tags: new string[] { "nosqldata" });
|
||||
|
||||
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
||||
{
|
||||
hcBuilder
|
||||
.AddAzureServiceBusTopic(
|
||||
configuration["EventBusConnection"],
|
||||
topicName: "eshop_event_bus",
|
||||
name: "coupon-servicebus-check",
|
||||
tags: new string[] { "servicebus" });
|
||||
}
|
||||
else
|
||||
{
|
||||
hcBuilder
|
||||
.AddRabbitMQ(
|
||||
$"amqp://{configuration["EventBusConnection"]}",
|
||||
name: "coupon-rabbitmqbus-check",
|
||||
tags: new string[] { "rabbitmqbus" });
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
11
src/Services/Coupon/Coupon.API/DTOs/CouponDto.cs
Normal file
11
src/Services/Coupon/Coupon.API/DTOs/CouponDto.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Coupon.API.DTOs
|
||||
{
|
||||
public record CouponDto
|
||||
{
|
||||
public int Discount { get; init; }
|
||||
|
||||
public string Code { get; init; }
|
||||
|
||||
public bool Consumed { get; init; }
|
||||
}
|
||||
}
|
59
src/Services/Coupon/Coupon.API/Dockerfile
Normal file
59
src/Services/Coupon/Coupon.API/Dockerfile
Normal file
@ -0,0 +1,59 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles
|
||||
# to take advantage of Docker's build cache, to speed up local container builds
|
||||
COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWebApps.sln"
|
||||
|
||||
COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj"
|
||||
COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj"
|
||||
COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj"
|
||||
COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj"
|
||||
COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj"
|
||||
COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj"
|
||||
COPY "BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj" "BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj"
|
||||
COPY "BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj" "BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj"
|
||||
COPY "BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj" "BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj"
|
||||
COPY "Services/Basket/Basket.API/Basket.API.csproj" "Services/Basket/Basket.API/Basket.API.csproj"
|
||||
COPY "Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj" "Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj"
|
||||
COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj"
|
||||
COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj"
|
||||
COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj"
|
||||
COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj"
|
||||
COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj"
|
||||
COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj"
|
||||
COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj"
|
||||
COPY "Services/Ordering/Ordering.Domain/Ordering.Domain.csproj" "Services/Ordering/Ordering.Domain/Ordering.Domain.csproj"
|
||||
COPY "Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj" "Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj"
|
||||
COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
|
||||
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
|
||||
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
|
||||
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
|
||||
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
|
||||
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
|
||||
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"
|
||||
COPY "Web/WebMVC/WebMVC.csproj" "Web/WebMVC/WebMVC.csproj"
|
||||
COPY "Web/WebSPA/WebSPA.csproj" "Web/WebSPA/WebSPA.csproj"
|
||||
COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj"
|
||||
|
||||
COPY "docker-compose.dcproj" "docker-compose.dcproj"
|
||||
|
||||
COPY "NuGet.config" "NuGet.config"
|
||||
|
||||
RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln"
|
||||
|
||||
COPY . .
|
||||
WORKDIR /src/Services/Coupon/Coupon.API
|
||||
RUN dotnet publish --no-restore -c Release -o /app
|
||||
|
||||
FROM build AS publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app .
|
||||
ENTRYPOINT ["dotnet", "Coupon.API.dll"]
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
108
src/Services/Coupon/Coupon.API/Program.cs
Normal file
108
src/Services/Coupon/Coupon.API/Program.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using Azure.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore;
|
||||
using System.Net;
|
||||
using Serilog;
|
||||
using Coupon.API;
|
||||
using Azure.Identity;
|
||||
|
||||
var configuration = GetConfiguration();
|
||||
|
||||
Log.Logger = CreateSerilogLogger(configuration);
|
||||
|
||||
try
|
||||
{
|
||||
Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName);
|
||||
var host = BuildWebHost(configuration, args);
|
||||
|
||||
Log.Information("Starting web host ({ApplicationContext})...", Program.AppName);
|
||||
host.Run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
|
||||
IWebHost BuildWebHost(IConfiguration configuration, string[] args) =>
|
||||
WebHost.CreateDefaultBuilder(args)
|
||||
.CaptureStartupErrors(false)
|
||||
.ConfigureKestrel(options =>
|
||||
{
|
||||
var ports = GetDefinedPorts(configuration);
|
||||
options.Listen(IPAddress.Any, ports.httpPort, listenOptions =>
|
||||
{
|
||||
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
|
||||
});
|
||||
|
||||
options.Listen(IPAddress.Any, ports.grpcPort, listenOptions =>
|
||||
{
|
||||
listenOptions.Protocols = HttpProtocols.Http2;
|
||||
});
|
||||
|
||||
})
|
||||
.ConfigureAppConfiguration(x => x.AddConfiguration(configuration))
|
||||
//.UseFailing(options =>
|
||||
//{
|
||||
// options.ConfigPath = "/Failing";
|
||||
// options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" });
|
||||
//})
|
||||
.UseStartup<Startup>()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseSerilog()
|
||||
.Build();
|
||||
|
||||
Serilog.ILogger CreateSerilogLogger(IConfiguration configuration)
|
||||
{
|
||||
var seqServerUrl = configuration["Serilog:SeqServerUrl"];
|
||||
var logstashUrl = configuration["Serilog:LogstashgUrl"];
|
||||
return new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.Enrich.WithProperty("ApplicationContext", Program.AppName)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl)
|
||||
.WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl)
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
IConfiguration GetConfiguration()
|
||||
{
|
||||
var builder = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
var config = builder.Build();
|
||||
|
||||
if (config.GetValue<bool>("UseVault", false))
|
||||
{
|
||||
TokenCredential credential = new ClientSecretCredential(
|
||||
config["Vault:TenantId"],
|
||||
config["Vault:ClientId"],
|
||||
config["Vault:ClientSecret"]);
|
||||
builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential);
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config)
|
||||
{
|
||||
var grpcPort = config.GetValue("GRPC_PORT", 5001);
|
||||
var port = config.GetValue("PORT", 80);
|
||||
return (port, grpcPort);
|
||||
}
|
||||
|
||||
public partial class Program
|
||||
{
|
||||
public static string Namespace = typeof(Startup).Namespace;
|
||||
public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
17
src/Services/Coupon/Coupon.API/Services/IdentityService.cs
Normal file
17
src/Services/Coupon/Coupon.API/Services/IdentityService.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Coupon.API.Services
|
||||
{
|
||||
public class IdentityService: IIdentityService
|
||||
{
|
||||
private IHttpContextAccessor _context;
|
||||
|
||||
public IdentityService(IHttpContextAccessor context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public string GetUserIdentity()
|
||||
{
|
||||
return _context.HttpContext.User.FindFirst("sub").Value;
|
||||
}
|
||||
}
|
||||
}
|
307
src/Services/Coupon/Coupon.API/Startup.cs
Normal file
307
src/Services/Coupon/Coupon.API/Startup.cs
Normal file
@ -0,0 +1,307 @@
|
||||
using Autofac;
|
||||
using Coupon.API.Controllers;
|
||||
using Coupon.API.Infrastructure;
|
||||
using Coupon.API.Infrastructure.Filters;
|
||||
using Coupon.API.Middlewares;
|
||||
using Coupon.API.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
|
||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using RabbitMQ.Client;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using HealthChecks.UI.Client;
|
||||
using Coupon.API.IntegrationEvents.EventHandling;
|
||||
using Coupon.API.IntegrationEvents.Events;
|
||||
|
||||
namespace Coupon.API
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
//services.AddGrpc(options =>
|
||||
//{
|
||||
// options.EnableDetailedErrors = true;
|
||||
//});
|
||||
|
||||
RegisterAppInsights(services);
|
||||
|
||||
services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
|
||||
//options.Filters.Add(typeof(ValidateModelStateFilter));
|
||||
|
||||
}) // Added for functional tests
|
||||
.AddApplicationPart(typeof(CouponController).Assembly)
|
||||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
|
||||
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "eShopOnContainers - Coupon HTTP API",
|
||||
Version = "v1",
|
||||
Description = "The Coupon Service HTTP API"
|
||||
});
|
||||
|
||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows()
|
||||
{
|
||||
Implicit = new OpenApiOAuthFlow()
|
||||
{
|
||||
AuthorizationUrl = new Uri($"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
|
||||
TokenUrl = new Uri($"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>()
|
||||
{
|
||||
{ "basket", "Basket API" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
||||
});
|
||||
|
||||
ConfigureAuthService(services);
|
||||
|
||||
services.AddCustomHealthCheck(Configuration);
|
||||
|
||||
services.Configure<CouponSettings>(Configuration);
|
||||
|
||||
//By connecting here we are making sure that our service
|
||||
//cannot start until redis is ready. This might slow down startup,
|
||||
//but given that there is a delay on resolving the ip address
|
||||
//and then creating the connection it seems reasonable to move
|
||||
//that cost to startup instead of having the first request pay the
|
||||
//penalty.
|
||||
//services.AddSingleton<ConnectionMultiplexer>(sp =>
|
||||
//{
|
||||
// var settings = sp.GetRequiredService<IOptions<CouponSettings>>().Value;
|
||||
// var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
|
||||
|
||||
// return ConnectionMultiplexer.Connect(configuration);
|
||||
//});
|
||||
|
||||
|
||||
if (Configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
||||
{
|
||||
services.AddSingleton<IServiceBusPersisterConnection>(sp =>
|
||||
{
|
||||
var serviceBusConnectionString = Configuration["EventBusConnection"];
|
||||
|
||||
return new DefaultServiceBusPersisterConnection(serviceBusConnectionString);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
|
||||
|
||||
var factory = new ConnectionFactory()
|
||||
{
|
||||
HostName = Configuration["EventBusConnection"],
|
||||
DispatchConsumersAsync = true
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(Configuration["EventBusUserName"]))
|
||||
{
|
||||
factory.UserName = Configuration["EventBusUserName"];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Configuration["EventBusPassword"]))
|
||||
{
|
||||
factory.Password = Configuration["EventBusPassword"];
|
||||
}
|
||||
|
||||
var retryCount = 5;
|
||||
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
|
||||
{
|
||||
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
|
||||
}
|
||||
|
||||
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
|
||||
});
|
||||
}
|
||||
|
||||
RegisterEventBus(services);
|
||||
|
||||
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("CorsPolicy",
|
||||
builder => builder
|
||||
.SetIsOriginAllowed((host) => true)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials());
|
||||
});
|
||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddTransient<ICouponRepository, CouponRepository>();
|
||||
services.AddTransient<IIdentityService, IdentityService>();
|
||||
|
||||
services.AddOptions();
|
||||
|
||||
var container = new ContainerBuilder();
|
||||
container.Populate(services);
|
||||
|
||||
return new AutofacServiceProvider(container.Build());
|
||||
}
|
||||
|
||||
private void RegisterAppInsights(IServiceCollection services)
|
||||
{
|
||||
services.AddApplicationInsightsTelemetry(Configuration);
|
||||
services.AddApplicationInsightsKubernetesEnricher();
|
||||
}
|
||||
|
||||
private void ConfigureAuthService(IServiceCollection services)
|
||||
{
|
||||
// prevent from mapping "sub" claim to nameidentifier.
|
||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
|
||||
|
||||
var identityUrl = Configuration.GetValue<string>("IdentityUrl");
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
|
||||
}).AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = identityUrl;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.Audience = "basket";
|
||||
});
|
||||
}
|
||||
|
||||
private void RegisterEventBus(IServiceCollection services)
|
||||
{
|
||||
if (Configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
||||
{
|
||||
services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
|
||||
{
|
||||
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();
|
||||
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
|
||||
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();
|
||||
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
|
||||
string subscriptionName = Configuration["SubscriptionClientName"];
|
||||
|
||||
return new EventBusServiceBus(serviceBusPersisterConnection, logger,
|
||||
eventBusSubscriptionsManager, iLifetimeScope, subscriptionName);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
|
||||
{
|
||||
var subscriptionClientName = Configuration["SubscriptionClientName"];
|
||||
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
|
||||
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
|
||||
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
|
||||
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
|
||||
|
||||
var retryCount = 5;
|
||||
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
|
||||
{
|
||||
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
|
||||
}
|
||||
|
||||
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubscriptionsManager, subscriptionClientName, retryCount);
|
||||
});
|
||||
}
|
||||
|
||||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
|
||||
|
||||
//services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
|
||||
services.AddTransient<OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
|
||||
{
|
||||
//loggerFactory.AddAzureWebAppDiagnostics();
|
||||
//loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
|
||||
|
||||
var pathBase = Configuration["PATH_BASE"];
|
||||
if (!string.IsNullOrEmpty(pathBase))
|
||||
{
|
||||
app.UsePathBase(pathBase);
|
||||
}
|
||||
|
||||
app.UseSwagger()
|
||||
.UseSwaggerUI(setup =>
|
||||
{
|
||||
setup.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "1Basket.API V1");
|
||||
setup.OAuthClientId("basketswaggerui");
|
||||
setup.OAuthAppName("Basket Swagger UI");
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
app.UseCors("CorsPolicy");
|
||||
ConfigureAuth(app);
|
||||
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
//endpoints.MapGrpcService<BasketService>();
|
||||
endpoints.MapDefaultControllerRoute();
|
||||
endpoints.MapControllers();
|
||||
//endpoints.MapGet("/_proto/", async ctx =>
|
||||
//{
|
||||
// ctx.Response.ContentType = "text/plain";
|
||||
// using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read);
|
||||
// using var sr = new StreamReader(fs);
|
||||
// while (!sr.EndOfStream)
|
||||
// {
|
||||
// var line = await sr.ReadLineAsync();
|
||||
// if (line != "/* >>" || line != "<< */")
|
||||
// {
|
||||
// await ctx.Response.WriteAsync(line);
|
||||
// }
|
||||
// }
|
||||
//});
|
||||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
|
||||
{
|
||||
Predicate = _ => true,
|
||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
||||
});
|
||||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
|
||||
{
|
||||
Predicate = r => r.Name.Contains("self")
|
||||
});
|
||||
});
|
||||
|
||||
ConfigureEventBus(app);
|
||||
}
|
||||
|
||||
protected virtual void ConfigureAuth(IApplicationBuilder app)
|
||||
{
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
}
|
||||
|
||||
private void ConfigureEventBus(IApplicationBuilder app)
|
||||
{
|
||||
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
|
||||
|
||||
//eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
|
||||
eventBus.Subscribe<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent, OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Information"
|
||||
}
|
||||
}
|
||||
}
|
13
src/Services/Coupon/Coupon.API/appsettings.json
Normal file
13
src/Services/Coupon/Coupon.API/appsettings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionString": "mongodb://localhost:27017",
|
||||
"DatabaseName": "eshop",
|
||||
"CouponsCollectionName": "coupons",
|
||||
"SubscriptionClientName": "Coupon"
|
||||
}
|
5
src/Services/Coupon/Coupon.API/libman.json
Normal file
5
src/Services/Coupon/Coupon.API/libman.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"defaultProvider": "cdnjs",
|
||||
"libraries": []
|
||||
}
|
@ -54,6 +54,9 @@ public class CreateOrderCommand
|
||||
[DataMember]
|
||||
public int CardTypeId { get; private set; }
|
||||
|
||||
[DataMember]
|
||||
public string CouponCode { get; private set; }
|
||||
|
||||
[DataMember]
|
||||
public IEnumerable<OrderItemDTO> OrderItems => _orderItems;
|
||||
|
||||
@ -64,7 +67,7 @@ public class CreateOrderCommand
|
||||
|
||||
public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,
|
||||
string cardNumber, string cardHolderName, DateTime cardExpiration,
|
||||
string cardSecurityNumber, int cardTypeId) : this()
|
||||
string cardSecurityNumber, int cardTypeId, string couponCode = null) : this()
|
||||
{
|
||||
_orderItems = basketItems.ToOrderItemsDTO().ToList();
|
||||
UserId = userId;
|
||||
@ -79,6 +82,7 @@ public class CreateOrderCommand
|
||||
CardExpiration = cardExpiration;
|
||||
CardSecurityNumber = cardSecurityNumber;
|
||||
CardTypeId = cardTypeId;
|
||||
CouponCode = couponCode;
|
||||
}
|
||||
|
||||
|
||||
|
@ -37,7 +37,7 @@ public class CreateOrderCommandHandler
|
||||
// methods and constructor so validations, invariants and business logic
|
||||
// make sure that consistency is preserved across the whole aggregate
|
||||
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
|
||||
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
|
||||
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration, couponCode: message.CouponCode);
|
||||
|
||||
foreach (var item in message.OrderItems)
|
||||
{
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStockConfirmed;
|
||||
using Ordering.API.Application.IntegrationEvents.Events;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStockConfirmed;
|
||||
|
||||
public class OrderStatusChangedToStockConfirmedDomainEventHandler
|
||||
: INotificationHandler<OrderStatusChangedToStockConfirmedDomainEvent>
|
||||
@ -27,9 +29,13 @@ public class OrderStatusChangedToStockConfirmedDomainEventHandler
|
||||
orderStatusChangedToStockConfirmedDomainEvent.OrderId, nameof(OrderStatus.StockConfirmed), OrderStatus.StockConfirmed.Id);
|
||||
|
||||
var order = await _orderRepository.GetAsync(orderStatusChangedToStockConfirmedDomainEvent.OrderId);
|
||||
var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
||||
//var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString());
|
||||
|
||||
var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
||||
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToStockConfirmedIntegrationEvent);
|
||||
var orderStatusChangedToAwaitingCouponValidationIntegrationEvent =
|
||||
new OrderStatusChangedToAwaitingCouponValidationIntegrationEvent(order.Id, order.CouponCode);
|
||||
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToAwaitingCouponValidationIntegrationEvent);
|
||||
|
||||
//var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name);
|
||||
//await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToStockConfirmedIntegrationEvent);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling;
|
||||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
|
||||
using Ordering.API.Application.IntegrationEvents.Events;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling;
|
||||
|
||||
public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHandler<UserCheckoutAcceptedIntegrationEvent>
|
||||
{
|
||||
@ -37,7 +40,7 @@ public class UserCheckoutAcceptedIntegrationEventHandler : IIntegrationEventHand
|
||||
var createOrderCommand = new CreateOrderCommand(@event.Basket.Items, @event.UserId, @event.UserName, @event.City, @event.Street,
|
||||
@event.State, @event.Country, @event.ZipCode,
|
||||
@event.CardNumber, @event.CardHolderName, @event.CardExpiration,
|
||||
@event.CardSecurityNumber, @event.CardTypeId);
|
||||
@event.CardSecurityNumber, @event.CardTypeId, @event.CouponCode);
|
||||
|
||||
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand, bool>(createOrderCommand, @event.RequestId);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -32,10 +32,12 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
||||
|
||||
public CustomerBasket Basket { get; }
|
||||
|
||||
public string CouponCode { get; set; }
|
||||
|
||||
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
|
||||
string state, string country, string zipCode, string cardNumber, string cardHolderName,
|
||||
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
|
||||
CustomerBasket basket)
|
||||
CustomerBasket basket, string couponCode = null)
|
||||
{
|
||||
UserId = userId;
|
||||
City = city;
|
||||
@ -52,6 +54,7 @@ public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
||||
Basket = basket;
|
||||
RequestId = requestId;
|
||||
UserName = userName;
|
||||
CouponCode = couponCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ public class OrderQueries
|
||||
var result = await connection.QueryAsync<dynamic>(
|
||||
@"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description,
|
||||
o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode,
|
||||
os.Name as status,
|
||||
os.Name as status, o.CouponCode as couponcode,
|
||||
oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl
|
||||
FROM ordering.Orders o
|
||||
LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid
|
||||
@ -70,7 +70,9 @@ public class OrderQueries
|
||||
zipcode = result[0].zipcode,
|
||||
country = result[0].country,
|
||||
orderitems = new List<Orderitem>(),
|
||||
total = 0
|
||||
total = 0,
|
||||
couponcode = result[0].couponcode,
|
||||
discount = result[0].couponcode == null ? 0 : int.Parse((result[0].couponcode as string).Split("-").Last()),
|
||||
};
|
||||
|
||||
foreach (dynamic item in result)
|
||||
|
@ -20,6 +20,8 @@ public record Order
|
||||
public string country { get; init; }
|
||||
public List<Orderitem> orderitems { get; set; }
|
||||
public decimal total { get; set; }
|
||||
public decimal discount { get; set; }
|
||||
public string couponcode { get; set; }
|
||||
}
|
||||
|
||||
public record OrderSummary
|
||||
|
@ -6,11 +6,19 @@ namespace Ordering.API.Infrastructure.Migrations
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CouponCode",
|
||||
schema: "ordering",
|
||||
table: "orders",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CouponCode",
|
||||
schema: "ordering",
|
||||
table: "orders");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@
|
||||
<Protobuf Include="Proto\ordering.proto" GrpcServices="Server" Generator="MSBuild:Compile" />
|
||||
<Content Include="Proto\ordering.proto" />
|
||||
<None Remove="@(Protobuf)" />
|
||||
<None Remove="Application\IntegrationEvents\EventHandling\OrderCouponConfirmedIntegrationEventHandler.cs~RF159e4744.TMP" />
|
||||
<None Remove="Application\IntegrationEvents\EventHandling\UserCheckoutAcceptedIntegrationEventHandler.cs~RFaf93a3b.TMP" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -44,6 +46,8 @@
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.4.0" />
|
||||
<PackageReference Include="Dapper" Version="2.0.78" />
|
||||
<PackageReference Include="FluentMigrator" Version="3.3.2" />
|
||||
<PackageReference Include="FluentMigrator.Runner.SqlServer" Version="3.3.2" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="9.3.0" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.15.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.34.0" />
|
||||
@ -56,6 +60,10 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NETCore.Platforms" Version="6.0.0" />
|
||||
<PackageReference Include="Polly" Version="7.2.1" />
|
||||
|
@ -1,3 +1,5 @@
|
||||
using Ordering.API.Application.IntegrationEvents.Events;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API;
|
||||
|
||||
public class Startup
|
||||
@ -106,6 +108,8 @@ public class Startup
|
||||
eventBus.Subscribe<OrderStockRejectedIntegrationEvent, IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>>();
|
||||
eventBus.Subscribe<OrderPaymentFailedIntegrationEvent, IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>>();
|
||||
eventBus.Subscribe<OrderPaymentSucceededIntegrationEvent, IIntegrationEventHandler<OrderPaymentSucceededIntegrationEvent>>();
|
||||
eventBus.Subscribe<OrderCouponConfirmedIntegrationEvent, IIntegrationEventHandler<OrderCouponConfirmedIntegrationEvent>>();
|
||||
eventBus.Subscribe<OrderCouponRejectedIntegrationEvent, IIntegrationEventHandler<OrderCouponRejectedIntegrationEvent>>();
|
||||
}
|
||||
|
||||
protected virtual void ConfigureAuth(IApplicationBuilder app)
|
||||
|
@ -13,6 +13,8 @@ public class Order
|
||||
// Address is a Value Object pattern example persisted as EF Core 2.0 owned entity
|
||||
public Address Address { get; private set; }
|
||||
|
||||
public string CouponCode { get; private set; }
|
||||
|
||||
public int? GetBuyerId => _buyerId;
|
||||
private int? _buyerId;
|
||||
|
||||
@ -49,13 +51,14 @@ public class Order
|
||||
}
|
||||
|
||||
public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
|
||||
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) : this()
|
||||
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null, string couponCode = null) : this()
|
||||
{
|
||||
_buyerId = buyerId;
|
||||
_paymentMethodId = paymentMethodId;
|
||||
_orderStatusId = OrderStatus.Submitted.Id;
|
||||
_orderDate = DateTime.UtcNow;
|
||||
Address = address;
|
||||
CouponCode = couponCode;
|
||||
|
||||
// Add the OrderStarterDomainEvent to the domain events collection
|
||||
// to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ]
|
||||
|
@ -11,6 +11,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="10.0.1" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
||||
|
23473
src/Web/WebSPA/Client/package-lock.json
generated
23473
src/Web/WebSPA/Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -34,9 +34,20 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div *ngIf="order.couponcode">
|
||||
<div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase">
|
||||
<div>Subtotal</div>
|
||||
<div class="ml-3">${{order.total | number:'.2-2'}}</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase">
|
||||
<div>Coupon</div>
|
||||
<div class="ml-3">{{order.couponcode}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase u-text-xl">
|
||||
<div>Total</div>
|
||||
<div class="ml-3">${{order.total | number:'.2-2'}}</div>
|
||||
<div class="ml-3">${{order.total - order.discount | number:'.2-2'}}</div>
|
||||
</div>
|
||||
|
||||
<aside class="d-flex justify-content-end mt-5">
|
||||
|
@ -84,9 +84,41 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-end mt-4 mb-4 text-uppercase">
|
||||
<div>Subtotal</div>
|
||||
<div class="ml-3">${{order.total | number:'.2-2'}}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-nowrap justify-content-between align-items-center mb-3 mt-3">
|
||||
<div>
|
||||
<div *ngIf="!coupon">
|
||||
<div class="u-text-uppercase">Have a discount code?</div>
|
||||
<div class="d-flex flex-nowrap justify-content-between align-items-center mt-1">
|
||||
<input #discountcode
|
||||
class="esh-orders_new-coupon mr-2 form-control"
|
||||
type="text"
|
||||
placeholder="Coupon number"
|
||||
(keydown)="keyDownValidationCoupon($event, discountcode.value)">
|
||||
<button type="button"
|
||||
(click)="checkValidationCoupon(discountcode.value)"
|
||||
class="btn btn-secondary u-minwidth-unset">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-1" *ngIf="couponValidationMessage">{{couponValidationMessage}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-end text-uppercase">
|
||||
<div *ngIf="coupon?.code">{{coupon?.code}}</div>
|
||||
<div>
|
||||
<div class="text-right ml-3" *ngIf="coupon?.discount">-${{coupon?.discount | number:'.2-2'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider d-flex align-items-center justify-content-end mb-4 pt-4 text-uppercase u-text-xl">
|
||||
<div>Total</div>
|
||||
<div class="ml-3">${{ order.total | number:'.2-2'}}</div>
|
||||
<div class="ml-3">${{ order.total - coupon?.discount | number:'.2-2'}}</div>
|
||||
</div>
|
||||
|
||||
<div class="esh-orders_new-buttons d-flex justify-content-end align-items-center">
|
||||
|
@ -9,6 +9,8 @@ import { BasketWrapperService } from '../../shared/services/
|
||||
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ICoupon } from '../../shared/models/coupon.model';
|
||||
import { DataService } from '../../shared/services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'esh-orders_new .esh-orders_new .mb-5',
|
||||
@ -20,6 +22,7 @@ export class OrdersNewComponent implements OnInit {
|
||||
isOrderProcessing: boolean;
|
||||
errorReceived: boolean;
|
||||
order: IOrder;
|
||||
coupon: ICoupon;
|
||||
|
||||
constructor(private orderService: OrdersService, private basketService: BasketService, fb: FormBuilder, private router: Router) {
|
||||
// Obtain user profile information
|
||||
@ -39,6 +42,13 @@ export class OrdersNewComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
checkValidationCoupon(value: string)
|
||||
{
|
||||
this.coupon = <ICoupon>{};
|
||||
this.coupon.code = value;
|
||||
this.coupon.discount = +value.split('-')[1];
|
||||
}
|
||||
|
||||
submitForm(value: any) {
|
||||
this.order.street = this.newOrderForm.controls['street'].value;
|
||||
this.order.city = this.newOrderForm.controls['city'].value;
|
||||
@ -50,6 +60,9 @@ export class OrdersNewComponent implements OnInit {
|
||||
this.order.cardexpiration = new Date(20 + this.newOrderForm.controls['expirationdate'].value.split('/')[1], this.newOrderForm.controls['expirationdate'].value.split('/')[0]);
|
||||
this.order.cardsecuritynumber = this.newOrderForm.controls['securitycode'].value;
|
||||
let basketCheckout = this.basketService.mapBasketInfoCheckout(this.order);
|
||||
if (this.coupon) {
|
||||
basketCheckout.couponCode = this.coupon.code;
|
||||
}
|
||||
this.basketService.setBasketCheckout(basketCheckout)
|
||||
.pipe(catchError((errMessage) => {
|
||||
this.errorReceived = true;
|
||||
|
@ -13,4 +13,5 @@
|
||||
buyer: string;
|
||||
ordernumber: string;
|
||||
total: number;
|
||||
couponCode: string;
|
||||
}
|
@ -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;
|
||||
}
|
@ -12,4 +12,6 @@ export interface IOrderDetail {
|
||||
country: number;
|
||||
total: number;
|
||||
orderitems: IOrderItem[];
|
||||
couponcode: string;
|
||||
discount: number;
|
||||
}
|
||||
|
@ -3,6 +3,13 @@
|
||||
ARG NODE_IMAGE=node:12.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||
WORKDIR /app
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install curl gnupg
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN apt-get -y install nodejs
|
||||
RUN npm install
|
||||
RUN npm -v
|
||||
RUN npm install -g @angular/cli@11.2.14
|
||||
EXPOSE 80
|
||||
|
||||
FROM ${NODE_IMAGE} as node-build
|
||||
@ -60,6 +67,13 @@ RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln"
|
||||
COPY . .
|
||||
COPY --from=node-build /web/wwwroot /src/Web/WebSPA/wwwroot/
|
||||
WORKDIR /src/Web/WebSPA
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install curl gnupg
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN apt-get -y install nodejs
|
||||
RUN npm install
|
||||
RUN npm -v
|
||||
RUN npm install -g @angular/cli@11.2.14
|
||||
RUN dotnet publish --no-restore -c Release -o /app
|
||||
|
||||
FROM build AS publish
|
||||
|
@ -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>
|
||||
|
@ -6,6 +6,7 @@
|
||||
"PurchaseUrlHC": "http://host.docker.internal:5202/hc",
|
||||
"IdentityUrlHC": "http://host.docker.internal:5105/hc",
|
||||
"SignalrHubUrl": "http://host.docker.internal:5112",
|
||||
"CouponUrl" : "http://host.docker.internal:5203",
|
||||
"UseCustomizationData": true,
|
||||
"IsClusterEnv": "False",
|
||||
"ActivateCampaignDetailFunction": false,
|
||||
|
3
src/Web/WebSPA/package-lock.json
generated
Normal file
3
src/Web/WebSPA/package-lock.json
generated
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
@ -79,6 +79,27 @@ services:
|
||||
- "5103:80"
|
||||
- "9103:81"
|
||||
|
||||
coupon-api:
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_URLS=http://0.0.0.0:80
|
||||
- ConnectionString=${ESHOP_AZURE_MONGO_COUPON_DB}
|
||||
- identityUrl=http://identity-api
|
||||
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
|
||||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
|
||||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
|
||||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
|
||||
- AzureServiceBusEnabled=False
|
||||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
|
||||
- OrchestratorType=${ORCHESTRATOR_TYPE}
|
||||
- UseLoadTest=${USE_LOADTEST:-False}
|
||||
- PATH_BASE=/coupon-api
|
||||
- GRPC_PORT=81
|
||||
- PORT=80
|
||||
ports:
|
||||
- "5203:80"
|
||||
- "9203:81"
|
||||
|
||||
catalog-api:
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
@ -98,8 +119,8 @@ services:
|
||||
- PORT=80
|
||||
- PATH_BASE=/catalog-api
|
||||
ports:
|
||||
- "5101:80"
|
||||
- "9101:81"
|
||||
- "5201:80"
|
||||
- "9201:81"
|
||||
|
||||
ordering-api:
|
||||
environment:
|
||||
@ -272,7 +293,7 @@ services:
|
||||
|
||||
webspa:
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_URLS=http://0.0.0.0:80
|
||||
- IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
|
||||
- PurchaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5202
|
||||
|
@ -78,6 +78,16 @@ services:
|
||||
depends_on:
|
||||
- sqldata
|
||||
|
||||
coupon-api:
|
||||
image: ${REGISTRY:-eshop}/coupon.api:${PLATFORM:-linux}-${TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Services/Coupon/Coupon.API/Dockerfile
|
||||
depends_on:
|
||||
- rabbitmq
|
||||
- nosqldata
|
||||
- identity-api
|
||||
|
||||
mobileshoppingapigw:
|
||||
image: envoyproxy/envoy:v1.11.1
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29020.237
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32526.322
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{FEA0C318-FFED-4D39-8781-265718CA43DD}"
|
||||
EndProject
|
||||
@ -124,6 +124,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{373D8AA1
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{95D735BE-2899-4495-BE3F-2600E93B4E3C}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Coupon", "Coupon", "{44844635-633D-49B3-A70E-E44E8E2992EE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coupon.API", "Services\Coupon\Coupon.API\Coupon.API.csproj", "{D9651304-75EB-4EA7-8314-86C06C253EA8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
|
||||
@ -450,8 +454,8 @@ Global
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x64.Build.0 = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x86.ActiveCfg = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x86.Build.0 = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||
@ -1530,6 +1534,54 @@ Global
|
||||
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|ARM.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|ARM.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|iPhone.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|x64.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|x64.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|x86.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.AppStore|x86.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|iPhone.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|ARM.ActiveCfg = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|ARM.Build.0 = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|iPhone.ActiveCfg = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|iPhone.Build.0 = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -1588,6 +1640,8 @@ Global
|
||||
{B62E859F-825E-4C8B-93EC-5966EACFD026} = {798BFC44-2CCD-45FA-B37A-5173B03C2B30}
|
||||
{373D8AA1-36BE-49EC-89F0-6CB736666285} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F}
|
||||
{95D735BE-2899-4495-BE3F-2600E93B4E3C} = {373D8AA1-36BE-49EC-89F0-6CB736666285}
|
||||
{44844635-633D-49B3-A70E-E44E8E2992EE} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8}
|
||||
{D9651304-75EB-4EA7-8314-86C06C253EA8} = {44844635-633D-49B3-A70E-E44E8E2992EE}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9}
|
||||
|
Loading…
x
Reference in New Issue
Block a user