Fixing cart storage and management

master
Mark Moffat 2019-12-30 13:40:57 +10:30
parent ba33ad2f96
commit 03573129b3
6 changed files with 182 additions and 233 deletions

View File

@ -111,15 +111,24 @@ const clearSessionValue = (session, sessionVar) => {
return temp;
};
const updateTotalCartAmount = (req, res) => {
const updateTotalCart = (req, res) => {
const config = getConfig();
req.session.totalCartAmount = 0;
req.session.cartTotalItems = 0;
_(req.session.cart).forEach((item) => {
req.session.totalCartAmount = req.session.totalCartAmount + item.totalItemPrice;
// If cart is empty return zero values
if(!req.session.cart){
return;
}
Object.keys(req.session.cart).forEach((item) => {
req.session.totalCartAmount = req.session.totalCartAmount + req.session.cart[item].totalItemPrice;
});
// Update the total items in cart for the badge
req.session.cartTotalItems = Object.keys(req.session.cart).length;
// under the free shipping threshold
if(req.session.totalCartAmount < config.freeShippingAmount){
req.session.totalCartAmount = req.session.totalCartAmount + parseInt(config.flatShipping);
@ -136,7 +145,7 @@ const updateSubscriptionCheck = (req, res) => {
return;
}
req.session.cart.forEach((item) => {
Object.keys(req.session.cart).forEach((item) => {
if(item.productSubscription){
req.session.cartSubscription = item.productSubscription;
}else{
@ -279,7 +288,7 @@ const updateConfig = (fields) => {
}
});
// delete settings
// delete any settings
delete settingsFile.customCss_input;
delete settingsFile.footerHtml_input;
delete settingsFile.googleAnalytics_input;
@ -583,7 +592,7 @@ module.exports = {
convertBool,
addSitemapProducts,
clearSessionValue,
updateTotalCartAmount,
updateTotalCart,
updateSubscriptionCheck,
checkDirectorySync,
getThemes,

View File

@ -19,6 +19,13 @@ $(document).ready(function (){
$('#offcanvasClose').hide();
}
// If cart was open before reload, open it again
var isCartOpen = (localStorage.getItem('cartOpen') === 'true');
if(isCartOpen === true){
localStorage.setItem('cartOpen', false);
$('body').addClass('pushy-open-right');
}
$('#userSetupForm').validator().on('submit', function(e){
if(!e.isDefaultPrevented()){
e.preventDefault();
@ -40,24 +47,6 @@ $(document).ready(function (){
}
});
// $('.shipping-form input').each(function(e){
// $(this).wrap('<fieldset></fieldset>');
// var tag = $(this).attr('placeholder');
// $(this).after('<label for="name" class="hidden">' + tag + '</label>');
// });
// $('.shipping-form input').on('focus', function(){
// $(this).next().addClass('floatLabel');
// $(this).next().removeClass('hidden');
// });
// $('.shipping-form input').on('blur', function(){
// if($(this).val() === ''){
// $(this).next().addClass('hidden');
// $(this).next().removeClass('floatLabel');
// }
// });
$(document).on('click', '.menu-btn', function(e){
e.preventDefault();
$('body').addClass('pushy-open-right');
@ -68,7 +57,9 @@ $(document).ready(function (){
$(this).addClass('table table-hover');
});
$('#productTags').tokenfield();
if($('#productTags').length){
$('#productTags').tokenfield();
}
$(document).on('click', '.dashboard_list', function(e){
window.document.location = $(this).attr('href');
@ -79,7 +70,6 @@ $(document).ready(function (){
$(document).on('click', '.btn-qty-minus', function(e){
e.preventDefault();
var qtyElement = $(e.target).parent().parent().find('.cart-product-quantity');
// console.log('qtyElement', qtyElement);
$(qtyElement).val(parseInt(qtyElement.val()) - 1);
cartUpdate(qtyElement);
});
@ -91,11 +81,6 @@ $(document).ready(function (){
cartUpdate(qtyElement);
});
// $(document).on('change', '.cart-product-quantity', function (e){
// console.log('test');
// cartUpdate(e.target);
// });
$(document).on('click', '.btn-delete-from-cart', function(e){
deleteFromCart($(e.target));
});
@ -172,6 +157,7 @@ $(document).ready(function (){
});
$('#checkoutInformation').validator().on('click', function(e){
console.log('here?');
e.preventDefault();
if($('#shipping-form').validator('validate').has('.has-error').length === 0){
// Change route if customer to be saved for later
@ -320,16 +306,15 @@ $(document).ready(function (){
})
.done(function(msg){
$('#cart-count').text(msg.totalCartItems);
updateCartDiv();
showNotification(msg.message, 'success');
showNotification(msg.message, 'success', true);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
});
$('.cart-product-quantity').on('input', function(){
cartUpdate();
$('.cart-product-quantity').on('focusout', function(e){
cartUpdate($(e.target));
});
$(document).on('click', '.pushy-link', function(e){
@ -352,8 +337,7 @@ $(document).ready(function (){
})
.done(function(msg){
$('#cart-count').text(msg.totalCartItems);
updateCartDiv();
showNotification(msg.message, 'success');
showNotification(msg.message, 'success', true);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
@ -368,7 +352,6 @@ $(document).ready(function (){
})
.done(function(msg){
$('#cart-count').text(msg.totalCartItems);
updateCartDiv();
showNotification(msg.message, 'success', true);
});
});
@ -420,25 +403,11 @@ function deleteFromCart(element){
$.ajax({
method: 'POST',
url: '/product/removefromcart',
data: { cartId: element.attr('data-id') }
data: { productId: element.attr('data-id') }
})
.done(function(msg){
$('#cart-count').text(msg.totalCartItems);
if(msg.totalCartItems === 0){
$(element).closest('.cart-row').hide('slow', function(){
$(element).closest('.cart-row').remove();
});
$('.cart-contents-shipping').hide('slow', function(){
$('.cart-contents-shipping').remove();
});
showNotification(msg.message, 'success');
setTimeout(function(){
window.location = '/';
}, 3700);
}else{
$(element).closest('.cart-row').hide('slow', function(){ $(element).closest('.cart-row').remove(); });
showNotification(msg.message, 'success');
}
setCartOpen();
showNotification(msg.message, 'success', true);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
@ -446,61 +415,42 @@ function deleteFromCart(element){
}
function cartUpdate(element){
console.log('element', element.val());
if($(element).val() > 0){
if($(element).val() !== ''){
updateCart();
updateCart(element);
}
}else{
$(element).val(1);
}
}
function updateCart(){
// gather items of cart
var cartItems = [];
$('.cart-product-quantity').each(function(){
cartItems.push({
productId: $(this).attr('data-id'),
quantity: $(this).val()
});
});
console.log('cartItems', cartItems)
function setCartOpen(){
if($('body').hasClass('pushy-open-right') === true){
localStorage.setItem('cartOpen', true);
}else{
localStorage.setItem('cartOpen', false);
}
}
function updateCart(element){
// update cart on server
$.ajax({
method: 'POST',
url: '/product/updatecart',
data: { items: JSON.stringify(cartItems) }
data: {
productId: element.attr('data-id'),
quantity: element.val()
}
})
.done(function(msg){
// update cart items
updateCartDiv();
$('#cart-count').text(msg.totalCartItems);
setCartOpen();
showNotification(msg.message, 'success', true);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger', true);
});
}
function updateCartDiv(){
// get new cart render
var path = window.location.pathname.split('/').length > 0 ? window.location.pathname.split('/')[1] : '';
$.ajax({
method: 'GET',
url: '/cartPartial',
data: { path: path }
})
.done(function(msg){
// update cart div
$('#cart').html(msg);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
}
function getSelectedOptions(){
var options = {};
$('.product-opt').each(function(){

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,6 @@
const express = require('express');
const router = express.Router();
const colors = require('colors');
const async = require('async');
const _ = require('lodash');
const {
getId,
@ -11,7 +10,7 @@ const {
getMenu,
getPaymentConfig,
getImages,
updateTotalCartAmount,
updateTotalCart,
updateSubscriptionCheck,
getData,
addSitemapProducts,
@ -155,6 +154,10 @@ router.get('/checkout/cart', (req, res) => {
});
});
router.get('/checkout/cartdata', (req, res) => {
res.status(200).json(req.session.cart);
});
router.get('/checkout/payment', (req, res) => {
const config = req.app.config;
@ -244,14 +247,10 @@ router.get('/cart/retrieve', async (req, res, next) => {
});
// Updates a single product quantity
router.post('/product/updatecart', (req, res, next) => {
router.post('/product/updatecart', async (req, res, next) => {
const db = req.app.db;
const config = req.app.config;
const cartItems = JSON.parse(req.body.items);
let hasError = false;
let stockError = false;
console.log('cartItems', cartItems);
const cartItem = req.body;
// Check cart exists
if(!req.session.cart){
@ -259,100 +258,84 @@ router.post('/product/updatecart', (req, res, next) => {
return;
}
console.log('req.session.cart', req.session.cart);
// Calculate the quantity to update
let productQuantity = cartItem.quantity ? cartItem.quantity : 1;
if(typeof productQuantity === 'string'){
productQuantity = parseInt(productQuantity);
}
async.eachSeries(cartItems, async (cartItem, callback) => {
// Find index in cart
const cartIndex = _.findIndex(req.session.cart, { productId: cartItem.productId });
if(productQuantity === 0){
// quantity equals zero so we remove the item
delete req.session.cart[cartItem.productId];
res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
return;
}
// Calculate the quantity to update
let productQuantity = cartItem.quantity ? cartItem.quantity : 1;
if(typeof productQuantity === 'string'){
productQuantity = parseInt(productQuantity);
}
const product = await db.products.findOne({ _id: getId(cartItem.productId) });
if(!product){
res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
return;
}
console.log('productQuantity', productQuantity);
if(productQuantity === 0){
// quantity equals zero so we remove the item
req.session.cart.splice(cartIndex, 1);
callback(null);
// If stock management on check there is sufficient stock for this product
if(config.trackStock){
if(productQuantity > product.productStock){
res.status(400).json({ message: 'There is insufficient stock of this product.', totalCartItems: Object.keys(req.session.cart).length });
return;
}
const product = await db.products.findOne({ _id: getId(cartItem.productId) });
if(product){
// If stock management on check there is sufficient stock for this product
if(config.trackStock){
if(productQuantity > product.productStock){
hasError = true;
stockError = true;
callback(null);
return;
}
}
}
const productPrice = parseFloat(product.productPrice).toFixed(2);
if(req.session.cart[cartIndex]){
req.session.cart[cartIndex].quantity = productQuantity;
req.session.cart[cartIndex].totalItemPrice = productPrice * productQuantity;
callback(null);
}
}else{
hasError = true;
callback(null);
}
}, async () => {
// update total cart amount
updateTotalCartAmount(req, res);
const productPrice = parseFloat(product.productPrice).toFixed(2);
if(!req.session.cart[cartItem.productId]){
res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
return;
}
// Update checking cart for subscription
updateSubscriptionCheck(req, res);
// Update the cart
req.session.cart[cartItem.productId].quantity = productQuantity;
req.session.cart[cartItem.productId].totalItemPrice = productPrice * productQuantity;
// Update cart to the DB
await db.cart.updateOne({ sessionId: req.session.id }, {
$set: { cart: req.session.cart }
});
// update total cart amount
updateTotalCart(req, res);
// show response
if(hasError === false){
res.status(200).json({ message: 'Cart successfully updated', totalCartItems: Object.keys(req.session.cart).length });
}else{
if(stockError){
res.status(400).json({ message: 'There is insufficient stock of this product.', totalCartItems: Object.keys(req.session.cart).length });
}else{
res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
}
}
// Update checking cart for subscription
updateSubscriptionCheck(req, res);
// Update cart to the DB
await db.cart.updateOne({ sessionId: req.session.id }, {
$set: { cart: req.session.cart }
});
res.status(200).json({ message: 'Cart successfully updated', totalCartItems: Object.keys(req.session.cart).length });
});
// Remove single product from cart
router.post('/product/removefromcart', async (req, res, next) => {
const db = req.app.db;
let itemRemoved = false;
// Check for item in cart
if(!req.session.cart[req.body.productId]){
return res.status(400).json({ message: 'Product not found in cart' });
}
// remove item from cart
req.session.cart.forEach((item) => {
if(item){
if(item.productId === req.body.cartId){
itemRemoved = true;
req.session.cart = _.pull(req.session.cart, item);
}
}
});
delete req.session.cart[req.body.productId];
// If not items in cart, empty it
if(Object.keys(req.session.cart).length === 0){
return emptyCart(req, res, 'json');
}
// Update cart in DB
await db.cart.updateOne({ sessionId: req.session.id }, {
$set: { cart: req.session.cart }
});
// update total cart amount
updateTotalCartAmount(req, res);
// update total cart
updateTotalCart(req, res);
// Update checking cart for subscription
updateSubscriptionCheck(req, res);
if(itemRemoved === false){
return res.status(400).json({ message: 'Product not found in cart' });
}
return res.status(200).json({ message: 'Product successfully removed', totalCartItems: Object.keys(req.session.cart).length });
});
@ -372,8 +355,8 @@ const emptyCart = async (req, res, type, customMessage) => {
// Remove cart from DB
await db.cart.deleteOne({ sessionId: req.session.id });
// update total cart amount
updateTotalCartAmount(req, res);
// update total cart
updateTotalCart(req, res);
// Update checking cart for subscription
updateSubscriptionCheck(req, res);
@ -409,7 +392,7 @@ router.post('/product/addtocart', async (req, res, next) => {
// setup cart object if it doesn't exist
if(!req.session.cart){
req.session.cart = [];
req.session.cart = {};
}
// Get the product from the DB
@ -425,14 +408,19 @@ router.post('/product/addtocart', async (req, res, next) => {
}
// If existing cart isn't empty check if product is a subscription
if(req.session.cart.length !== 0){
if(Object.keys(req.session.cart).length !== 0){
if(product.productSubscription){
return res.status(400).json({ message: 'You cannot combine scubscription products with existing in your cart. Empty your cart and try again.' });
return res.status(400).json({ message: 'You cannot combine subscription products with existing in your cart. Empty your cart and try again.' });
}
}
// If stock management on check there is sufficient stock for this product
if(config.trackStock && product.productStock){
// If there is more stock than total (ignoring held)
if(productQuantity > product.productStock){
return res.status(400).json({ message: 'There is insufficient stock of this product.' });
}
const stockHeld = await db.cart.aggregate(
{
$match: {
@ -478,22 +466,14 @@ router.post('/product/addtocart', async (req, res, next) => {
}
}catch(ex){}
}
const findDoc = {
productId: req.body.productId,
options: options
};
// if exists we add to the existing value
const cartIndex = _.findIndex(req.session.cart, findDoc);
let cartQuantity = 0;
if(cartIndex > -1){
cartQuantity = parseInt(req.session.cart[cartIndex].quantity) + productQuantity;
req.session.cart[cartIndex].quantity = cartQuantity;
req.session.cart[cartIndex].totalItemPrice = productPrice * parseInt(req.session.cart[cartIndex].quantity);
if(req.session.cart[product._id]){
cartQuantity = parseInt(req.session.cart[product._id].quantity) + productQuantity;
req.session.cart[product._id].quantity = cartQuantity;
req.session.cart[product._id].totalItemPrice = productPrice * parseInt(req.session.cart[product._id].quantity);
}else{
// Doesnt exist so we add to the cart session
req.session.cartTotalItems = req.session.cartTotalItems + productQuantity;
// Set the card quantity
cartQuantity = productQuantity;
@ -514,7 +494,7 @@ router.post('/product/addtocart', async (req, res, next) => {
}
// merge into the current cart
req.session.cart.push(productObj);
req.session.cart[product._id] = productObj;
}
// Update cart to the DB
@ -523,7 +503,7 @@ router.post('/product/addtocart', async (req, res, next) => {
}, { upsert: true });
// update total cart amount
updateTotalCartAmount(req, res);
updateTotalCart(req, res);
// Update checking cart for subscription
updateSubscriptionCheck(req, res);
@ -532,8 +512,6 @@ router.post('/product/addtocart', async (req, res, next) => {
req.session.cartSubscription = product.productSubscription;
}
// update how many products in the shopping cart
req.session.cartTotalItems = req.session.cart.reduce((a, b) => +a + +b.quantity, 0);
return res.status(200).json({ message: 'Cart successfully updated', totalCartItems: req.session.cartTotalItems });
});

View File

@ -75,13 +75,13 @@ test('[Success] Update cart', async t => {
.get('/cart/retrieve')
.expect(200);
// Adjust the quantity of an item
cart.body.cart[0].quantity = 10;
const productId = g.products[0]._id;
const res = await g.request
.post('/product/updatecart')
.send({
items: JSON.stringify(cart.body.cart)
productId: productId,
quantity: 10
})
.expect(200);
@ -92,8 +92,8 @@ test('[Success] Update cart', async t => {
.expect(200);
// Check new quantity and total price has been updated
t.deepEqual(checkCart.body.cart[0].quantity, 10);
t.deepEqual(checkCart.body.cart[0].totalItemPrice, cart.body.cart[0].totalItemPrice * 10);
t.deepEqual(checkCart.body.cart[productId].quantity, 10);
t.deepEqual(checkCart.body.cart[productId].totalItemPrice, cart.body.cart[productId].totalItemPrice * 10);
});
test('[Fail] Cannot add subscripton when other product in cart', async t => {
@ -105,7 +105,7 @@ test('[Fail] Cannot add subscripton when other product in cart', async t => {
productOptions: {}
})
.expect(400);
t.deepEqual(res.body.message, 'You cannot combine scubscription products with existing in your cart. Empty your cart and try again.');
t.deepEqual(res.body.message, 'You cannot combine subscription products with existing in your cart. Empty your cart and try again.');
});
test('[Fail] Add product to cart with not enough stock', async t => {
@ -132,10 +132,20 @@ test('[Fail] Add incorrect product to cart', async t => {
});
test('[Success] Remove item previously added to cart', async t => {
// Add a second product to cart
await g.request
.post('/product/addtocart')
.send({
productId: g.products[1]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[1].productOptions)
})
.expect(200);
const res = await g.request
.post('/product/removefromcart')
.send({
cartId: g.products[0]._id
productId: g.products[0]._id
})
.expect(200);
t.deepEqual(res.body.message, 'Product successfully removed');

View File

@ -2,51 +2,53 @@
<div class="card top-marg-15 bottom-marg-15">
<div class="card-body cart-body">
<h5 class="card-title">{{ @root.__ "Cart contents" }}</h5>
{{#each @root.session.cart}}
<div class="d-flex flex-row bottom-pad-15">
<div class="col-xs-4 col-md-3">
{{#if productImage}}
<img class="img-fluid" src="{{this.productImage}}" alt="{{this.title}} product image"> {{else}}
<img class="img-fluid" src="/uploads/placeholder.png" alt="{{this.title}} product image"> {{/if}}
</div>
<div class="col-sm-12 col-md-7">
<div class="row h-200">
<div class="col-sm-12 text-left no-pad-left">
<h6><a href="/product/{{this.link}}">{{this.title}}</a></h6>
</div>
<div class="col-sm-12 text-left no-pad-left">
{{#each this.options}}
{{#if @last}}
{{@key}}: {{this}}
{{else}}
{{@key}}: {{this}} /
{{/if}}
{{/each}}
</div>
{{#ifCond cartReadOnly '!=' true}}
<div class="col-md-8 no-pad-left">
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-primary btn-qty-minus" type="button">-</button>
</div>
<input type="number" class="form-control cart-product-quantity text-center" data-id="{{../this.productId}}" data-index="{{@key}}"
maxlength="2" value="{{../this.quantity}}">
<div class="input-group-append">
<button class="btn btn-outline-primary btn-qty-add" type="button">+</button>
<div id="cartBodyWrapper">
{{#each @root.session.cart}}
<div class="d-flex flex-row bottom-pad-15">
<div class="col-xs-4 col-md-3">
{{#if productImage}}
<img class="img-fluid" src="{{this.productImage}}" alt="{{this.title}} product image"> {{else}}
<img class="img-fluid" src="/uploads/placeholder.png" alt="{{this.title}} product image"> {{/if}}
</div>
<div class="col-sm-12 col-md-7">
<div class="row h-200">
<div class="col-sm-12 text-left no-pad-left">
<h6><a href="/product/{{this.link}}">{{this.title}}</a></h6>
</div>
<div class="col-sm-12 text-left no-pad-left">
{{#each this.options}}
{{#if @last}}
{{@key}}: {{this}}
{{else}}
{{@key}}: {{this}} /
{{/if}}
{{/each}}
</div>
{{#ifCond cartReadOnly '!=' true}}
<div class="col-md-8 no-pad-left">
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-primary btn-qty-minus" type="button">-</button>
</div>
<input type="number" class="form-control cart-product-quantity text-center" data-id="{{../this.productId}}" data-index="{{@key}}"
maxlength="2" value="{{../this.quantity}}">
<div class="input-group-append">
<button class="btn btn-outline-primary btn-qty-add" type="button">+</button>
</div>
</div>
</div>
<div class="col-md-4 text-right">
<button class="btn btn-outline-danger btn-delete-from-cart" data-id="{{../this.productId}}" type="button"><i class="fa fa-trash" data-id="{{../this.productId}}" aria-hidden="true"></i></button>
</div>
{{/ifCond}}
</div>
<div class="col-md-4 text-right">
<button class="btn btn-outline-danger btn-delete-from-cart" data-id="{{../this.productId}}" type="button"><i class="fa fa-trash" data-id="{{../this.productId}}" aria-hidden="true"></i></button>
</div>
{{/ifCond}}
</div>
<div class="align-self-center col-sm-12 col-md-2 text-right no-pad-right">
<strong class="my-auto">{{currencySymbol @root.config.currencySymbol}}{{formatAmount this.totalItemPrice}}</strong>
</div>
</div>
<div class="align-self-center col-sm-12 col-md-2 text-right no-pad-right">
<strong class="my-auto">{{currencySymbol @root.config.currencySymbol}}{{formatAmount this.totalItemPrice}}</strong>
</div>
{{/each}}
</div>
{{/each}}
<div class="container-fluid">
{{#if @root.session.cart}}
<div class="row">