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

View File

@ -19,6 +19,13 @@ $(document).ready(function (){
$('#offcanvasClose').hide(); $('#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){ $('#userSetupForm').validator().on('submit', function(e){
if(!e.isDefaultPrevented()){ if(!e.isDefaultPrevented()){
e.preventDefault(); 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){ $(document).on('click', '.menu-btn', function(e){
e.preventDefault(); e.preventDefault();
$('body').addClass('pushy-open-right'); $('body').addClass('pushy-open-right');
@ -68,7 +57,9 @@ $(document).ready(function (){
$(this).addClass('table table-hover'); $(this).addClass('table table-hover');
}); });
if($('#productTags').length){
$('#productTags').tokenfield(); $('#productTags').tokenfield();
}
$(document).on('click', '.dashboard_list', function(e){ $(document).on('click', '.dashboard_list', function(e){
window.document.location = $(this).attr('href'); window.document.location = $(this).attr('href');
@ -79,7 +70,6 @@ $(document).ready(function (){
$(document).on('click', '.btn-qty-minus', function(e){ $(document).on('click', '.btn-qty-minus', function(e){
e.preventDefault(); e.preventDefault();
var qtyElement = $(e.target).parent().parent().find('.cart-product-quantity'); var qtyElement = $(e.target).parent().parent().find('.cart-product-quantity');
// console.log('qtyElement', qtyElement);
$(qtyElement).val(parseInt(qtyElement.val()) - 1); $(qtyElement).val(parseInt(qtyElement.val()) - 1);
cartUpdate(qtyElement); cartUpdate(qtyElement);
}); });
@ -91,11 +81,6 @@ $(document).ready(function (){
cartUpdate(qtyElement); 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){ $(document).on('click', '.btn-delete-from-cart', function(e){
deleteFromCart($(e.target)); deleteFromCart($(e.target));
}); });
@ -172,6 +157,7 @@ $(document).ready(function (){
}); });
$('#checkoutInformation').validator().on('click', function(e){ $('#checkoutInformation').validator().on('click', function(e){
console.log('here?');
e.preventDefault(); e.preventDefault();
if($('#shipping-form').validator('validate').has('.has-error').length === 0){ if($('#shipping-form').validator('validate').has('.has-error').length === 0){
// Change route if customer to be saved for later // Change route if customer to be saved for later
@ -320,16 +306,15 @@ $(document).ready(function (){
}) })
.done(function(msg){ .done(function(msg){
$('#cart-count').text(msg.totalCartItems); $('#cart-count').text(msg.totalCartItems);
updateCartDiv(); showNotification(msg.message, 'success', true);
showNotification(msg.message, 'success');
}) })
.fail(function(msg){ .fail(function(msg){
showNotification(msg.responseJSON.message, 'danger'); showNotification(msg.responseJSON.message, 'danger');
}); });
}); });
$('.cart-product-quantity').on('input', function(){ $('.cart-product-quantity').on('focusout', function(e){
cartUpdate(); cartUpdate($(e.target));
}); });
$(document).on('click', '.pushy-link', function(e){ $(document).on('click', '.pushy-link', function(e){
@ -352,8 +337,7 @@ $(document).ready(function (){
}) })
.done(function(msg){ .done(function(msg){
$('#cart-count').text(msg.totalCartItems); $('#cart-count').text(msg.totalCartItems);
updateCartDiv(); showNotification(msg.message, 'success', true);
showNotification(msg.message, 'success');
}) })
.fail(function(msg){ .fail(function(msg){
showNotification(msg.responseJSON.message, 'danger'); showNotification(msg.responseJSON.message, 'danger');
@ -368,7 +352,6 @@ $(document).ready(function (){
}) })
.done(function(msg){ .done(function(msg){
$('#cart-count').text(msg.totalCartItems); $('#cart-count').text(msg.totalCartItems);
updateCartDiv();
showNotification(msg.message, 'success', true); showNotification(msg.message, 'success', true);
}); });
}); });
@ -420,25 +403,11 @@ function deleteFromCart(element){
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: '/product/removefromcart', url: '/product/removefromcart',
data: { cartId: element.attr('data-id') } data: { productId: element.attr('data-id') }
}) })
.done(function(msg){ .done(function(msg){
$('#cart-count').text(msg.totalCartItems); setCartOpen();
if(msg.totalCartItems === 0){ showNotification(msg.message, 'success', true);
$(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');
}
}) })
.fail(function(msg){ .fail(function(msg){
showNotification(msg.responseJSON.message, 'danger'); showNotification(msg.responseJSON.message, 'danger');
@ -446,61 +415,42 @@ function deleteFromCart(element){
} }
function cartUpdate(element){ function cartUpdate(element){
console.log('element', element.val());
if($(element).val() > 0){ if($(element).val() > 0){
if($(element).val() !== ''){ if($(element).val() !== ''){
updateCart(); updateCart(element);
} }
}else{ }else{
$(element).val(1); $(element).val(1);
} }
} }
function updateCart(){ function setCartOpen(){
// gather items of cart if($('body').hasClass('pushy-open-right') === true){
var cartItems = []; localStorage.setItem('cartOpen', true);
$('.cart-product-quantity').each(function(){ }else{
cartItems.push({ localStorage.setItem('cartOpen', false);
productId: $(this).attr('data-id'), }
quantity: $(this).val() }
});
});
console.log('cartItems', cartItems)
function updateCart(element){
// update cart on server // update cart on server
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: '/product/updatecart', url: '/product/updatecart',
data: { items: JSON.stringify(cartItems) } data: {
productId: element.attr('data-id'),
quantity: element.val()
}
}) })
.done(function(msg){ .done(function(msg){
// update cart items setCartOpen();
updateCartDiv(); showNotification(msg.message, 'success', true);
$('#cart-count').text(msg.totalCartItems);
}) })
.fail(function(msg){ .fail(function(msg){
showNotification(msg.responseJSON.message, 'danger', true); 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(){ function getSelectedOptions(){
var options = {}; var options = {};
$('.product-opt').each(function(){ $('.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 express = require('express');
const router = express.Router(); const router = express.Router();
const colors = require('colors'); const colors = require('colors');
const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const { const {
getId, getId,
@ -11,7 +10,7 @@ const {
getMenu, getMenu,
getPaymentConfig, getPaymentConfig,
getImages, getImages,
updateTotalCartAmount, updateTotalCart,
updateSubscriptionCheck, updateSubscriptionCheck,
getData, getData,
addSitemapProducts, 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) => { router.get('/checkout/payment', (req, res) => {
const config = req.app.config; const config = req.app.config;
@ -244,14 +247,10 @@ router.get('/cart/retrieve', async (req, res, next) => {
}); });
// Updates a single product quantity // 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 db = req.app.db;
const config = req.app.config; const config = req.app.config;
const cartItems = JSON.parse(req.body.items); const cartItem = req.body;
let hasError = false;
let stockError = false;
console.log('cartItems', cartItems);
// Check cart exists // Check cart exists
if(!req.session.cart){ if(!req.session.cart){
@ -259,50 +258,45 @@ router.post('/product/updatecart', (req, res, next) => {
return; return;
} }
console.log('req.session.cart', req.session.cart);
async.eachSeries(cartItems, async (cartItem, callback) => {
// Find index in cart
const cartIndex = _.findIndex(req.session.cart, { productId: cartItem.productId });
// Calculate the quantity to update // Calculate the quantity to update
let productQuantity = cartItem.quantity ? cartItem.quantity : 1; let productQuantity = cartItem.quantity ? cartItem.quantity : 1;
if(typeof productQuantity === 'string'){ if(typeof productQuantity === 'string'){
productQuantity = parseInt(productQuantity); productQuantity = parseInt(productQuantity);
} }
console.log('productQuantity', productQuantity);
if(productQuantity === 0){ if(productQuantity === 0){
// quantity equals zero so we remove the item // quantity equals zero so we remove the item
req.session.cart.splice(cartIndex, 1); delete req.session.cart[cartItem.productId];
callback(null); res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
return; return;
} }
const product = await db.products.findOne({ _id: getId(cartItem.productId) }); const product = await db.products.findOne({ _id: getId(cartItem.productId) });
if(product){ if(!product){
res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
return;
}
// If stock management on check there is sufficient stock for this product // If stock management on check there is sufficient stock for this product
if(config.trackStock){ if(config.trackStock){
if(productQuantity > product.productStock){ if(productQuantity > product.productStock){
hasError = true; res.status(400).json({ message: 'There is insufficient stock of this product.', totalCartItems: Object.keys(req.session.cart).length });
stockError = true;
callback(null);
return; return;
} }
} }
const productPrice = parseFloat(product.productPrice).toFixed(2); const productPrice = parseFloat(product.productPrice).toFixed(2);
if(req.session.cart[cartIndex]){ if(!req.session.cart[cartItem.productId]){
req.session.cart[cartIndex].quantity = productQuantity; res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
req.session.cart[cartIndex].totalItemPrice = productPrice * productQuantity; return;
callback(null);
} }
}else{
hasError = true; // Update the cart
callback(null); req.session.cart[cartItem.productId].quantity = productQuantity;
} req.session.cart[cartItem.productId].totalItemPrice = productPrice * productQuantity;
}, async () => {
// update total cart amount // update total cart amount
updateTotalCartAmount(req, res); updateTotalCart(req, res);
// Update checking cart for subscription // Update checking cart for subscription
updateSubscriptionCheck(req, res); updateSubscriptionCheck(req, res);
@ -312,47 +306,36 @@ router.post('/product/updatecart', (req, res, next) => {
$set: { cart: req.session.cart } $set: { cart: req.session.cart }
}); });
// show response
if(hasError === false){
res.status(200).json({ message: 'Cart successfully updated', totalCartItems: Object.keys(req.session.cart).length }); 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 });
}
}
});
}); });
// Remove single product from cart // Remove single product from cart
router.post('/product/removefromcart', async (req, res, next) => { router.post('/product/removefromcart', async (req, res, next) => {
const db = req.app.db; 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 // remove item from cart
req.session.cart.forEach((item) => { delete req.session.cart[req.body.productId];
if(item){
if(item.productId === req.body.cartId){ // If not items in cart, empty it
itemRemoved = true; if(Object.keys(req.session.cart).length === 0){
req.session.cart = _.pull(req.session.cart, item); return emptyCart(req, res, 'json');
} }
}
});
// Update cart in DB // Update cart in DB
await db.cart.updateOne({ sessionId: req.session.id }, { await db.cart.updateOne({ sessionId: req.session.id }, {
$set: { cart: req.session.cart } $set: { cart: req.session.cart }
}); });
// update total cart amount // update total cart
updateTotalCartAmount(req, res); updateTotalCart(req, res);
// Update checking cart for subscription // Update checking cart for subscription
updateSubscriptionCheck(req, res); 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 }); 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 // Remove cart from DB
await db.cart.deleteOne({ sessionId: req.session.id }); await db.cart.deleteOne({ sessionId: req.session.id });
// update total cart amount // update total cart
updateTotalCartAmount(req, res); updateTotalCart(req, res);
// Update checking cart for subscription // Update checking cart for subscription
updateSubscriptionCheck(req, res); updateSubscriptionCheck(req, res);
@ -409,7 +392,7 @@ router.post('/product/addtocart', async (req, res, next) => {
// setup cart object if it doesn't exist // setup cart object if it doesn't exist
if(!req.session.cart){ if(!req.session.cart){
req.session.cart = []; req.session.cart = {};
} }
// Get the product from the DB // 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 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){ 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 stock management on check there is sufficient stock for this product
if(config.trackStock && product.productStock){ 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( const stockHeld = await db.cart.aggregate(
{ {
$match: { $match: {
@ -478,22 +466,14 @@ router.post('/product/addtocart', async (req, res, next) => {
} }
}catch(ex){} }catch(ex){}
} }
const findDoc = {
productId: req.body.productId,
options: options
};
// if exists we add to the existing value // if exists we add to the existing value
const cartIndex = _.findIndex(req.session.cart, findDoc);
let cartQuantity = 0; let cartQuantity = 0;
if(cartIndex > -1){ if(req.session.cart[product._id]){
cartQuantity = parseInt(req.session.cart[cartIndex].quantity) + productQuantity; cartQuantity = parseInt(req.session.cart[product._id].quantity) + productQuantity;
req.session.cart[cartIndex].quantity = cartQuantity; req.session.cart[product._id].quantity = cartQuantity;
req.session.cart[cartIndex].totalItemPrice = productPrice * parseInt(req.session.cart[cartIndex].quantity); req.session.cart[product._id].totalItemPrice = productPrice * parseInt(req.session.cart[product._id].quantity);
}else{ }else{
// Doesnt exist so we add to the cart session
req.session.cartTotalItems = req.session.cartTotalItems + productQuantity;
// Set the card quantity // Set the card quantity
cartQuantity = productQuantity; cartQuantity = productQuantity;
@ -514,7 +494,7 @@ router.post('/product/addtocart', async (req, res, next) => {
} }
// merge into the current cart // merge into the current cart
req.session.cart.push(productObj); req.session.cart[product._id] = productObj;
} }
// Update cart to the DB // Update cart to the DB
@ -523,7 +503,7 @@ router.post('/product/addtocart', async (req, res, next) => {
}, { upsert: true }); }, { upsert: true });
// update total cart amount // update total cart amount
updateTotalCartAmount(req, res); updateTotalCart(req, res);
// Update checking cart for subscription // Update checking cart for subscription
updateSubscriptionCheck(req, res); updateSubscriptionCheck(req, res);
@ -532,8 +512,6 @@ router.post('/product/addtocart', async (req, res, next) => {
req.session.cartSubscription = product.productSubscription; 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 }); 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') .get('/cart/retrieve')
.expect(200); .expect(200);
// Adjust the quantity of an item const productId = g.products[0]._id;
cart.body.cart[0].quantity = 10;
const res = await g.request const res = await g.request
.post('/product/updatecart') .post('/product/updatecart')
.send({ .send({
items: JSON.stringify(cart.body.cart) productId: productId,
quantity: 10
}) })
.expect(200); .expect(200);
@ -92,8 +92,8 @@ test('[Success] Update cart', async t => {
.expect(200); .expect(200);
// Check new quantity and total price has been updated // Check new quantity and total price has been updated
t.deepEqual(checkCart.body.cart[0].quantity, 10); t.deepEqual(checkCart.body.cart[productId].quantity, 10);
t.deepEqual(checkCart.body.cart[0].totalItemPrice, cart.body.cart[0].totalItemPrice * 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 => { 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: {} productOptions: {}
}) })
.expect(400); .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 => { 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 => { 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 const res = await g.request
.post('/product/removefromcart') .post('/product/removefromcart')
.send({ .send({
cartId: g.products[0]._id productId: g.products[0]._id
}) })
.expect(200); .expect(200);
t.deepEqual(res.body.message, 'Product successfully removed'); t.deepEqual(res.body.message, 'Product successfully removed');

View File

@ -2,6 +2,7 @@
<div class="card top-marg-15 bottom-marg-15"> <div class="card top-marg-15 bottom-marg-15">
<div class="card-body cart-body"> <div class="card-body cart-body">
<h5 class="card-title">{{ @root.__ "Cart contents" }}</h5> <h5 class="card-title">{{ @root.__ "Cart contents" }}</h5>
<div id="cartBodyWrapper">
{{#each @root.session.cart}} {{#each @root.session.cart}}
<div class="d-flex flex-row bottom-pad-15"> <div class="d-flex flex-row bottom-pad-15">
<div class="col-xs-4 col-md-3"> <div class="col-xs-4 col-md-3">
@ -47,6 +48,7 @@
</div> </div>
</div> </div>
{{/each}} {{/each}}
</div>
<div class="container-fluid"> <div class="container-fluid">
{{#if @root.session.cart}} {{#if @root.session.cart}}
<div class="row"> <div class="row">