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; // Address is a Value Object pattern example persisted as EF Core 2.0 owned entity 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 IReadOnlyCollection OrderItems => _orderItems; 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.Submitted.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; } public void SetAwaitingValidationStatus() { if (_orderStatusId == OrderStatus.Cancelled.Id || _orderStatusId != OrderStatus.Submitted.Id) { StatusChangeException(OrderStatus.AwaitingValidation); } AddDomainEvent(new OrderStatusChangedToAwaitingValidationDomainEvent(Id, _orderItems)); _orderStatusId = OrderStatus.AwaitingValidation.Id; } public void SetStockConfirmedStatus() { if (_orderStatusId != OrderStatus.AwaitingValidation.Id) { StatusChangeException(OrderStatus.StockConfirmed); } AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id)); _orderStatusId = OrderStatus.StockConfirmed.Id; _description = "All the items were confirmed with available stock."; } public void SetPaidStatus() { if (_orderStatusId != OrderStatus.StockConfirmed.Id) { StatusChangeException(OrderStatus.Paid); } AddDomainEvent(new OrderStatusChangedToPaidDomainEvent(Id, OrderItems)); _orderStatusId = OrderStatus.Paid.Id; _description = "The payment was performed at a simulated \"American Bank checking bank account endinf on XX35071\""; } public void SetShippedStatus() { if (_orderStatusId != OrderStatus.Paid.Id) { StatusChangeException(OrderStatus.Shipped); } _orderStatusId = OrderStatus.Shipped.Id; _description = "The order was shipped."; } public void SetCancelledStatus() { if (_orderStatusId == OrderStatus.Paid.Id || _orderStatusId == OrderStatus.Shipped.Id) { StatusChangeException(OrderStatus.Cancelled); } _orderStatusId = OrderStatus.Cancelled.Id; _description = $"The order was cancelled."; } public void SetCancelledStatusWhenStockIsRejected(IEnumerable orderStockRejectedItems) { if (_orderStatusId != OrderStatus.AwaitingValidation.Id) { StatusChangeException(OrderStatus.Cancelled); } _orderStatusId = OrderStatus.Cancelled.Id; var itemsStockRejectedProductNames = OrderItems .Where(c => orderStockRejectedItems.Contains(c.ProductId)) .Select(c => c.GetOrderItemProductName()); var itemsStockRejectedDescription = string.Join(", ", itemsStockRejectedProductNames); _description = $"The product items don't have stock: ({itemsStockRejectedDescription})."; } 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(OrderStatus orderStatusToChange) { throw new OrderingDomainException($"Is not possible to change the order status from {OrderStatus.Name} to {orderStatusToChange.Name}."); } public decimal GetTotal() { return _orderItems.Sum(o => o.GetUnits() * o.GetUnitPrice()); } } }