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)); }
/// <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 }); } } }
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"); }
/// <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); }
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)); }
/// <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; } } }
/// <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); } } }