<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Handlers/Customers/CustomerCreatedHandler.cs<|end_filename|>
using System.Threading.Tasks;
using DShop.Common.Handlers;
using DShop.Common.RabbitMq;
using DShop.Services.Discounts.Domain;
using DShop.Services.Discounts.Messages.Events;
using DShop.Services.Discounts.Repositories;
using Microsoft.Extensions.Logging;

namespace DShop.Services.Discounts.Handlers.Customers
{
    public class CustomerCreatedHandler : IEventHandler<CustomerCreated>
    {
        private readonly ICustomersRepository _customersRepository;
        private readonly ILogger<CustomerCreatedHandler> _logger;

        public CustomerCreatedHandler(ICustomersRepository customersRepository,
            ILogger<CustomerCreatedHandler> logger)
        {
            _customersRepository = customersRepository;
            _logger = logger;
        }

        public async Task HandleAsync(CustomerCreated @event, ICorrelationContext context)
        {
            await _customersRepository.AddAsync(new Customer(@event.Id, @event.Email));
            _logger.LogInformation($"Created customer with id: '{@event.Id}'.");
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Metrics/MetricsRegistry.cs<|end_filename|>
using App.Metrics;
using App.Metrics.Counter;

namespace DShop.Services.Discounts.Metrics
{
    public class MetricsRegistry : IMetricsRegistry
    {
        private readonly IMetricsRoot _metricsRoot;
        private readonly CounterOptions _findDiscountsQueries = new CounterOptions { Name = "find-discounts" };

        public MetricsRegistry(IMetricsRoot metricsRoot)
            => _metricsRoot = metricsRoot;
        
        public void IncrementFindDiscountsQuery()
            => _metricsRoot.Measure.Counter.Increment(_findDiscountsQueries);
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Services/IOrdersService.cs<|end_filename|>
using System;
using System.Threading.Tasks;
using DShop.Services.Discounts.Dto;
using RestEase;

namespace DShop.Services.Discounts.Services
{
    public interface IOrdersService
    {
        [AllowAnyStatusCode]
        [Get("orders/{id}")]
        Task<OrderDetailsDto> GetAsync([Path] Guid id);
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Dto/DiscountDetailsDto.cs<|end_filename|>
namespace DShop.Services.Discounts.Dto
{
    public class DiscountDetailsDto
    {
        public CustomerDto Customer { get; set; }
        public DiscountDto Discount { get; set; }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/test/DShop.Services.Discounts.IntegrationTests/Fixtures/RabbitMqFixture.cs<|end_filename|>
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using DShop.Common;
using DShop.Common.Messages;
using DShop.Common.RabbitMq;
using DShop.Services.Discounts.Domain;
using DShop.Services.Discounts.IntegrationTests.Config;
using RawRabbit.Configuration;
using RawRabbit.Instantiation;
using RawRabbit;
using RawRabbit.Common;
using RawRabbit.Enrichers.MessageContext;
using Xunit;

namespace DShop.Services.Discounts.IntegrationTests.Fixtures
{   
    public class RabbitMqFixture
    {
        private readonly RawRabbit.Instantiation.Disposable.BusClient _client;
        bool _disposed = false;
        
        public RabbitMqFixture()
        {
            _client = RawRabbitFactory.CreateSingleton(new RawRabbitOptions()
            {
                ClientConfiguration = new RawRabbitConfiguration
                {
                    Hostnames = new List<string> { "localhost" }, // localhost
                    VirtualHost = "/",
                    Port = 5672,
                    Username = "guest",
                    Password = "<PASSWORD>",
                },
                DependencyInjection = ioc =>
                {
                    ioc.AddSingleton<INamingConventions>(new RabbitMqNamingConventions("discounts"));
                },
                Plugins = p => p  
                    .UseAttributeRouting()
                    .UseRetryLater()
                    .UseMessageContext<CorrelationContext>()
                    .UseContextForwarding()
            });
        }

        public Task PublishAsync<TMessage>(TMessage message, string @namespace = null) where TMessage : class
            => _client.PublishAsync(message, ctx => 
                ctx.UseMessageContext(CorrelationContext.Empty).UsePublishConfiguration(p => p.WithRoutingKey(GetRoutingKey(@message, @namespace))));
        
        public async Task<TaskCompletionSource<Discount>> SubscribeAndGetAsync<TEvent>(
            Func<Guid, TaskCompletionSource<Discount>, Task> onMessageReceived, Guid id) where TEvent : IEvent
        {
            var taskCompletionSource = new TaskCompletionSource<Discount>();
            var guid = Guid.NewGuid().ToString();
            
            await _client.SubscribeAsync<TEvent>(
                async _ => await onMessageReceived(id, taskCompletionSource),
                ctx => ctx.UseSubscribeConfiguration(cfg =>
                    cfg
                        .FromDeclaredQueue(
                            builder => builder
                                .WithDurability(false)
                                .WithName(guid))));
            return taskCompletionSource;
        }
        
        private string GetRoutingKey<T>(T message, string @namespace = null)
        {
            @namespace = @namespace ?? "discounts";
            @namespace = string.IsNullOrWhiteSpace(@namespace) ? string.Empty : $"{@namespace}.";

            return $"{@namespace}{typeof(T).Name.Underscore()}".ToLowerInvariant();
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
            if (disposing)
            {
                _client.Dispose();
            }

            _disposed = true;
        }
        
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Dockerfile<|end_filename|>
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2
COPY ./bin/docker .
ENV ASPNETCORE_ENVIRONMENT docker
ENV ASPNETCORE_URLS http://*:5000
ENTRYPOINT dotnet DShop.Services.Discounts.dll

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Repositories/ICustomersRepository.cs<|end_filename|>
using System;
using System.Threading.Tasks;
using DShop.Services.Discounts.Domain;

namespace DShop.Services.Discounts.Repositories
{
    public interface ICustomersRepository
    {
        Task<Customer> GetAsync(Guid id);
        Task AddAsync(Customer customer);
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Dto/OrderDetailsDto.cs<|end_filename|>
using System;
using System.Collections.Generic;

namespace DShop.Services.Discounts.Dto
{
    public class OrderDetailsDto
    {
        public Guid Id { get; set; }
        public Guid CustomerId { get; set; }
        public int ItemsCount { get; set; }
        public decimal TotalAmount { get; set; }
        public string Currency { get; set; }
        public string Status { get; set; }
        public IEnumerable<OrderItemDto> Items { get; set; }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Handlers/Discounts/CreateDiscountHandler.cs<|end_filename|>
using System.Threading.Tasks;
using DShop.Common.Handlers;
using DShop.Common.RabbitMq;
using DShop.Common.Types;
using DShop.Services.Discounts.Domain;
using DShop.Services.Discounts.Messages.Commands;
using DShop.Services.Discounts.Messages.Events;
using DShop.Services.Discounts.Repositories;
using Microsoft.Extensions.Logging;

namespace DShop.Services.Discounts.Handlers.Discounts
{
    public class CreateDiscountHandler : ICommandHandler<CreateDiscount>
    {
        private readonly IDiscountsRepository _discountsRepository;
        private readonly ICustomersRepository _customersRepository;
        private readonly IBusPublisher _busPublisher;
        private readonly ILogger<CreateDiscountHandler> _logger;

        public CreateDiscountHandler(IDiscountsRepository discountsRepository,
            ICustomersRepository customersRepository,
            IBusPublisher busPublisher, ILogger<CreateDiscountHandler> logger)
        {
            _discountsRepository = discountsRepository;
            _customersRepository = customersRepository;
            _busPublisher = busPublisher;
            _logger = logger;
        }
        
        // Command -> Event - how to track this simple use case?
        // Command -> Event -> Event -> Command -> Event ... or this one?

        public async Task HandleAsync(CreateDiscount command, ICorrelationContext context)
        {
            // Customer validation
            var customer = await _customersRepository.GetAsync(command.CustomerId);
            if (customer is null)
            {
                //onError: -> publish CreateDiscountRejected
                throw new DShopException("customer_not_found",
                    $"Customer with id: '{command.CustomerId}' was not found.");
                
                // _logger.LogWarning($"Customer with id: '{command.CustomerId}' was not found.");
                // await _busPublisher.PublishAsync(new CreateDiscountRejected(command.CustomerId,
                //     $"Customer with id: '{command.CustomerId}' was not found.",
                //     "customer_not_found"), context);
                
                // return;
            }

            // Unique code validation
            var discount = new Discount(command.Id, command.CustomerId,
                command.Code, command.Percentage);
            await _discountsRepository.AddAsync(discount);
            await _busPublisher.PublishAsync(new DiscountCreated(command.Id,
                command.CustomerId, command.Code, command.Percentage), context);
            // Send an email about a new discount to the customer
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/test/DShop.Services.Discounts.IntegrationTests/ControllersTests.cs<|end_filename|>
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using DShop.Services.Discounts.Domain;
using DShop.Services.Discounts.Dto;
using DShop.Services.Discounts.IntegrationTests.Fixtures;
using Microsoft.AspNetCore.Mvc.Testing;
using Newtonsoft.Json;
using RawRabbit.Serialization;
using Shouldly;
using Xunit;

namespace DShop.Services.Discounts.IntegrationTests
{
    public class ControllersTests : IClassFixture<MongoDbFixture>,
        IClassFixture<WebApplicationFactory<Startup>>
    {
        private readonly MongoDbFixture _mongoDbFixture;
        private readonly HttpClient _client;

        public ControllersTests(MongoDbFixture mongoDbFixture,WebApplicationFactory<Startup> factory)
        {
            _mongoDbFixture = mongoDbFixture;
            _client = factory.CreateClient();
        }

        [Theory]
        [InlineData("")]
        [InlineData("ping")]
        [InlineData("discounts")]
        public async Task Given_Endpoints_Should_Return_Success_Http_Status_Code(string endpoint)
        {
            var response = await _client.GetAsync(endpoint);
            response.IsSuccessStatusCode.ShouldBeTrue();
        }

        [Fact]
        public async Task Discounts_Details_Endpoint_Should_Return_Correct_Model()
        {
            var customer = new Customer(Guid.NewGuid(), "<EMAIL>");
            var discount = new Discount(Guid.NewGuid(), customer.Id, "DISCOUNT", 10);

            await _mongoDbFixture.InsertAsync("Customers", customer);
            await _mongoDbFixture.InsertAsync("Discounts", discount);

            var response = await _client.GetAsync($"discounts/{discount.Id}");
            var contentString = await response.Content.ReadAsStringAsync();
            var discountsDetails = JsonConvert.DeserializeObject<DiscountDetailsDto>(contentString);
            
            discountsDetails.Customer.ShouldNotBeNull();
            discountsDetails.Customer.Id.ShouldBe(customer.Id);
            discountsDetails.Customer.Email.ShouldBe(customer.Email);
            discountsDetails.Discount.ShouldNotBeNull();
            discountsDetails.Discount.Id.ShouldBe(discount.Id);
            discountsDetails.Discount.CustomerId.ShouldBe(customer.Id);
            discountsDetails.Discount.Code.ShouldBe(discount.Code);
            discountsDetails.Discount.Percentage.ShouldBe(discount.Percentage);
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/test/DShop.Services.Discounts.IntegrationTests/Config/RabbitMqNamingConventions.cs<|end_filename|>
using System;
using System.Reflection;
using DShop.Common;
using DShop.Common.Messages;
using RawRabbit.Common;

namespace DShop.Services.Discounts.IntegrationTests.Config
{  
    public class RabbitMqNamingConventions : NamingConventions
    {
        public RabbitMqNamingConventions(string defaultNamespace)
        {
            ExchangeNamingConvention = type => GetNamespace(type, defaultNamespace).ToLowerInvariant();
            RoutingKeyConvention = type =>
                $"#.{GetRoutingKeyNamespace(type, defaultNamespace)}{type.Name.Underscore()}".ToLowerInvariant();
            ErrorExchangeNamingConvention = () => $"{defaultNamespace}.error";
            RetryLaterExchangeConvention = span => $"{defaultNamespace}.retry";
            RetryLaterQueueNameConvetion = (exchange, span) =>
                $"{defaultNamespace}.retry_for_{exchange.Replace(".", "_")}_in_{span.TotalMilliseconds}_ms".ToLowerInvariant();
        }

        private static string GetRoutingKeyNamespace(Type type, string defaultNamespace)
        {
            var @namespace = type.GetCustomAttribute<MessageNamespaceAttribute>()?.Namespace ?? defaultNamespace;

            return string.IsNullOrWhiteSpace(@namespace) ? string.Empty : $"{@namespace}.";
        }
            
        private static string GetNamespace(Type type, string defaultNamespace)
        {
            var @namespace = type.GetCustomAttribute<MessageNamespaceAttribute>()?.Namespace ?? defaultNamespace;

            return string.IsNullOrWhiteSpace(@namespace) ? "#" : $"{@namespace}";
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Repositories/DiscountsRepository.cs<|end_filename|>
using System.Threading.Tasks;
using DShop.Common.Mongo;
using DShop.Services.Discounts.Domain;

namespace DShop.Services.Discounts.Repositories
{
    public class DiscountsRepository : IDiscountsRepository
    {
        private readonly IMongoRepository<Discount> _repository;

        public DiscountsRepository(IMongoRepository<Discount> repository)
        {
            _repository = repository;
        }

        public async Task AddAsync(Discount discount)
            => await _repository.AddAsync(discount);

        public async Task UpdateAsync(Discount discount)
            => await _repository.UpdateAsync(discount);
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/test/DShop.Services.Discounts.IntegrationTests/DiscountsCommandsTests.cs<|end_filename|>
using System;
using System.Threading.Tasks;
using DShop.Services.Discounts.Domain;
using DShop.Services.Discounts.IntegrationTests.Fixtures;
using DShop.Services.Discounts.Messages.Commands;
using DShop.Services.Discounts.Messages.Events;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Shouldly;
using Xunit;

namespace DShop.Services.Discounts.IntegrationTests
{
    public class DiscountsCommandsTests :  IClassFixture<MongoDbFixture>, IClassFixture<RabbitMqFixture>,
        IClassFixture<WebApplicationFactory<Startup>>
    {
        private readonly MongoDbFixture _mongoDbFixture;
        private readonly RabbitMqFixture _rabbitMqFixture;

        public DiscountsCommandsTests(MongoDbFixture mongoFixture, RabbitMqFixture rabbitFixture,
            WebApplicationFactory<Startup> factory)
        {
            _mongoDbFixture = mongoFixture;
            _rabbitMqFixture = rabbitFixture;
            var client = factory.CreateClient();
        }

        [Fact]
        public async Task Create_Discount_Command_Should_Create_MongoEntity()
        {
            var customerId = Guid.NewGuid();
            await _mongoDbFixture.InsertAsync("Customers", new Customer(customerId, "<EMAIL>"));
            
            var command = new CreateDiscount(Guid.NewGuid(), customerId, "DISCOUNT", 10);
            var creationTask = await _rabbitMqFixture.SubscribeAndGetAsync<DiscountCreated>(_mongoDbFixture.GetMongoEntity, command.Id);
            await _rabbitMqFixture.PublishAsync(command);
            
            var createdMongoEntity = await creationTask.Task;
            
            createdMongoEntity.Id.ShouldBe(command.Id);
            createdMongoEntity.CustomerId.ShouldBe(command.CustomerId);
            createdMongoEntity.Code.ShouldBe(command.Code);
            createdMongoEntity.Percentage.ShouldBe(command.Percentage);
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Handlers/Orders/OrderCompletedHandler.cs<|end_filename|>
using System.Net.Http;
using System.Threading.Tasks;
using DShop.Common.Consul;
using DShop.Common.Handlers;
using DShop.Common.RabbitMq;
using DShop.Services.Discounts.Dto;
using DShop.Services.Discounts.Messages.Events;
using DShop.Services.Discounts.Services;
using Newtonsoft.Json;

namespace DShop.Services.Discounts.Handlers.Orders
{
    public class OrderCompletedHandler : IEventHandler<OrderCompleted>
    {
        private readonly IOrdersService _ordersService;

        public OrderCompletedHandler(IOrdersService ordersService)
        {
            _ordersService = ordersService;
        }
        
        public async Task HandleAsync(OrderCompleted @event, ICorrelationContext context)
        {
            // Hard-coded way
            // Level 0
//            var response = await _httpClient.GetAsync($"http://localhost:5005/orders/{@event.Id}");
//            var content = await response.Content.ReadAsStringAsync();
//            var orderDto = JsonConvert.DeserializeObject<OrderDetailsDto>(content);
            
            // Level 1 - store microservice URL in appsettings.json e.g. for RestEase
            
            // Level 2 - use service discovery
//            var orderDto = await _consulHttpClient
//                .GetAsync<OrderDetailsDto>($"orders-service/orders/{@event.Id}");
            
            // Level 3 -  additional load balancer - Fabio
            // var response = await _httpClient.GetAsync($"http://localhost:9999/orders-service/orders/{@event.Id}");

            // Level 4
            var orderDto = await _ordersService.GetAsync(@event.Id);
            
            await Task.CompletedTask;
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Domain/Customer.cs<|end_filename|>
using System;
using DShop.Common.Types;

namespace DShop.Services.Discounts.Domain
{
    public class Customer : IIdentifiable
    {
        public Guid Id { get; private set; }
        public string Email { get; private set; }

        public Customer(Guid id, string email)
        {
            Id = id;
            Email = email;
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/test/DShop.Services.Discounts.IntegrationTests/Fixtures/MongoDbFixture.cs<|end_filename|>
using System;
using System.Threading.Tasks;
using DShop.Common.Mongo;
using DShop.Common.Types;
using DShop.Services.Discounts.Domain;
using MongoDB.Driver;
using Xunit;

namespace DShop.Services.Discounts.IntegrationTests.Fixtures
{
    public class MongoDbFixture : IDisposable
    {
        private readonly IMongoClient _client;
        private readonly IMongoDatabase _database;
        private readonly IMongoCollection<Discount> _collection;
        private const string CollectionName = "Discounts";
        private const string DatabaseName = "discounts-service";
        bool _disposed = false;
        
        public MongoDbFixture()
        {
            _client = new MongoClient("mongodb://localhost:27017");
            _database = _client.GetDatabase(DatabaseName);
            InitializeMongo();
            _collection = _database.GetCollection<Discount>(CollectionName);
        }
        
        public void InitializeMongo()
            => new MongoDbInitializer(_database, null, new MongoDbOptions())
                .InitializeAsync().GetAwaiter().GetResult();

        public Task InsertAsync<TEntity>(string collectionName, TEntity entity) where TEntity : IIdentifiable
            => _database.GetCollection<TEntity>(collectionName).InsertOneAsync(entity);
        
        public async Task GetMongoEntity(Guid expectedId, TaskCompletionSource<Discount> receivedTask)
        {
            if (expectedId == null)
            {
                throw new ArgumentNullException(nameof(expectedId));
            }

            var entity = await _collection.Find(d => d.Id == expectedId).SingleOrDefaultAsync();
            
            if (entity is null)
            {
                receivedTask.TrySetCanceled();
                return;
            }
            
            receivedTask.TrySetResult(entity);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
            
            if (disposing)
            {
                _client.DropDatabase(DatabaseName);
            }

            _disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Repositories/CustomersRepository.cs<|end_filename|>
using System;
using System.Threading.Tasks;
using DShop.Common.Mongo;
using DShop.Services.Discounts.Domain;

namespace DShop.Services.Discounts.Repositories
{
    public class CustomersRepository : ICustomersRepository
    {
        private readonly IMongoRepository<Customer> _repository;

        public CustomersRepository(IMongoRepository<Customer> repository)
        {
            _repository = repository;
        }

        public Task<Customer> GetAsync(Guid id)
            => _repository.GetAsync(id);

        public Task AddAsync(Customer customer)
            => _repository.AddAsync(customer);
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Metrics/IMetricsRegistry.cs<|end_filename|>
namespace DShop.Services.Discounts.Metrics
{
    public interface IMetricsRegistry
    {
        void IncrementFindDiscountsQuery();
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/src/DShop.Services.Discounts/Messages/Events/OrderCompleted.cs<|end_filename|>
using System;
using DShop.Common.Messages;
using Newtonsoft.Json;

namespace DShop.Services.Discounts.Messages.Events
{
    [MessageNamespace("orders")]
    public class OrderCompleted : IEvent
    {
        public Guid Id { get; }
        public Guid CustomerId { get; }

        [JsonConstructor]
        public OrderCompleted(Guid id, Guid customerId)
        {
            Id = id;
            CustomerId = customerId;
        }
    }
}

<|start_filename|>DNC-DShop.Services.Discounts/Dockerfile<|end_filename|>
FROM mcr.microsoft.com/dotnet/core/sdk AS build
WORKDIR /app
COPY . .
RUN dotnet publish src/DShop.Services.Discounts -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2
WORKDIR /app
COPY --from=build /app/src/DShop.Services.Discounts/out .
ENV ASPNETCORE_URLS http://*:5000
ENV ASPNETCORE_ENVIRONMENT docker
ENTRYPOINT dotnet DShop.Services.Discounts.dll