Skip to content

Commit a6bf271

Browse files
authored
Refactor checkout (#1055)
Simpler shopping cart. Completely resolve the issue about adding product to shopping cart while checkout in progress
1 parent 153dd57 commit a6bf271

103 files changed

Lines changed: 6005 additions & 31817 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ language: csharp
22
solution: SimplCommerce.sln
33
sudo: required
44
dist: xenial
5-
dotnet: 6.0.100
5+
dotnet: 7.0.111
66
mono: none
77
os:
88
- linux

SimplCommerce.sln

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 16
4-
VisualStudioVersion = 16.0.29020.237
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.7.34024.191
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C9BFDDC4-5671-47A3-B57D-197C2A51FA8A}"
77
EndProject
@@ -134,6 +134,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.Paymen
134134
EndProject
135135
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.PaymentCashfree", "src\Modules\SimplCommerce.Module.PaymentCashfree\SimplCommerce.Module.PaymentCashfree.csproj", "{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}"
136136
EndProject
137+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplCommerce.Module.Checkouts", "src\Modules\SimplCommerce.Module.Checkouts\SimplCommerce.Module.Checkouts.csproj", "{4473538D-2BFA-4C53-B642-0D0DC4F16863}"
138+
EndProject
137139
Global
138140
GlobalSection(SolutionConfigurationPlatforms) = preSolution
139141
Debug|Any CPU = Debug|Any CPU
@@ -696,6 +698,18 @@ Global
696698
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x64.Build.0 = Release|Any CPU
697699
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x86.ActiveCfg = Release|Any CPU
698700
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92}.Release|x86.Build.0 = Release|Any CPU
701+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
702+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|Any CPU.Build.0 = Debug|Any CPU
703+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|x64.ActiveCfg = Debug|Any CPU
704+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|x64.Build.0 = Debug|Any CPU
705+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|x86.ActiveCfg = Debug|Any CPU
706+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Debug|x86.Build.0 = Debug|Any CPU
707+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|Any CPU.ActiveCfg = Release|Any CPU
708+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|Any CPU.Build.0 = Release|Any CPU
709+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|x64.ActiveCfg = Release|Any CPU
710+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|x64.Build.0 = Release|Any CPU
711+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|x86.ActiveCfg = Release|Any CPU
712+
{4473538D-2BFA-4C53-B642-0D0DC4F16863}.Release|x86.Build.0 = Release|Any CPU
699713
EndGlobalSection
700714
GlobalSection(SolutionProperties) = preSolution
701715
HideSolutionNode = FALSE
@@ -750,6 +764,7 @@ Global
750764
{1A8B6FA0-8341-4D27-9B71-57F70AB37571} = {0A27C140-4CCB-40DD-BE48-F5DE16D1177B}
751765
{14586564-62CC-4117-AC1B-858ED53C2D6C} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
752766
{E30CF10F-FABF-4917-8BEB-CB81E4CE2C92} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
767+
{4473538D-2BFA-4C53-B642-0D0DC4F16863} = {7EFA2FA7-32DD-4047-B021-50E77A83D714}
753768
EndGlobalSection
754769
GlobalSection(ExtensibilityGlobals) = postSolution
755770
SolutionGuid = {B9D0D8F0-1AB9-44DD-839F-ED8CEE7DDB10}

azure-pipelines.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ trigger:
99
jobs:
1010
- job: Linux
1111
pool:
12-
vmImage: 'ubuntu-18.04'
12+
vmImage: 'ubuntu-latest'
1313
steps:
1414
- task: UseDotNet@2
1515
displayName: 'Use .NET Core sdk'
1616
inputs:
1717
packageType: 'sdk'
18-
version: '6.0.100'
18+
version: '7.0.111'
1919
- script: dotnet build ./SimplCommerce.sln
2020
displayName: 'dotnet build'
2121
- task: DotNetCoreCLI@2
@@ -41,13 +41,13 @@ jobs:
4141

4242
- job: macOS
4343
pool:
44-
vmImage: 'macOS-10.15'
44+
vmImage: 'macOS-latest'
4545
steps:
4646
- task: UseDotNet@2
4747
displayName: 'Use .NET Core sdk'
4848
inputs:
4949
packageType: 'sdk'
50-
version: '6.0.100'
50+
version: '7.0.111'
5151
- script: dotnet build ./SimplCommerce.sln
5252
displayName: 'dotnet build'
5353
- task: DotNetCoreCLI@2
@@ -73,13 +73,13 @@ jobs:
7373

7474
- job: Windows
7575
pool:
76-
vmImage: 'windows-2019'
76+
vmImage: 'windows-latest'
7777
steps:
7878
- task: UseDotNet@2
7979
displayName: 'Use .NET Core sdk'
8080
inputs:
8181
packageType: 'sdk'
82-
version: '6.0.100'
82+
version: '7.0.111'
8383
- task: DotNetCoreCLI@2
8484
displayName: 'Restoring code using dotnet restore'
8585
inputs:
@@ -111,15 +111,15 @@ jobs:
111111

112112
- job: LinuxRelease
113113
pool:
114-
vmImage: 'ubuntu-18.04'
114+
vmImage: 'ubuntu-latest'
115115
variables:
116116
buildConfiguration: 'Release'
117117
steps:
118118
- task: UseDotNet@2
119119
displayName: 'Use .NET Core sdk'
120120
inputs:
121121
packageType: 'sdk'
122-
version: '6.0.100'
122+
version: '7.0.111'
123123
- script: dotnet build --configuration $(buildConfiguration)
124124
displayName: 'dotnet build $(buildConfiguration)'
125125
- task: DotNetCoreCLI@2

src/Modules/SimplCommerce.Module.Orders/Areas/Orders/Controllers/CheckoutController.cs renamed to src/Modules/SimplCommerce.Module.Checkouts/Areas/Checkouts/Controllers/CheckoutController.cs

Lines changed: 82 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,100 +4,144 @@
44
using Microsoft.AspNetCore.Authorization;
55
using Microsoft.AspNetCore.Mvc;
66
using Microsoft.AspNetCore.Mvc.Rendering;
7+
using Microsoft.EntityFrameworkCore;
78
using Newtonsoft.Json;
89
using SimplCommerce.Infrastructure.Data;
10+
using SimplCommerce.Module.Checkouts.Areas.Checkouts.ViewModels;
11+
using SimplCommerce.Module.Checkouts.Models;
12+
using SimplCommerce.Module.Checkouts.Services;
913
using SimplCommerce.Module.Core.Extensions;
1014
using SimplCommerce.Module.Core.Models;
11-
using SimplCommerce.Module.Orders.Areas.Orders.ViewModels;
12-
using SimplCommerce.Module.Orders.Services;
13-
using SimplCommerce.Module.ShippingPrices.Services;
1415
using SimplCommerce.Module.ShoppingCart.Models;
1516
using SimplCommerce.Module.ShoppingCart.Services;
1617

17-
namespace SimplCommerce.Module.Orders.Areas.Orders.Controllers
18+
namespace SimplCommerce.Module.Checkouts.Areas.Checkouts.Controllers
1819
{
19-
[Area("Orders")]
20+
[Area("Checkouts")]
2021
[Route("checkout")]
2122
[Authorize]
2223
[ApiExplorerSettings(IgnoreApi = true)]
2324
public class CheckoutController : Controller
2425
{
25-
private readonly IOrderService _orderService;
2626
private readonly IRepositoryWithTypedId<Country, string> _countryRepository;
2727
private readonly IRepository<StateOrProvince> _stateOrProvinceRepository;
2828
private readonly IRepository<UserAddress> _userAddressRepository;
29-
private readonly IShippingPriceService _shippingPriceService;
30-
private readonly ICartService _cartService;
29+
private readonly ICheckoutService _checkoutService;
30+
private readonly IRepository<CartItem> _cartItemRepository;
3131
private readonly IWorkContext _workContext;
32-
private readonly IRepository<Cart> _cartRepository;
32+
private readonly IRepositoryWithTypedId<Checkout, Guid> _checkoutRepository;
3333

3434
public CheckoutController(
3535
IRepository<StateOrProvince> stateOrProvinceRepository,
3636
IRepositoryWithTypedId<Country, string> countryRepository,
3737
IRepository<UserAddress> userAddressRepository,
38-
IShippingPriceService shippingPriceService,
39-
IOrderService orderService,
40-
ICartService cartService,
38+
ICheckoutService checkout,
39+
IRepository<CartItem> cartItemRepository,
4140
IWorkContext workContext,
42-
IRepository<Cart> cartRepository)
41+
IRepositoryWithTypedId<Checkout, Guid> checkoutRepository)
4342
{
4443
_stateOrProvinceRepository = stateOrProvinceRepository;
4544
_countryRepository = countryRepository;
4645
_userAddressRepository = userAddressRepository;
47-
_shippingPriceService = shippingPriceService;
48-
_orderService = orderService;
49-
_cartService = cartService;
46+
_checkoutService = checkout;
47+
_cartItemRepository = cartItemRepository;
5048
_workContext = workContext;
51-
_cartRepository = cartRepository;
49+
_checkoutRepository = checkoutRepository;
5250
}
5351

54-
[HttpGet("shipping")]
55-
public async Task<IActionResult> Shipping()
52+
//TODO: Consider to allow customer select a subset of products in cart and pass to this endpoint
53+
[HttpPost]
54+
[AllowAnonymous]
55+
public async Task<IActionResult> Create(CheckoutFormVm checkoutFormVm)
56+
{
57+
if (!HttpContext.User.Identity.IsAuthenticated)
58+
{
59+
return Redirect("/login?ReturnUrl=%2Fcart");
60+
}
61+
var currentUser = await _workContext.GetCurrentUser();
62+
var cartItems = await _cartItemRepository.Query().Where(x => x.CustomerId == currentUser.Id).ToListAsync();
63+
if (!cartItems.Any())
64+
{
65+
return NotFound();
66+
}
67+
68+
var cartItemToCheckouts = cartItems.Select(x => new CartItemToCheckoutVm
69+
{
70+
ProductId = x.ProductId,
71+
Quantity = x.Quantity
72+
}).ToList();
73+
74+
var checkout = await _checkoutService.Create(currentUser.Id, currentUser.Id, cartItemToCheckouts, checkoutFormVm.CouponCode);
75+
76+
return Redirect($"~/checkout/{checkout.Id}/shipping");
77+
}
78+
79+
[HttpGet("{checkoutId}/shipping")]
80+
public async Task<IActionResult> Shipping(Guid checkoutId)
5681
{
5782
var currentUser = await _workContext.GetCurrentUser();
58-
var cart = await _cartService.GetActiveCartDetails(currentUser.Id);
59-
if(cart == null || !cart.Items.Any())
83+
var checkout = await _checkoutRepository.Query().FirstOrDefaultAsync(x => x.Id == checkoutId);
84+
if (checkout == null)
6085
{
61-
return Redirect("~/");
86+
return NotFound();
87+
}
88+
89+
if (checkout.CreatedBy != currentUser)
90+
{
91+
return Forbid();
6292
}
6393

6494
var model = new DeliveryInformationVm();
95+
model.CheckoutId = checkoutId;
6596

6697
PopulateShippingForm(model, currentUser);
6798

6899
return View(model);
69100
}
70101

71-
[HttpPost("shipping")]
72-
public async Task<IActionResult> Shipping(DeliveryInformationVm model)
102+
[HttpPost("{checkoutId}/shipping")]
103+
public async Task<IActionResult> Shipping(Guid checkoutId, DeliveryInformationVm model)
73104
{
74105
var currentUser = await _workContext.GetCurrentUser();
75-
// TODO Handle error messages
106+
var checkout = await _checkoutRepository.Query().FirstOrDefaultAsync(x => x.Id == checkoutId);
107+
if (checkout == null)
108+
{
109+
return NotFound();
110+
}
111+
112+
if (checkout.CreatedBy != currentUser)
113+
{
114+
return Forbid();
115+
}
116+
76117
if ((!model.NewAddressForm.IsValid() && model.ShippingAddressId == 0) ||
77118
(!model.NewBillingAddressForm.IsValid() && !model.UseShippingAddressAsBillingAddress && model.BillingAddressId == 0))
78119
{
79120
PopulateShippingForm(model, currentUser);
80121
return View(model);
81122
}
82123

83-
var cart = await _cartService.GetActiveCart(currentUser.Id);
124+
checkout.ShippingData = JsonConvert.SerializeObject(model);
125+
await _checkoutRepository.SaveChangesAsync();
126+
return Redirect($"~/checkout/{checkoutId}/payment");
127+
}
84128

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

90-
cart.ShippingData = JsonConvert.SerializeObject(model);
91-
await _cartRepository.SaveChangesAsync();
92-
return Redirect("~/checkout/payment");
93-
}
139+
if (checkout.CreatedBy != currentUser)
140+
{
141+
return Forbid();
142+
}
94143

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

102146
return Ok(orderTaxAndShippingPrice);
103147
}
@@ -114,20 +158,6 @@ public IActionResult Error(long orderId)
114158
return View(orderId);
115159
}
116160

117-
[HttpPost("cancel")]
118-
public async Task<IActionResult> Cancel()
119-
{
120-
var currentUser = await _workContext.GetCurrentUser();
121-
var cart = await _cartService.GetActiveCart(currentUser.Id);
122-
if(cart != null && cart.LockedOnCheckout)
123-
{
124-
cart.LockedOnCheckout = false;
125-
await _cartRepository.SaveChangesAsync();
126-
}
127-
128-
return Redirect("~/");
129-
}
130-
131161
private void PopulateShippingForm(DeliveryInformationVm model, User currentUser)
132162
{
133163
model.ExistingShippingAddresses = _userAddressRepository
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
using Microsoft.AspNetCore.Mvc.Rendering;
4+
using SimplCommerce.Infrastructure.Models;
5+
6+
namespace SimplCommerce.Module.Checkouts.Areas.Checkouts.ViewModels
7+
{
8+
public class AddressFormVm : ValidatableObject
9+
{
10+
[Required(ErrorMessage = "The {0} field is required.")]
11+
public string ContactName { get; set; }
12+
13+
[Required(ErrorMessage = "The {0} field is required.")]
14+
public string Phone { get; set; }
15+
16+
[Required(ErrorMessage = "The {0} field is required.")]
17+
public string AddressLine1 { get; set; }
18+
19+
public string AddressLine2 { get; set; }
20+
21+
public long StateOrProvinceId { get; set; }
22+
23+
public long? DistrictId { get; set; }
24+
25+
public string CountryId { get; set; }
26+
27+
public string City { get; set; }
28+
29+
public string ZipCode { get; set; }
30+
31+
public IList<SelectListItem> StateOrProvinces { get; set; }
32+
33+
public IList<SelectListItem> Districts { get; set; }
34+
35+
public IList<SelectListItem> ShipableContries { get; set; }
36+
}
37+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace SimplCommerce.Module.Checkouts.Areas.Checkouts.ViewModels
2+
{
3+
public class CartItemToCheckoutVm
4+
{
5+
public long ProductId { get; set; }
6+
7+
public decimal ProductPrice { get; set; }
8+
9+
public int Quantity { get; set; }
10+
}
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace SimplCommerce.Module.Checkouts.Areas.Checkouts.ViewModels
2+
{
3+
public class CheckoutFormVm
4+
{
5+
public string CouponCode { get; set; }
6+
}
7+
}

0 commit comments

Comments
 (0)