Ejemplo n.º 1
0
        public async void GameListWithOrders_ContainsOrders_CorrectQuantity()
        {
            Guid productId = new Guid("9AEE5E2E-378D-4828-B142-F69B81C53D8C");

            GameProduct gameProduct = new PhysicalGameProduct { Id = productId };

            Game game = new Game  { GameSKUs = new List<GameProduct> { gameProduct } };

            WebOrder webOrder = new WebOrder
            {
                OrderItems = new List<OrderItem>
                {
                    new OrderItem
                    {
                        Product = gameProduct,
                        ProductId = gameProduct.Id,
                        Quantity = 2
                    }
                }
            };

            Mock<IVeilDataAccess> dbStub = TestHelpers.GetVeilDataAccessFake();

            Mock<DbSet<Game>> dbGameStub = TestHelpers.GetFakeAsyncDbSet(new List<Game> {game}.AsQueryable());
            dbStub.Setup(db => db.Games).Returns(dbGameStub.Object);

            Mock<DbSet<WebOrder>> dbWebOrdersStub = TestHelpers.GetFakeAsyncDbSet(new List<WebOrder> {webOrder}.AsQueryable());
            dbStub.Setup(db => db.WebOrders).Returns(dbWebOrdersStub.Object);

            ReportsController controller = new ReportsController(dbStub.Object);

            var result = await controller.GameList() as ViewResult;

            Assert.That(result != null);
            Assert.That(result.Model, Is.InstanceOf<DateFilteredListViewModel<GameListRowViewModel>>());

            var model = (DateFilteredListViewModel<GameListRowViewModel>)result.Model;
            var items = model.Items;

            Assert.That(items.Count, Is.EqualTo(1));

            var item = items[0];

            Assert.That(item.QuantitySold, Is.EqualTo(2));
        }
Ejemplo n.º 2
0
        /// <summary>
        ///     Decreases inventory levels and adds all the cart items to <see cref="newOrder"/>
        /// </summary>
        /// <param name="cart">
        ///     The <see cref="Cart"/> to retrieve items from
        /// </param>
        /// <param name="newOrder">
        ///     The <see cref="WebOrder"/> to add items to
        /// </param>
        /// <returns>
        ///     A <see cref="Task"/> to await
        /// </returns>
        private async Task DecreaseInventoryAndAddToOrder(Cart cart, WebOrder newOrder)
        {
            foreach (var productGrouping in cart.Items.GroupBy(i => i.ProductId))
            {
                ProductLocationInventory inventory = await db.ProductLocationInventories.
                    Where(
                        pli => pli.ProductId == productGrouping.Key &&
                            pli.Location.SiteName == Location.ONLINE_WAREHOUSE_NAME).
                    FirstOrDefaultAsync();

                foreach (var lineItem in productGrouping)
                {
                    if (lineItem.IsNew)
                    {
                        AvailabilityStatus itemStatus = lineItem.Product.ProductAvailabilityStatus;

                        if (itemStatus == AvailabilityStatus.Available ||
                            itemStatus == AvailabilityStatus.PreOrder)
                        {
                            inventory.NewOnHand -= lineItem.Quantity;
                        }
                        else if ((itemStatus == AvailabilityStatus.DiscontinuedByManufacturer ||
                            itemStatus == AvailabilityStatus.NotForSale) &&
                            lineItem.Quantity <= inventory.NewOnHand)
                        {
                            inventory.NewOnHand -= lineItem.Quantity;
                        }
                        else
                        {
                            throw new NotEnoughInventoryException(
                                $"Not enough copies of {lineItem.Product.Name}, which has been discontinued, to " +
                                    "guarantee we will be able to fulfill your order.",
                                lineItem.Product);
                        }
                    }
                    else
                    {
                        if (inventory.UsedOnHand < lineItem.Quantity)
                        {
                            throw new NotEnoughInventoryException(
                                $"Not enough used copies of {lineItem.Product.Name} to guarantee we " +
                                    "will be able to fulfill your order.",
                                lineItem.Product);
                        }

                        inventory.UsedOnHand -= lineItem.Quantity;
                    }

                    newOrder.OrderItems.Add(
                        new OrderItem
                        {
                            IsNew = lineItem.IsNew,
                            ListPrice = lineItem.IsNew ? lineItem.Product.NewWebPrice : lineItem.Product.UsedWebPrice.Value,
                            Product = lineItem.Product,
                            ProductId = lineItem.ProductId,
                            Quantity = lineItem.Quantity
                        });
                }
            }
        }
Ejemplo n.º 3
0
        public async Task<ActionResult> PlaceOrder(List<CartItem> items)
        {
            // Steps:
            // Confirm session is in a valid state to place the order
            // Confirm the cart isn't empty
            // Confirm the cart matches the one the user placed an order for
            // Get the shipping info
            // Get the last 4 digits of the card for the order record
            // Get the stripe card token to be charged
            // Calculate the order total including taxes and shipping
            // Create a new order with the address information, last 4 digits, charge token, memberId, order date, and order status
            // Decrease inventory levels and add the item to the web order
            // Charge the stripe token
            // Clear the cart
            // Saves changes
            // If any exceptions occur, refund the charge
            // If no exceptions occur, clear out the session item for the order and send a order confirmation email

            var orderCheckoutDetails = Session[OrderCheckoutDetailsKey] as WebOrderCheckoutDetails;

            RedirectToRouteResult invalidSessionResult = 
                EnsureValidSessionForConfirmStep(orderCheckoutDetails);

            if (invalidSessionResult != null)
            {
                return invalidSessionResult;
            }

            Contract.Assume(orderCheckoutDetails != null);

            Guid memberId = GetUserId();
            Cart cart = await GetCartWithLoadedProductsAsync(memberId);

            if (cart == null || cart.Items.Count == 0)
            {
                this.AddAlert(AlertType.Error, "You can't place an order with an empty cart.");

                return RedirectToAction("Index", "Cart");
            }

            if (!EnsureCartMatchesConfirmedCart(items, memberId, cart))
            {
                this.AddAlert(AlertType.Warning,
                    "Your cart changed between confirming it and placing the order.");

                return RedirectToAction("ConfirmOrder");
            }

            /* Setup the address information */
            MemberAddress memberAddress = await GetShippingAddress(orderCheckoutDetails);

            if (memberAddress == null)
            {
                return RedirectToAction("ShippingInfo");
            }

            /* Setup the credit card information */
            string last4Digits = await GetLast4DigitsAsync(orderCheckoutDetails, memberId);

            if (last4Digits == null)
            {
                return RedirectToAction("BillingInfo");
            }

            bool usingExistingCreditCard = orderCheckoutDetails.MemberCreditCardId != null;

            string memberStripeCustomerId = null;
            if (usingExistingCreditCard)
            {
                memberStripeCustomerId = await db.Members.
                    Where(m => m.UserId == memberId).
                    Select(m => m.StripeCustomerId).
                    SingleOrDefaultAsync();
            }

            string stripeCardToken = await GetStripeCardToken(orderCheckoutDetails, memberId);

            decimal cartTotal = Round(cart.TotalCartItemsPrice, 2);
            decimal shippingCost = shippingCostService.CalculateShippingCost(cartTotal, cart.Items);
            decimal taxAmount = Round(
                cartTotal *
                    (memberAddress.Province.ProvincialTaxRate + memberAddress.Country.FederalTaxRate),
                2);

            decimal orderTotal = cart.TotalCartItemsPrice + taxAmount + shippingCost;

            var order = new WebOrder
            {
                OrderItems = new List<OrderItem>(),
                Address = memberAddress.Address,
                ProvinceCode = memberAddress.ProvinceCode,
                CountryCode = memberAddress.CountryCode,
                MemberId = memberId,
                CreditCardLast4Digits = last4Digits,
                OrderDate = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time")),
                OrderStatus = OrderStatus.PendingProcessing,
                TaxAmount = taxAmount,
                ShippingCost = shippingCost,
                OrderSubtotal = cartTotal
            };

            using (var newOrderScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                try
                {
                    await DecreaseInventoryAndAddToOrder(cart, order);
                }
                catch (NotEnoughInventoryException ex)
                {
                    this.AddAlert(AlertType.Error, ex.Message);

                    return RedirectToAction("ConfirmOrder");
                }

                string stripeChargeId;

                try
                {
                    stripeChargeId = stripeService.ChargeCard(
                        orderTotal, stripeCardToken, memberStripeCustomerId);

                    order.StripeChargeId = stripeChargeId;
                }
                catch (StripeServiceException ex)
                {
                    if (ex.ExceptionType == StripeExceptionType.CardError)
                    {
                        ModelState.AddModelError(
                            ManageController.STRIPE_ISSUES_MODELSTATE_KEY,
                            ex.Message);
                    }

                    this.AddAlert(AlertType.Error, ex.Message);

                    return RedirectToAction("ConfirmOrder");
                }

                db.WebOrders.Add(order);

                // This only clears out the cart as we have it. 
                // Anything added during this method's execution will remain in the cart.
                // I consider this to be the desired outcome.
                cart.Items.Clear();

                try
                {
                    await db.SaveChangesAsync();

                    newOrderScope.Complete();
                }
                catch (DataException ex)
                {
                    try
                    {
                        stripeService.RefundCharge(stripeChargeId);
                    }
                    catch (StripeServiceException stripeEx)
                    {
                        this.AddAlert(AlertType.Error, stripeEx.Message);
                    }
                    

                    this.AddAlert(
                        AlertType.Error,
                        "An error occured while placing your order. Please try again.");

                    return RedirectToAction("ConfirmOrder");
                }
            }

            Session.Remove(OrderCheckoutDetailsKey);
            Session[CartController.CART_QTY_SESSION_KEY] = null;

            string orderDetailLink = HtmlHelper.GenerateLink(
                ControllerContext.RequestContext,
                RouteTable.Routes,
                "View Order",
                null,
                "Details",
                "WebOrders",
                new RouteValueDictionary(new { id = order.Id }),
                null);

            this.AddAlert(AlertType.Success,
                $"Successfully placed order #{order.Id} for {orderTotal:C}. ", orderDetailLink);

            await SendConfirmationEmailAsync(order, memberId);

            return RedirectToAction("Index", "Home");
        }
Ejemplo n.º 4
0
        /// <summary>
        ///     Sends an order confirmation email to the user
        /// </summary>
        /// <param name="order">
        ///     The order to send a confirmation email for
        /// </param>
        /// <param name="memberId">
        ///     The id of the member to send the email to
        /// </param>
        /// <returns>
        ///     A task to await
        /// </returns>
        private async Task SendConfirmationEmailAsync(WebOrder order, Guid memberId)
        {
            string subject = $"Veil Order Confirmation - # {order.Id}";
            string body = RenderRazorPartialViewToString("_OrderConfirmationEmail", order);

            await userManager.SendEmailAsync(memberId, subject, body);
        }
Ejemplo n.º 5
0
        public async void GameDetailWithOrders_CorrectQuantitiesAndSales()
        {
            Guid gameId = new Guid("40D655DF-FB62-4FD5-8065-A81C9868B145");
            Guid productId = new Guid("9AEE5E2E-378D-4828-B142-F69B81C53D8C");

            int newQty = 1;
            decimal newPrice = 9.99m;
            int usedQty = 2;
            decimal usedPrice = 0.99m;

            GameProduct gameProduct = new PhysicalGameProduct
            {
                Id = productId,
                GameId = gameId
            };

            Game game = new Game { GameSKUs = new List<GameProduct> { gameProduct } };

            WebOrder webOrder = new WebOrder
            {
                OrderItems = new List<OrderItem>
                {
                    new OrderItem
                    {
                        Product = gameProduct,
                        ProductId = gameProduct.Id,
                        IsNew = true,
                        Quantity = newQty,
                        ListPrice = newPrice
                    },
                    new OrderItem
                    {
                        Product = gameProduct,
                        ProductId = gameProduct.Id,
                        IsNew = false,
                        Quantity = usedQty,
                        ListPrice = usedPrice
                    }
                },
                OrderStatus = OrderStatus.Processed
            };

            Mock<IVeilDataAccess> dbStub = TestHelpers.GetVeilDataAccessFake();

            Mock<DbSet<Game>> dbGameStub = TestHelpers.GetFakeAsyncDbSet(new List<Game> { game }.AsQueryable());
            dbStub.Setup(db => db.Games).Returns(dbGameStub.Object);

            Mock<DbSet<GameProduct>> dbGameProductStub = TestHelpers.GetFakeAsyncDbSet(new List<GameProduct> { gameProduct }.AsQueryable());
            dbStub.Setup(db => db.GameProducts).Returns(dbGameProductStub.Object);

            Mock<DbSet<WebOrder>> dbWebOrdersStub = TestHelpers.GetFakeAsyncDbSet(new List<WebOrder> { webOrder }.AsQueryable());
            dbStub.Setup(db => db.WebOrders).Returns(dbWebOrdersStub.Object);

            ReportsController controller = new ReportsController(dbStub.Object);

            var result = await controller.GameDetail(gameId) as ViewResult;

            Assert.That(result != null);
            Assert.That(result.Model, Is.InstanceOf<GameDetailViewModel>());

            var model = (GameDetailViewModel)result.Model;

            decimal totalNewSales = newPrice*newQty;
            decimal totalUsedSales = usedPrice*usedQty;

            Assert.That(model.TotalNewSales, Is.EqualTo(totalNewSales));
            Assert.That(model.TotalUsedSales, Is.EqualTo(totalUsedSales));
            Assert.That(model.TotalSales, Is.EqualTo(totalNewSales + totalUsedSales));

            Assert.That(model.TotalNewQuantity, Is.EqualTo(newQty));
            Assert.That(model.TotalUsedQuantity, Is.EqualTo(usedQty));
            Assert.That(model.TotalQuantity, Is.EqualTo(newQty + usedQty));
        }
Ejemplo n.º 6
0
        /// <summary>
        ///     Adds the items in a cancelled order back to the OnHand inventory levels
        /// </summary>
        /// <param name="order">
        ///     The order being cancelled
        /// </param>
        /// <returns>
        ///     A Task to await
        /// </returns>
        private async Task RestoreInventoryOnCancellation(WebOrder order)
        {
            foreach (var item in order.OrderItems)
            {
                ProductLocationInventory inventory = await db.ProductLocationInventories.
                    Where(
                        pli => pli.ProductId == item.ProductId &&
                            pli.Location.SiteName == Location.ONLINE_WAREHOUSE_NAME).
                    FirstOrDefaultAsync();

                if (item.IsNew)
                {
                    inventory.NewOnHand += item.Quantity;
                }
                else
                {
                    inventory.UsedOnHand += item.Quantity;
                }
            }
        }
Ejemplo n.º 7
0
        /// <summary>
        ///     An order is cancelled and payment is refunded
        /// </summary>
        /// <param name="order">
        ///     The order to be cancelled
        /// </param>
        /// <param name="reasonForCancellation">
        ///     The reason the order is being cancelled
        /// </param>
        /// <returns>
        ///     A Task to await
        /// </returns>
        private async Task CancelAndRefundOrder(WebOrder order, string reasonForCancellation)
        {
            using (var refundScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                order.ReasonForCancellationMessage = reasonForCancellation;
                await RestoreInventoryOnCancellation(order);
                db.MarkAsModified(order);

                try
                {
                    await db.SaveChangesAsync();
                    stripeService.RefundCharge(order.StripeChargeId);

                    this.AddAlert(AlertType.Success, "The order has been cancelled and payment refunded.");
                    refundScope.Complete();

                    string subject = $"Veil Order Cancelled - # {order.Id}";
                    string body = RenderRazorPartialViewToString("_OrderCancellationEmail", order);

                    await userManager.SendEmailAsync(order.MemberId, subject, body);
                }
                catch (Exception ex) when (ex is DataException || ex is StripeServiceException)
                {
                    string customerSupportLink = HtmlHelper.GenerateLink(
                        ControllerContext.RequestContext,
                        RouteTable.Routes,
                        "customer support.",
                        null,
                        "Contact",
                        "Home",
                        null,
                        null);

                    string errorMessage;

                    if (ex is DataException)
                    {
                        errorMessage =
                            "An error occurred cancelling the order. Payment has not been refunded. Please try again. If this issue persists please contact ";
                    }
                    else if (((StripeServiceException) ex).ExceptionType == StripeExceptionType.ApiKeyError)
                    {
                        throw new HttpException((int)HttpStatusCode.InternalServerError, ex.Message, ex);
                    }
                    else
                    {
                        errorMessage =
                            "An error occurred refunding payment. Please try again. If this issue persists please contact ";
                    }

                    this.AddAlert(AlertType.Error, errorMessage, customerSupportLink);
                }
            }
        }