Fixed same product - diff opts in cart. Added confirm on cart empty.

master
Mark Moffat 2020-01-21 20:30:52 +10:30
parent 799ed301f2
commit 5d4c469e38
12 changed files with 251 additions and 185 deletions

5
package-lock.json generated
View File

@ -7244,6 +7244,11 @@
}
}
},
"object-hash": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.1.tgz",
"integrity": "sha512-HgcGMooY4JC2PBt9sdUdJ6PMzpin+YtY3r/7wg0uTifP+HJWW8rammseSEHuyt0UeShI183UGssCJqm1bJR7QA=="
},
"object-is": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz",

View File

@ -60,6 +60,7 @@
"node-cron": "^2.0.3",
"nodemailer": "^4.7.0",
"numeral": "^2.0.6",
"object-hash": "^2.0.1",
"paypal-rest-sdk": "^1.6.9",
"rand-token": "^0.4.0",
"rimraf": "^2.7.1",

View File

@ -377,15 +377,17 @@ $(document).ready(function (){
}
});
// On empty cart click
$(document).on('click', '#empty-cart', function(e){
$.ajax({
method: 'POST',
url: '/product/emptycart'
})
.done(function(msg){
showNotification(msg.message, 'success', true);
updateCartDiv();
});
$('#confirmModal').modal('show');
$('#buttonConfirm').attr('data-func', 'emptyCart');
});
$(document).on('click', '#buttonConfirm', function(e){
// Get the function and run it
var func = $(e.target).attr('data-func');
window[func]();
$('#confirmModal').modal('hide');
});
$('.qty-btn-minus').on('click', function(){
@ -449,7 +451,9 @@ function deleteFromCart(element){
$.ajax({
method: 'POST',
url: '/product/removefromcart',
data: { productId: element.attr('data-id') }
data: {
cartId: element.attr('data-cartid')
}
})
.done(function(msg){
showNotification(msg.message, 'success');
@ -476,6 +480,7 @@ function updateCart(element){
method: 'POST',
url: '/product/updatecart',
data: {
cartId: element.attr('data-cartid'),
productId: element.attr('data-id'),
quantity: element.val()
}
@ -596,14 +601,14 @@ function updateCartDiv(){
<div class="input-group-prepend">
<button class="btn btn-primary btn-qty-minus" type="button">-</button>
</div>
<input type="number" class="form-control cart-product-quantity text-center" id="${productId}-qty" data-id="${productId}" maxlength="2" value="${item.quantity}">
<input type="number" class="form-control cart-product-quantity text-center" data-cartid="${productId}" data-id="${item.id}" maxlength="2" value="${item.quantity}">
<div class="input-group-append">
<button class="btn btn-primary btn-qty-add" type="button">+</button>
</div>
</div>
</div>
<div class="col-4 col-md-2 no-pad-left">
<button class="btn btn-danger btn-delete-from-cart" data-id="${productId}" type="button"><i class="far fa-trash-alt" data-id="${productId}" aria-hidden="true"></i></button>
<button class="btn btn-danger btn-delete-from-cart" data-cartid="${productId}" type="button"><i class="far fa-trash-alt" data-cartid="${productId}" aria-hidden="true"></i></button>
</div>
<div class="col-8 col-md-4 align-self-center text-right">
<strong class="my-auto">${result.currencySymbol}${productTotalAmount}</strong>
@ -663,3 +668,15 @@ function upperFirst(value){
return chr.toUpperCase();
});
}
// eslint-disable-next-line no-unused-vars
function emptyCart(){
$.ajax({
method: 'POST',
url: '/product/emptycart'
})
.done(function(msg){
showNotification(msg.message, 'success', true);
updateCartDiv();
});
}

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
width: 500px;
height: 100%;
top: 0;
z-index: 9999;
z-index: 999;
background: #fff;
overflow: auto;
visibility: hidden;
@ -80,7 +80,7 @@
right: 0;
bottom: 0;
left: 0;
z-index: 9998;
z-index: 998;
background-color: rgba(0, 0, 0, 0.5);
-webkit-animation: fade 500ms;
animation: fade 500ms;

View File

@ -1,4 +1,4 @@
/*! Pushy - v1.0.0 - 2016-3-1
* Pushy is a responsive off-canvas navigation menu using CSS transforms & transitions.
* https://github.com/christophery/pushy/
* by Christopher Yee */.pushy{position:fixed;width:500px;height:100%;top:0;z-index:9999;background:#fff;overflow:auto;visibility:hidden;-webkit-overflow-scrolling:touch}.pushy ul:first-child{margin-top:10px}.pushy.pushy-left{left:0}.pushy.pushy-right{right:0}.pushy-left{-webkit-transform:translate3d(-500px,0,0);-ms-transform:translate3d(-500px,0,0);transform:translate3d(-500px,0,0)}.pushy-open-left #container,.pushy-open-left .push{-webkit-transform:translate3d(500px,0,0);-ms-transform:translate3d(500px,0,0);transform:translate3d(500px,0,0)}.pushy-right{-webkit-transform:translate3d(500px,0,0);-ms-transform:translate3d(500px,0,0);transform:translate3d(500px,0,0)}.pushy-open-right #container,.pushy-open-right .push{-webkit-transform:translate3d(-500px,0,0);-ms-transform:translate3d(-500px,0,0);transform:translate3d(-500px,0,0)}.pushy-open-left .pushy,.pushy-open-right .pushy{-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}#container,.push,.pushy{transition:transform .2s cubic-bezier(.16,.68,.43,.99)}.site-overlay{display:none}.pushy-open-left .site-overlay,.pushy-open-right .site-overlay{display:block;position:fixed;top:0;right:0;bottom:0;left:0;z-index:9998;background-color:rgba(0,0,0,.5);-webkit-animation:fade .5s;animation:fade .5s}@keyframes fade{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes fade{0%{opacity:0}100%{opacity:1}}.pushy-submenu ul{padding-left:15px;transition:max-height .2s ease-in-out}.pushy-submenu ul .pushy-link{transition:opacity .2s ease-in-out}.pushy-submenu>a{position:relative}.pushy-submenu>a::after{content:'';display:block;height:11px;width:8px;position:absolute;top:50%;right:15px;background:url(../img/arrow.svg) no-repeat;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);transition:transform .2s}.pushy-submenu-closed ul{max-height:0;overflow:hidden}.pushy-submenu-closed .pushy-link{opacity:0}.pushy-submenu-open ul{max-height:1000px}.pushy-submenu-open .pushy-link{opacity:1}.pushy-submenu-open a::after{-webkit-transform:translateY(-50%) rotate(90deg);-ms-transform:translateY(-50%) rotate(90deg);transform:translateY(-50%) rotate(90deg)}.no-csstransforms3d .pushy-submenu-closed ul{max-height:none;display:none}
* by Christopher Yee */.pushy{position:fixed;width:500px;height:100%;top:0;z-index:999;background:#fff;overflow:auto;visibility:hidden;-webkit-overflow-scrolling:touch}.pushy ul:first-child{margin-top:10px}.pushy.pushy-left{left:0}.pushy.pushy-right{right:0}.pushy-left{-webkit-transform:translate3d(-500px,0,0);-ms-transform:translate3d(-500px,0,0);transform:translate3d(-500px,0,0)}.pushy-open-left #container,.pushy-open-left .push{-webkit-transform:translate3d(500px,0,0);-ms-transform:translate3d(500px,0,0);transform:translate3d(500px,0,0)}.pushy-right{-webkit-transform:translate3d(500px,0,0);-ms-transform:translate3d(500px,0,0);transform:translate3d(500px,0,0)}.pushy-open-right #container,.pushy-open-right .push{-webkit-transform:translate3d(-500px,0,0);-ms-transform:translate3d(-500px,0,0);transform:translate3d(-500px,0,0)}.pushy-open-left .pushy,.pushy-open-right .pushy{-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}#container,.push,.pushy{transition:transform .2s cubic-bezier(.16,.68,.43,.99)}.site-overlay{display:none}.pushy-open-left .site-overlay,.pushy-open-right .site-overlay{display:block;position:fixed;top:0;right:0;bottom:0;left:0;z-index:998;background-color:rgba(0,0,0,.5);-webkit-animation:fade .5s;animation:fade .5s}@keyframes fade{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes fade{0%{opacity:0}100%{opacity:1}}.pushy-submenu ul{padding-left:15px;transition:max-height .2s ease-in-out}.pushy-submenu ul .pushy-link{transition:opacity .2s ease-in-out}.pushy-submenu>a{position:relative}.pushy-submenu>a::after{content:'';display:block;height:11px;width:8px;position:absolute;top:50%;right:15px;background:url(../img/arrow.svg) no-repeat;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);transition:transform .2s}.pushy-submenu-closed ul{max-height:0;overflow:hidden}.pushy-submenu-closed .pushy-link{opacity:0}.pushy-submenu-open ul{max-height:1000px}.pushy-submenu-open .pushy-link{opacity:1}.pushy-submenu-open a::after{-webkit-transform:translateY(-50%) rotate(90deg);-ms-transform:translateY(-50%) rotate(90deg);transform:translateY(-50%) rotate(90deg)}.no-csstransforms3d .pushy-submenu-closed ul{max-height:none;display:none}

View File

@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const colors = require('colors');
const hash = require('object-hash');
const moment = require('moment');
const _ = require('lodash');
const {
@ -362,12 +363,12 @@ router.post('/product/updatecart', async (req, res, next) => {
if(productQuantity === 0){
// quantity equals zero so we remove the item
delete req.session.cart[cartItem.productId];
delete req.session.cart[cartItem.cartId];
res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
return;
}
const product = await db.products.findOne({ _id: getId(cartItem.productId) });
const product = await db.products.findOne({ _id: getId(req.session.cart[cartItem.cartId].productId) });
if(!product){
res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
return;
@ -382,14 +383,14 @@ router.post('/product/updatecart', async (req, res, next) => {
}
const productPrice = parseFloat(product.productPrice).toFixed(2);
if(!req.session.cart[cartItem.productId]){
if(!req.session.cart[cartItem.cartId]){
res.status(400).json({ message: 'There was an error updating the cart', totalCartItems: Object.keys(req.session.cart).length });
return;
}
// Update the cart
req.session.cart[cartItem.productId].quantity = productQuantity;
req.session.cart[cartItem.productId].totalItemPrice = productPrice * productQuantity;
req.session.cart[cartItem.cartId].quantity = productQuantity;
req.session.cart[cartItem.cartId].totalItemPrice = productPrice * productQuantity;
// update total cart amount
await updateTotalCart(req, res);
@ -410,12 +411,12 @@ router.post('/product/removefromcart', async (req, res, next) => {
const db = req.app.db;
// Check for item in cart
if(!req.session.cart[req.body.productId]){
if(!req.session.cart[req.body.cartId]){
return res.status(400).json({ message: 'Product not found in cart' });
}
// remove item from cart
delete req.session.cart[req.body.productId];
delete req.session.cart[req.body.cartId];
// If not items in cart, empty it
if(Object.keys(req.session.cart).length === 0){
@ -535,19 +536,25 @@ router.post('/product/addtocart', async (req, res, next) => {
}catch(ex){}
}
// Product with options hash
const productHash = hash({
productId: product._id.toString(),
options
});
// if exists we add to the existing value
let cartQuantity = 0;
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);
if(req.session.cart[productHash]){
cartQuantity = parseInt(req.session.cart[productHash].quantity) + productQuantity;
req.session.cart[productHash].quantity = cartQuantity;
req.session.cart[productHash].totalItemPrice = productPrice * parseInt(req.session.cart[productHash].quantity);
}else{
// Set the card quantity
cartQuantity = productQuantity;
// new product deets
const productObj = {};
productObj.productId = req.body.productId;
productObj.productId = product._id;
productObj.title = product.productTitle;
productObj.quantity = productQuantity;
productObj.totalItemPrice = productPrice * productQuantity;
@ -562,7 +569,7 @@ router.post('/product/addtocart', async (req, res, next) => {
}
// merge into the current cart
req.session.cart[product._id] = productObj;
req.session.cart[productHash] = productObj;
}
// Update cart to the DB
@ -580,7 +587,11 @@ router.post('/product/addtocart', async (req, res, next) => {
req.session.cartSubscription = product.productSubscription;
}
return res.status(200).json({ message: 'Cart successfully updated', totalCartItems: req.session.totalCartItems });
return res.status(200).json({
message: 'Cart successfully updated',
cartId: productHash,
totalCartItems: req.session.totalCartItems
});
});
// search products

166
test/specs/cart.js Normal file
View File

@ -0,0 +1,166 @@
import{ serial as test }from'ava';
const {
runBefore,
g
} = require('../helper');
test.before(async () => {
await runBefore();
});
test('[Success] Add subscripton product to cart', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[7]._id,
productQuantity: 1,
productOptions: {}
})
.expect(200);
const sessions = await g.db.cart.find({}).toArray();
if(!sessions || sessions.length === 0){
t.fail();
}
t.deepEqual(res.body.message, 'Cart successfully updated');
});
test('[Fail] Add product to cart when subscription already added', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[1]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[1].productOptions)
})
.expect(400);
t.deepEqual(res.body.message, 'Subscription already existing in cart. You cannot add more.');
});
test('[Fail] Add quantity which exceeds the maxQuantity', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[4]._id,
productQuantity: 75,
productOptions: {}
})
.expect(400);
t.deepEqual(res.body.message, 'The quantity exceeds the max amount. Please contact us for larger orders.');
});
test('[Success] Empty cart', async t => {
const res = await g.request
.post('/product/emptycart')
.expect(200);
t.deepEqual(res.body.message, 'Cart successfully emptied');
});
test('[Success] Add product to cart', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(200);
const sessions = await g.db.cart.find({}).toArray();
if(!sessions || sessions.length === 0){
t.fail();
}
t.deepEqual(res.body.message, 'Cart successfully updated');
});
test('[Success] Update cart', async t => {
const cart = await g.request
.get('/cart/retrieve')
.expect(200);
const cartId = Object.keys(cart.body.cart)[0];
const productId = cart.body.cart[cartId].id;
const res = await g.request
.post('/product/updatecart')
.send({
productId: productId,
cartId: cartId,
quantity: 10
})
.expect(200);
t.deepEqual(res.body.message, 'Cart successfully updated');
const checkCart = await g.request
.get('/cart/retrieve')
.expect(200);
// Check new quantity and total price has been updated
t.deepEqual(checkCart.body.cart[cartId].quantity, 10);
t.deepEqual(checkCart.body.cart[cartId].totalItemPrice, cart.body.cart[cartId].totalItemPrice * 10);
});
test('[Fail] Cannot add subscripton when other product in cart', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[7]._id,
productQuantity: 1,
productOptions: {}
})
.expect(400);
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 => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 20,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(400);
t.deepEqual(res.body.message, 'There is insufficient stock of this product.');
});
test('[Fail] Add incorrect product to cart', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: 'fake_product_id',
productQuantity: 20,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(400);
t.deepEqual(res.body.message, 'Error updating cart. Please try again.');
});
test('[Success] Remove item previously added to cart', async t => {
// Add a second product to cart
const 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: cart.body.cartId
})
.expect(200);
t.deepEqual(res.body.message, 'Product successfully removed');
});
test('[Fail] Try remove an item which is not in the cart', async t => {
const res = await g.request
.post('/product/removefromcart')
.send({
cartId: 'bogus_product_id'
})
.expect(400);
t.deepEqual(res.body.message, 'Product not found in cart');
});

View File

@ -19,160 +19,6 @@ test('[Success] Get products JSON', async t => {
}
});
test('[Success] Add subscripton product to cart', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[7]._id,
productQuantity: 1,
productOptions: {}
})
.expect(200);
const sessions = await g.db.cart.find({}).toArray();
if(!sessions || sessions.length === 0){
t.fail();
}
t.deepEqual(res.body.message, 'Cart successfully updated');
});
test('[Fail] Add product to cart when subscription already added', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[1]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[1].productOptions)
})
.expect(400);
t.deepEqual(res.body.message, 'Subscription already existing in cart. You cannot add more.');
});
test('[Fail] Add quantity which exceeds the maxQuantity', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[4]._id,
productQuantity: 75,
productOptions: {}
})
.expect(400);
t.deepEqual(res.body.message, 'The quantity exceeds the max amount. Please contact us for larger orders.');
});
test('[Success] Empty cart', async t => {
const res = await g.request
.post('/product/emptycart')
.expect(200);
t.deepEqual(res.body.message, 'Cart successfully emptied');
});
test('[Success] Add product to cart', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(200);
const sessions = await g.db.cart.find({}).toArray();
if(!sessions || sessions.length === 0){
t.fail();
}
t.deepEqual(res.body.message, 'Cart successfully updated');
});
test('[Success] Update cart', async t => {
const cart = await g.request
.get('/cart/retrieve')
.expect(200);
const productId = g.products[0]._id;
const res = await g.request
.post('/product/updatecart')
.send({
productId: productId,
quantity: 10
})
.expect(200);
t.deepEqual(res.body.message, 'Cart successfully updated');
const checkCart = await g.request
.get('/cart/retrieve')
.expect(200);
// Check new quantity and total price has been updated
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 => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[7]._id,
productQuantity: 1,
productOptions: {}
})
.expect(400);
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 => {
const res = await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 20,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(400);
t.deepEqual(res.body.message, 'There is insufficient stock of this product.');
});
test('[Fail] Add incorrect product to cart', async t => {
const res = await g.request
.post('/product/addtocart')
.send({
id: 'fake_product_id',
state: false
})
.expect(400);
t.deepEqual(res.body.message, 'Error updating cart. Please try again.');
});
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({
productId: g.products[0]._id
})
.expect(200);
t.deepEqual(res.body.message, 'Product successfully removed');
});
test('[Fail] Try remove an item which is not in the cart', async t => {
const res = await g.request
.post('/product/removefromcart')
.send({
cartId: 'bogus_product_id'
})
.expect(400);
t.deepEqual(res.body.message, 'Product not found in cart');
});
test('[Success] Search products', async t => {
const res = await g.request
.get('/category/backpack?json=true')

View File

@ -208,5 +208,6 @@
</div>
</div>
{{/if}}
{{> partials/confirmModal}}
</body>
</html>

View File

@ -0,0 +1,19 @@
<div class="modal fade" id="confirmModal" tabindex="-1" role="dialog" aria-labelledby="confirmModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-danger" id="confirmModalLabel">Confirm</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Are you sure you want to proceed?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary mr-auto" data-dismiss="modal">Close</button>
<button type="button" id="buttonConfirm" class="btn btn-danger">Confirm</button>
</div>
</div>
</div>
</div>

View File

@ -31,14 +31,14 @@
<div class="input-group-prepend">
<button class="btn btn-primary btn-qty-minus" type="button">-</button>
</div>
<input type="number" class="form-control cart-product-quantity text-center" id="{{../this.productId}}-qty" data-id="{{../this.productId}}" maxlength="2" value="{{../this.quantity}}">
<input type="number" class="form-control cart-product-quantity text-center" data-cartid="{{../this.productId}}" data-id="{{../this.id}}" maxlength="2" value="{{../this.quantity}}">
<div class="input-group-append">
<button class="btn btn-primary btn-qty-add" type="button">+</button>
</div>
</div>
</div>
<div class="col-4 col-md-2 no-pad-left">
<button class="btn btn-danger btn-delete-from-cart" data-id="{{../this.productId}}" type="button"><i class="far fa-trash-alt" data-id="{{../this.productId}}" aria-hidden="true"></i></button>
<button class="btn btn-danger btn-delete-from-cart" data-id="{{../this.productId}}" data-cartid="{{../this.productId}}" type="button"><i class="far fa-trash-alt" aria-hidden="true"></i></button>
</div>
{{else}}
<div class="col-12 col-md-8 no-pad-left mb-2"></div>