Skip to content

Commit

Permalink
Refactor checkout (#1055)
Browse files Browse the repository at this point in the history
Simpler shopping cart.
Completely resolve the issue about adding product to shopping cart while checkout in progress
  • Loading branch information
thiennn committed Sep 30, 2023
1 parent 153dd57 commit a6bf271
Show file tree
Hide file tree
Showing 103 changed files with 6,005 additions and 31,817 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: csharp
solution: SimplCommerce.sln
sudo: required
dist: xenial
dotnet: 6.0.100
dotnet: 7.0.111
mono: none
os:
- linux
Expand Down
19 changes: 17 additions & 2 deletions SimplCommerce.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29020.237
# Visual Studio Version 17
VisualStudioVersion = 17.7.34024.191
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C9BFDDC4-5671-47A3-B57D-197C2A51FA8A}"
EndProject
Expand Down Expand Up @@ -134,6 +134,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.Paymen
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.PaymentCashfree", "src\Modules\SimplCommerce.Module.PaymentCashfree\SimplCommerce.Module.PaymentCashfree.csproj", "{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.Checkouts", "src\Modules\SimplCommerce.Module.Checkouts\SimplCommerce.Module.Checkouts.csproj", "{4473538D-2BFA-4C53-B642-0D0DC4F16863}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -696,6 +698,18 @@ Global
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x64.Build.0 = Release|Any CPU
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x86.ActiveCfg = Release|Any CPU
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x86.Build.0 = Release|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|x64.ActiveCfg = Debug|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|x64.Build.0 = Debug|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|x86.ActiveCfg = Debug|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|x86.Build.0 = Debug|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|Any CPU.Build.0 = Release|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|x64.ActiveCfg = Release|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|x64.Build.0 = Release|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|x86.ActiveCfg = Release|Any CPU
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -750,6 +764,7 @@ Global
{1A8B6FA0-8341-4D27-9B71-57F70AB37571} = {0A27C140-4CCB-40DD-BE48-F5DE16D1177B}
{14586564-62CC-4117-AC1B-858ED53C2D6C} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
{4473538D-2BFA-4C53-B642-0D0DC4F16863} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B9D0D8F0-1AB9-44DD-839F-ED8CEE7DDB10}
Expand Down
16 changes: 8 additions & 8 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ trigger:
jobs:
- job: Linux
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '6.0.100'
version: '7.0.111'
- script: dotnet build ./SimplCommerce.sln
displayName: 'dotnet build'
- task: DotNetCoreCLI@2
Expand All @@ -41,13 +41,13 @@ jobs:

- job: macOS
pool:
vmImage: 'macOS-10.15'
vmImage: 'macOS-latest'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '6.0.100'
version: '7.0.111'
- script: dotnet build ./SimplCommerce.sln
displayName: 'dotnet build'
- task: DotNetCoreCLI@2
Expand All @@ -73,13 +73,13 @@ jobs:

- job: Windows
pool:
vmImage: 'windows-2019'
vmImage: 'windows-latest'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '6.0.100'
version: '7.0.111'
- task: DotNetCoreCLI@2
displayName: 'Restoring code using dotnet restore'
inputs:
Expand Down Expand Up @@ -111,15 +111,15 @@ jobs:

- job: LinuxRelease
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '6.0.100'
version: '7.0.111'
- script: dotnet build --configuration $(buildConfiguration)
displayName: 'dotnet build $(buildConfiguration)'
- task: DotNetCoreCLI@2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,100 +4,144 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using SimplCommerce.Infrastructure.Data;
using SimplCommerce.Module.Checkouts.Areas.Checkouts.ViewModels;
using SimplCommerce.Module.Checkouts.Models;
using SimplCommerce.Module.Checkouts.Services;
using SimplCommerce.Module.Core.Extensions;
using SimplCommerce.Module.Core.Models;
using SimplCommerce.Module.Orders.Areas.Orders.ViewModels;
using SimplCommerce.Module.Orders.Services;
using SimplCommerce.Module.ShippingPrices.Services;
using SimplCommerce.Module.ShoppingCart.Models;
using SimplCommerce.Module.ShoppingCart.Services;

namespace SimplCommerce.Module.Orders.Areas.Orders.Controllers
namespace SimplCommerce.Module.Checkouts.Areas.Checkouts.Controllers
{
[Area("Orders")]
[Area("Checkouts")]
[Route("checkout")]
[Authorize]
[ApiExplorerSettings(IgnoreApi = true)]
public class CheckoutController : Controller
{
private readonly IOrderService _orderService;
private readonly IRepositoryWithTypedId<Country, string> _countryRepository;
private readonly IRepository<StateOrProvince> _stateOrProvinceRepository;
private readonly IRepository<UserAddress> _userAddressRepository;
private readonly IShippingPriceService _shippingPriceService;
private readonly ICartService _cartService;
private readonly ICheckoutService _checkoutService;
private readonly IRepository<CartItem> _cartItemRepository;
private readonly IWorkContext _workContext;
private readonly IRepository<Cart> _cartRepository;
private readonly IRepositoryWithTypedId<Checkout, Guid> _checkoutRepository;

public CheckoutController(
IRepository<StateOrProvince> stateOrProvinceRepository,
IRepositoryWithTypedId<Country, string> countryRepository,
IRepository<UserAddress> userAddressRepository,
IShippingPriceService shippingPriceService,
IOrderService orderService,
ICartService cartService,
ICheckoutService checkout,
IRepository<CartItem> cartItemRepository,
IWorkContext workContext,
IRepository<Cart> cartRepository)
IRepositoryWithTypedId<Checkout, Guid> checkoutRepository)
{
_stateOrProvinceRepository = stateOrProvinceRepository;
_countryRepository = countryRepository;
_userAddressRepository = userAddressRepository;
_shippingPriceService = shippingPriceService;
_orderService = orderService;
_cartService = cartService;
_checkoutService = checkout;
_cartItemRepository = cartItemRepository;
_workContext = workContext;
_cartRepository = cartRepository;
_checkoutRepository = checkoutRepository;
}

[HttpGet("shipping")]
public async Task<IActionResult> Shipping()
//TODO: Consider to allow customer select a subset of products in cart and pass to this endpoint
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Create(CheckoutFormVm checkoutFormVm)
{
if (!HttpContext.User.Identity.IsAuthenticated)
{
return Redirect("/login?ReturnUrl=%2Fcart");
}
var currentUser = await _workContext.GetCurrentUser();
var cartItems = await _cartItemRepository.Query().Where(x => x.CustomerId == currentUser.Id).ToListAsync();
if (!cartItems.Any())
{
return NotFound();
}

var cartItemToCheckouts = cartItems.Select(x => new CartItemToCheckoutVm
{
ProductId = x.ProductId,
Quantity = x.Quantity
}).ToList();

var checkout = await _checkoutService.Create(currentUser.Id, currentUser.Id, cartItemToCheckouts, checkoutFormVm.CouponCode);

return Redirect($"~/checkout/{checkout.Id}/shipping");
}

[HttpGet("{checkoutId}/shipping")]
public async Task<IActionResult> Shipping(Guid checkoutId)
{
var currentUser = await _workContext.GetCurrentUser();
var cart = await _cartService.GetActiveCartDetails(currentUser.Id);
if(cart == null || !cart.Items.Any())
var checkout = await _checkoutRepository.Query().FirstOrDefaultAsync(x => x.Id == checkoutId);
if (checkout == null)
{
return Redirect("~/");
return NotFound();
}

if (checkout.CreatedBy != currentUser)
{
return Forbid();
}

var model = new DeliveryInformationVm();
model.CheckoutId = checkoutId;

PopulateShippingForm(model, currentUser);

return View(model);
}

[HttpPost("shipping")]
public async Task<IActionResult> Shipping(DeliveryInformationVm model)
[HttpPost("{checkoutId}/shipping")]
public async Task<IActionResult> Shipping(Guid checkoutId, DeliveryInformationVm model)
{
var currentUser = await _workContext.GetCurrentUser();
// TODO Handle error messages
var checkout = await _checkoutRepository.Query().FirstOrDefaultAsync(x => x.Id == checkoutId);
if (checkout == null)
{
return NotFound();
}

if (checkout.CreatedBy != currentUser)
{
return Forbid();
}

if ((!model.NewAddressForm.IsValid() && model.ShippingAddressId == 0) ||
(!model.NewBillingAddressForm.IsValid() && !model.UseShippingAddressAsBillingAddress && model.BillingAddressId == 0))
{
PopulateShippingForm(model, currentUser);
return View(model);
}

var cart = await _cartService.GetActiveCart(currentUser.Id);
checkout.ShippingData = JsonConvert.SerializeObject(model);
await _checkoutRepository.SaveChangesAsync();
return Redirect($"~/checkout/{checkoutId}/payment");
}

if (cart == null)
[HttpPost("{checkoutId}/update-tax-and-shipping-prices")]
public async Task<IActionResult> UpdateTaxAndShippingPrices(Guid checkoutId, [FromBody] TaxAndShippingPriceRequestVm model)
{
var currentUser = await _workContext.GetCurrentUser();
var checkout = await _checkoutRepository.Query().FirstOrDefaultAsync(x => x.Id == checkoutId);
if(checkout == null)
{
throw new ApplicationException($"Cart of user {currentUser.Id} cannot be found");
return NotFound();
}

cart.ShippingData = JsonConvert.SerializeObject(model);
await _cartRepository.SaveChangesAsync();
return Redirect("~/checkout/payment");
}
if (checkout.CreatedBy != currentUser)
{
return Forbid();
}

[HttpPost("update-tax-and-shipping-prices")]
public async Task<IActionResult> UpdateTaxAndShippingPrices([FromBody] TaxAndShippingPriceRequestVm model)
{
var currentUser = await _workContext.GetCurrentUser();
var cart = await _cartService.GetActiveCart(currentUser.Id);
var orderTaxAndShippingPrice = await _orderService.UpdateTaxAndShippingPrices(cart.Id, model);
var orderTaxAndShippingPrice = await _checkoutService.UpdateTaxAndShippingPrices(checkoutId, model);

return Ok(orderTaxAndShippingPrice);
}
Expand All @@ -114,20 +158,6 @@ public IActionResult Error(long orderId)
return View(orderId);
}

[HttpPost("cancel")]
public async Task<IActionResult> Cancel()
{
var currentUser = await _workContext.GetCurrentUser();
var cart = await _cartService.GetActiveCart(currentUser.Id);
if(cart != null && cart.LockedOnCheckout)
{
cart.LockedOnCheckout = false;
await _cartRepository.SaveChangesAsync();
}

return Redirect("~/");
}

private void PopulateShippingForm(DeliveryInformationVm model, User currentUser)
{
model.ExistingShippingAddresses = _userAddressRepository
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;
using SimplCommerce.Infrastructure.Models;

namespace SimplCommerce.Module.Checkouts.Areas.Checkouts.ViewModels
{
public class AddressFormVm : ValidatableObject
{
[Required(ErrorMessage = "The {0} field is required.")]
public string ContactName { get; set; }

[Required(ErrorMessage = "The {0} field is required.")]
public string Phone { get; set; }

[Required(ErrorMessage = "The {0} field is required.")]
public string AddressLine1 { get; set; }

public string AddressLine2 { get; set; }

public long StateOrProvinceId { get; set; }

public long? DistrictId { get; set; }

public string CountryId { get; set; }

public string City { get; set; }

public string ZipCode { get; set; }

public IList<SelectListItem> StateOrProvinces { get; set; }

public IList<SelectListItem> Districts { get; set; }

public IList<SelectListItem> ShipableContries { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SimplCommerce.Module.Checkouts.Areas.Checkouts.ViewModels
{
public class CartItemToCheckoutVm
{
public long ProductId { get; set; }

public decimal ProductPrice { get; set; }

public int Quantity { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SimplCommerce.Module.Checkouts.Areas.Checkouts.ViewModels
{
public class CheckoutFormVm
{
public string CouponCode { get; set; }
}
}

0 comments on commit a6bf271

Please sign in to comment.