using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; using Ordering.Domain.Events; using Ordering.Domain.Exceptions; using System; using System.Collections.Generic; using System.Linq; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate { public class Order : Entity, IAggregateRoot { // DDD Patterns comment // Using private fields, allowed since EF Core 1.1, is a much better encapsulation // aligned with DDD Aggregates and Domain Entities (Instead of properties and property collections) private DateTime _orderDate; public Address Address { get; private set; } private int? _buyerId; public OrderStatus OrderStatus { get; private set; } private int _orderStatusId; private string _description; // DDD Patterns comment // Using a private collection field, better for DDD Aggregate's encapsulation // so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection, // but only through the method OrderAggrergateRoot.AddOrderItem() which includes behaviour. private readonly List _orderItems; public IEnumerable OrderItems => _orderItems.AsReadOnly(); // Using List<>.AsReadOnly() // This will create a read only wrapper around the private list so is protected against "external updates". // It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance) //https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx private int? _paymentMethodId; protected Order() { _orderItems = new List(); } public Order(string userId, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber, string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) { _orderItems = new List(); _buyerId = buyerId; _paymentMethodId = paymentMethodId; _orderStatusId = OrderStatus.Submited.Id; _orderDate = DateTime.UtcNow; Address = address; // Add the OrderStarterDomainEvent to the domain events collection // to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ] AddOrderStartedDomainEvent(userId, cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); } // DDD Patterns comment // This Order AggregateRoot's method "AddOrderitem()" should be the only way to add Items to the Order, // so any behavior (discounts, etc.) and validations are controlled by the AggregateRoot // in order to maintain consistency between the whole Aggregate. public void AddOrderItem(int productId, string productName, decimal unitPrice, decimal discount, string pictureUrl, int units = 1) { var existingOrderForProduct = _orderItems.Where(o => o.ProductId == productId) .SingleOrDefault(); if (existingOrderForProduct != null) { //if previous line exist modify it with higher discount and units.. if (discount > existingOrderForProduct.GetCurrentDiscount()) { existingOrderForProduct.SetNewDiscount(discount); existingOrderForProduct.AddUnits(units); } } else { //add validated new order item var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units); _orderItems.Add(orderItem); } } public void SetPaymentId(int id) { _paymentMethodId = id; } public void SetBuyerId(int id) { _buyerId = id; } #region Status Changes public void SetSubmitedStatus() { _orderStatusId = OrderStatus.Submited.Id; } public void SetAwaitingValidationStatus() { if (_orderStatusId != OrderStatus.Submited.Id) { StatusChangeException(); } _orderStatusId = OrderStatus.AwaitingValidation.Id; AddDomainEvent(new OrderStatusChangedToAwaitingValidationDomainEvent(Id, OrderItems)); } public void SetStockConfirmedStatus(IEnumerable orderStockNotConfirmedItems = null) { if (_orderStatusId != OrderStatus.AwaitingValidation.Id) { StatusChangeException(); } if (orderStockNotConfirmedItems is null) { OrderStatus = OrderStatus.StockConfirmed; _description = "All the items were confirmed with available stock."; AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id)); } else { OrderStatus = OrderStatus.Cancelled; var itemsStockNotConfirmedProductNames = OrderItems .Where(c => orderStockNotConfirmedItems.Contains(c.ProductId)) .Select(c => c.GetOrderItemProductName()); var itemsStockNotConfirmedDescription = string.Join(", ", itemsStockNotConfirmedProductNames); _description = $"The product items don't have stock: ({itemsStockNotConfirmedDescription})."; } } public void SetPaidStatus() { if (_orderStatusId != OrderStatus.StockConfirmed.Id) { StatusChangeException(); } _orderStatusId = OrderStatus.Paid.Id; _description = "The payment was performed at a simulated \"American Bank checking bank account endinf on XX35071\""; AddDomainEvent(new OrderStatusChangedToPaidDomainEvent(Id, OrderItems)); } public void SetShippedStatus() { if (_orderStatusId != OrderStatus.Paid.Id) { StatusChangeException(); } _orderStatusId = OrderStatus.Shipped.Id; _description = ""; //Call Domain Event } #endregion private void AddOrderStartedDomainEvent(string userId, int cardTypeId, string cardNumber, string cardSecurityNumber, string cardHolderName, DateTime cardExpiration) { var orderStartedDomainEvent = new OrderStartedDomainEvent( this, userId, cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); this.AddDomainEvent(orderStartedDomainEvent); } private void StatusChangeException() { throw new OrderingDomainException("Not able to process order event. Reason: no valid order status change"); } } }