Adding discount code support (#109)

Added a discount code module. Lots of work went into this. 

Please report any problems as issues to be fixed
master
Mark Moffat 2020-01-21 18:06:46 +10:30 committed by GitHub
parent 83dc6f76ee
commit 799ed301f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1243 additions and 292 deletions

7
app.js
View File

@ -20,7 +20,7 @@ const crypto = require('crypto');
const common = require('./lib/common');
const { runIndexing } = require('./lib/indexing');
const { addSchemas } = require('./lib/schema');
const { initDb } = require('./lib/db');
const { initDb, getDbUri } = require('./lib/db');
let handlebars = require('express-handlebars');
const i18n = require('i18n');
@ -237,6 +237,9 @@ handlebars = handlebars.create({
formatDate: (date, format) => {
return moment(date).format(format);
},
discountExpiry: (start, end) => {
return moment().isBetween(moment(start), moment(end));
},
ifCond: (v1, operator, v2, options) => {
switch(operator){
case'==':
@ -317,7 +320,7 @@ handlebars = handlebars.create({
// session store
const store = new MongoStore({
uri: config.databaseConnectionString,
uri: getDbUri(config.databaseConnectionString),
collection: 'sessions'
});

View File

@ -246,6 +246,36 @@
"orderProducts": []
}
],
"discounts": [
{
"code": "valid_10_amount_code",
"type": "amount",
"value": 10,
"start": "",
"end": ""
},
{
"code": "valid_10_percent_code",
"type": "percent",
"value": 10,
"start": "",
"end": ""
},
{
"code": "expired_10_percent_code",
"type": "percent",
"value": 10,
"start": "",
"end": ""
},
{
"code": "future_10_percent_code",
"type": "percent",
"value": 10,
"start": "",
"end": ""
}
],
"menu": {
"items": [
{

View File

@ -28,7 +28,8 @@
"maxQuantity": 25,
"modules": {
"enabled": {
"shipping": "shipping-basic"
"shipping": "shipping-basic",
"discount": "discount-voucher"
}
}
}

View File

@ -7,7 +7,7 @@ const restrictedRoutes = [
{ route: '/admin/product/edit/:id', response: 'redirect' },
{ route: '/admin/product/update', response: 'redirect' },
{ route: '/admin/product/delete/:id', response: 'redirect' },
{ route: '/admin/product/published_state', response: 'json' },
{ route: '/admin/product/publishedState', response: 'json' },
{ route: '/admin/product/setasmainimage', response: 'json' },
{ route: '/admin/product/deleteimage', response: 'json' },
{ route: '/admin/product/removeoption', response: 'json' },
@ -20,7 +20,7 @@ const restrictedRoutes = [
{ route: '/admin/settings/menu/new', response: 'json' },
{ route: '/admin/settings/menu/update', response: 'json' },
{ route: '/admin/settings/menu/delete', response: 'json' },
{ route: '/admin/settings/menu/save_order', response: 'json' },
{ route: '/admin/settings/menu/saveOrder', response: 'json' },
{ route: '/admin/file/upload', response: 'json' }
];

View File

@ -111,8 +111,9 @@ const clearSessionValue = (session, sessionVar) => {
return temp;
};
const updateTotalCart = (req, res) => {
const updateTotalCart = async (req, res) => {
const config = getConfig();
const db = req.app.db;
req.session.totalCartAmount = 0;
req.session.totalCartItems = 0;
@ -131,15 +132,34 @@ const updateTotalCart = (req, res) => {
// Update the total items in cart for the badge
req.session.totalCartItems = Object.keys(req.session.cart).length;
// Net cart amount
const netCartAmount = req.session.totalCartAmount - req.session.totalCartShipping || 0;
// Update the total amount not including shipping/discounts
req.session.totalCartNetAmount = req.session.totalCartAmount;
// Calculate shipping using the loaded module
config.modules.loaded.shipping.calculateShipping(
netCartAmount,
req.session.totalCartNetAmount,
config,
req
);
// If discount module enabled
if(config.modules.loaded.discount){
// Recalculate discounts
const discount = await db.discounts.findOne({ code: req.session.discountCode });
if(discount){
config.modules.loaded.discount.calculateDiscount(
discount,
req
);
}else{
// If discount code is not found, remove it
delete req.session.discountCode;
req.session.totalCartDiscount = 0;
}
}
// Calculate our total amount removing discount and adding shipping
req.session.totalCartAmount = (req.session.totalCartNetAmount - req.session.totalCartDiscount) + req.session.totalCartShipping;
};
const emptyCart = async (req, res, type, customMessage) => {
@ -150,12 +170,13 @@ const emptyCart = async (req, res, type, customMessage) => {
delete req.session.shippingAmount;
delete req.session.orderId;
delete req.session.cartSubscription;
delete req.session.discountCode;
// Remove cart from DB
await db.cart.deleteOne({ sessionId: req.session.id });
// update total cart
updateTotalCart(req, res);
await updateTotalCart(req, res);
// Update checking cart for subscription
updateSubscriptionCheck(req, res);

View File

@ -14,15 +14,14 @@ function initDb(dbUrl, callback){ // eslint-disable-line
return callback(err);
}
// Set the DB url
dbUrl = getDbUri(dbUrl);
// select DB
const dbUriObj = mongodbUri.parse(dbUrl);
let db;
// if in testing, set the testing DB
if(process.env.NODE_ENV === 'test'){
db = client.db('testingdb');
}else{
db = client.db(dbUriObj.database);
}
// Set the DB depending on ENV
const db = client.db(dbUriObj.database);
// setup the collections
db.users = db.collection('users');
@ -33,17 +32,28 @@ function initDb(dbUrl, callback){ // eslint-disable-line
db.customers = db.collection('customers');
db.cart = db.collection('cart');
db.sessions = db.collection('sessions');
db.discounts = db.collection('discounts');
_db = db;
return callback(null, _db);
}
};
function getDbUri(dbUrl){
const dbUriObj = mongodbUri.parse(dbUrl);
// if in testing, set the testing DB
if(process.env.NODE_ENV === 'test'){
dbUriObj.database = 'expresscart-test';
}
return mongodbUri.format(dbUriObj);
}
function getDb(){
return _db;
}
module.exports = {
getDb,
initDb
initDb,
getDbUri
};

View File

@ -0,0 +1,18 @@
const calculateDiscount = (discount, req) => {
let discountAmount = 0;
if(req.session.discountCode){
if(discount.type === 'amount'){
discountAmount = discount.value;
}
if(discount.type === 'percent'){
// Apply the discount on the net cart amount (eg: minus shipping)
discountAmount = (discount.value / 100) * req.session.totalCartNetAmount;
}
}
req.session.totalCartDiscount = discountAmount;
};
module.exports = {
calculateDiscount
};

View File

@ -24,7 +24,7 @@ const calculateShipping = (amount, config, req) => {
if(!req.session.customerCountry){
req.session.shippingMessage = 'Estimated shipping';
req.session.totalCartShipping = domesticShippingAmount;
req.session.totalCartAmount = req.session.totalCartAmount + domesticShippingAmount;
req.session.totalCartAmount = amount + domesticShippingAmount;
return;
}
@ -32,14 +32,14 @@ const calculateShipping = (amount, config, req) => {
if(req.session.customerCountry.toLowerCase() !== shippingFromCountry.toLowerCase()){
req.session.shippingMessage = 'International shipping';
req.session.totalCartShipping = internationalShippingAmount;
req.session.totalCartAmount = req.session.totalCartAmount + internationalShippingAmount;
req.session.totalCartAmount = amount + internationalShippingAmount;
return;
}
// Domestic shipping
req.session.shippingMessage = 'Domestic shipping';
req.session.totalCartShipping = domesticShippingAmount;
req.session.totalCartAmount = req.session.totalCartAmount + domesticShippingAmount;
req.session.totalCartAmount = amount + domesticShippingAmount;
};
module.exports = {

View File

@ -1,5 +1,6 @@
const path = require('path');
const fs = require('fs');
const moment = require('moment');
const glob = require('glob');
const Ajv = require('ajv');
const ajv = new Ajv();
@ -19,6 +20,13 @@ const addSchemas = () => {
const amountRegex = /^\d+\.\d\d$/;
ajv.addFormat('amount', amountRegex);
// Datetime format
ajv.addFormat('datetime', {
validate: (dateTimeString) => {
return moment(dateTimeString, 'DD/MM/YYYY HH:mm').isValid();
}
});
ajv.addKeyword('isNotEmpty', {
type: 'string',
validate: (schema, data) => {

View File

@ -0,0 +1,30 @@
{
"$id": "editDiscount",
"type": "object",
"properties": {
"code": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"amount",
"percent"
]
},
"value": {
"type": "number"
},
"start": {
"type": "object",
"format" : "datetime"
},
"end": {
"type": "object",
"format" : "datetime"
}
},
"required": [
"discountId"
]
}

View File

@ -0,0 +1,34 @@
{
"$id": "newDiscount",
"type": "object",
"properties": {
"code": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"amount",
"percent"
]
},
"value": {
"type": "number"
},
"start": {
"type": "object",
"format" : "datetime"
},
"end": {
"type": "object",
"format" : "datetime"
}
},
"required": [
"code",
"type",
"value",
"start",
"end"
]
}

View File

@ -1,6 +1,6 @@
const { getConfig } = require('./common');
const { initDb } = require('./db');
const { fixProductDates } = require('../test/helper');
const { fixProductDates, fixDiscountDates } = require('../test/helper');
const fs = require('fs');
const path = require('path');
@ -15,6 +15,7 @@ initDb(config.databaseConnectionString, (err, db) => {
db.users.deleteMany({}, {}),
db.customers.deleteMany({}, {}),
db.products.deleteMany({}, {}),
db.discounts.deleteMany({}, {}),
db.menu.deleteMany({}, {})
])
.then(() => {
@ -22,6 +23,7 @@ initDb(config.databaseConnectionString, (err, db) => {
db.users.insertMany(jsonData.users),
db.customers.insertMany(jsonData.customers),
db.products.insertMany(fixProductDates(jsonData.products)),
db.discounts.insertMany(fixDiscountDates(jsonData.discounts)),
db.menu.insertOne(jsonData.menu)
])
.then(() => {

View File

@ -172,5 +172,24 @@
"Dashboard": "Dashboard",
"Create order": "Create order",
"Order shipping amount": "Order shipping amount",
"Order net amount": "Order net amount"
"Order net amount": "Order net amount",
"Discount code": "Discount code",
"Apply": "Apply",
"Discount codes": "Discount codes",
"Code": "Code",
"Expiry": "Expiry",
"There are currently no discount codes setup.": "There are currently no discount codes setup.",
"New discount": "New discount",
"Amount": "Amount",
"Percent": "Percent",
"Discount type": "Discount type",
"Discount value": "Discount value",
"Discount": "Discount",
"Edit discount": "Edit discount",
"New Discount": "New Discount",
"Discount start": "Discount start",
"Discount end": "Discount end",
"Start": "Start",
"Running": "Running",
"Not running": "Not running"
}

145
package-lock.json generated
View File

@ -9255,71 +9255,6 @@
}
}
},
"superagent": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
"integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
"dev": true,
"requires": {
"component-emitter": "^1.2.0",
"cookiejar": "^2.1.0",
"debug": "^3.1.0",
"extend": "^3.0.0",
"form-data": "^2.3.1",
"formidable": "^1.2.0",
"methods": "^1.1.1",
"mime": "^1.4.1",
"qs": "^6.5.1",
"readable-stream": "^2.3.5"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"supertap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supertap/-/supertap-1.0.0.tgz",
@ -9363,22 +9298,78 @@
}
},
"supertest": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz",
"integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz",
"integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==",
"dev": true,
"requires": {
"methods": "^1.1.2",
"superagent": "^3.8.3"
}
},
"supertest-session": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/supertest-session/-/supertest-session-4.0.0.tgz",
"integrity": "sha512-9d7KAL+K9hnnicov7USv/Nu1tSl40qSrOsB8zZHOEtfEzHaAoID6tbl1NeCVUg7SJreJMlNn+KJ88V7FW8gD6Q==",
"dev": true,
"requires": {
"object-assign": "^4.0.1"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
},
"superagent": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
"integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
"dev": true,
"requires": {
"component-emitter": "^1.2.0",
"cookiejar": "^2.1.0",
"debug": "^3.1.0",
"extend": "^3.0.0",
"form-data": "^2.3.1",
"formidable": "^1.2.0",
"methods": "^1.1.1",
"mime": "^1.4.1",
"qs": "^6.5.1",
"readable-stream": "^2.3.5"
}
}
}
},
"supports-color": {

View File

@ -84,8 +84,7 @@
"gulp-minify": "^3.1.0",
"gulp-rename": "^1.4.0",
"less": "^3.10.3",
"supertest": "^3.4.2",
"supertest-session": "^4.0.0"
"supertest": "^4.0.2"
},
"main": "app.js",
"keywords": [

View File

@ -284,11 +284,11 @@ $(document).ready(function (){
});
// Call to API to check if a permalink is available
$(document).on('click', '#validate_permalink', function(e){
$(document).on('click', '#validatePermalink', function(e){
if($('#productPermalink').val() !== ''){
$.ajax({
method: 'POST',
url: '/admin/api/validate_permalink',
url: '/admin/validatePermalink',
data: { permalink: $('#productPermalink').val(), docId: $('#productId').val() }
})
.done(function(msg){
@ -404,10 +404,10 @@ $(document).ready(function (){
});
// Call to API for a change to the published state of a product
$('input[class="published_state"]').change(function(){
$('input[class="publishedState"]').change(function(){
$.ajax({
method: 'POST',
url: '/admin/product/published_state',
url: '/admin/product/publishedState',
data: { id: this.id, state: this.checked }
})
.done(function(msg){
@ -544,6 +544,86 @@ $(document).ready(function (){
}
});
$('#discountNewForm').validator().on('submit', function(e){
if(!e.isDefaultPrevented()){
e.preventDefault();
$.ajax({
method: 'POST',
url: '/admin/settings/discount/create',
data: {
code: $('#discountCode').val(),
type: $('#discountType').val(),
value: $('#discountValue').val(),
start: $('#discountStart').val(),
end: $('#discountEnd').val()
}
})
.done(function(msg){
showNotification(msg.message, 'success', false, '/admin/settings/discount/edit/' + msg.discountId);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
}
});
$('#discountEditForm').validator().on('submit', function(e){
if(!e.isDefaultPrevented()){
e.preventDefault();
$.ajax({
method: 'POST',
url: '/admin/settings/discount/update',
data: {
discountId: $('#discountId').val(),
code: $('#discountCode').val(),
type: $('#discountType').val(),
value: $('#discountValue').val(),
start: $('#discountStart').val(),
end: $('#discountEnd').val()
}
})
.done(function(msg){
showNotification(msg.message, 'success');
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
}
});
$('#discountStart').datetimepicker({
uiLibrary: 'bootstrap4',
footer: true,
modal: true,
format: 'dd/mm/yyyy HH:MM',
showOtherMonths: true
});
$('#discountEnd').datetimepicker({
uiLibrary: 'bootstrap4',
footer: true,
modal: true,
format: 'dd/mm/yyyy HH:MM'
});
$(document).on('click', '#btnDiscountDelete', function(e){
e.preventDefault();
if(confirm('Are you sure?')){
$.ajax({
method: 'DELETE',
url: '/admin/settings/discount/delete',
data: {
discountId: $(this).attr('data-id')
}
})
.done(function(msg){
showNotification(msg.message, 'success', true);
})
.fail(function(msg){
showNotification(msg.message, 'danger', true);
});
}
});
$(document).on('click', '#settings-menu-new', function(e){
e.preventDefault();
$.ajax({
@ -613,7 +693,7 @@ $(document).ready(function (){
$.ajax({
data: { order: menuOrder },
type: 'POST',
url: '/admin/settings/menu/save_order'
url: '/admin/settings/menu/saveOrder'
})
.done(function(){
showNotification('Menu order saved', 'success', true);

File diff suppressed because one or more lines are too long

View File

@ -183,6 +183,38 @@ $(document).ready(function (){
}
});
$('#addDiscountCode').on('click', function(e){
e.preventDefault();
$.ajax({
method: 'POST',
url: '/checkout/adddiscountcode',
data: {
discountCode: $('#discountCode').val()
}
})
.done(function(msg){
showNotification(msg.message, 'success', true);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
});
$('#removeDiscountCode').on('click', function(e){
e.preventDefault();
$.ajax({
method: 'POST',
url: '/checkout/removediscountcode',
data: {}
})
.done(function(msg){
showNotification(msg.message, 'success', true);
})
.fail(function(msg){
showNotification(msg.responseJSON.message, 'danger');
});
});
$('#loginForm').on('click', function(e){
if(!e.isDefaultPrevented()){
e.preventDefault();
@ -513,6 +545,15 @@ function updateCartDiv(){
shippingTotal = `<span id="shipping-amount">${session.shippingMessage}</span>`;
}
var discountTotalAmt = numeral(session.totalCartDiscount).format('0.00');
var discountTotal = '';
if(session.totalCartDiscount > 0){
discountTotal = `
<div class="text-right">
Discount: <strong id="discount-amount">${result.currencySymbol}${discountTotalAmt}</strong>
</div>`;
}
// If the cart has contents
if(cart){
$('#cart-empty').empty();
@ -553,16 +594,16 @@ function updateCartDiv(){
<div class="col-12 col-md-6 no-pad-left mb-2">
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-primary btn-qty-minus" type="button">-</button>
<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}">
<div class="input-group-append">
<button class="btn btn-outline-primary btn-qty-add" type="button">+</button>
<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-outline-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-id="${productId}" type="button"><i class="far fa-trash-alt" data-id="${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>
@ -588,6 +629,7 @@ function updateCartDiv(){
<div class="text-right">
${shippingTotal}
</div>
${discountTotal}
<div class="text-right">
Total:
<strong id="total-cart-amount">${result.currencySymbol}${totalAmount}</strong>

File diff suppressed because one or more lines are too long

View File

@ -4,11 +4,6 @@
background-color: #000000;
border-color: #000000;
}
.btn-outline-danger {
color: #ffffff !important;
background-color: #cc4135;
border-color: #cc4135;
}
.has-error input,
.has-error textarea,
.has-error div {

View File

@ -1 +1 @@
.btn-outline-primary,.btn-warning{color:#fff!important;background-color:#000;border-color:#000}.btn-outline-danger{color:#fff!important;background-color:#cc4135;border-color:#cc4135}.has-error div,.has-error input,.has-error textarea{border-color:#cc4135}#frm_search,.search-bar-input,.search-bar-input .btn{padding-top:10px;height:45px}.productsWrapper{padding-right:10px;padding-left:10px}.searchBarWrapper{padding-right:0;padding-left:0}.footer{padding-top:20px}.product-price{padding-bottom:0}.navbarMenuWrapper{background-color:#f5f5f5}.navbarMenu>ul>li>a:hover{color:#cc4135!important}.navbarMenu{padding-right:0;padding-left:0}.product-wrapper>a:hover{color:#cc4135!important}#navbar,#navbar>.navbar-nav,#navbar>.navbar-nav>li>a,.navbar-header,.navbar-static-top{margin-bottom:0;height:100px!important}#navbar>.navbar-nav>li>a{padding-top:35px}.pagination>li>a{background-color:#cc4135!important}body .popover{display:none!important}.navbar-brand{color:#cc4135!important;letter-spacing:4px;padding-left:20px!important;padding-top:0!important;height:80px!important;font-size:55px!important}.navbar-brand,.navbar-brand-image{height:80px;padding-left:10px;padding-top:10px}.navbar-default .badge{background-color:#cc4135}#empty-cart:active,#empty-cart:active:hover,#empty-cart:focus,#empty-cart:hover,.pushy-link:active,.pushy-link:active:hover,.pushy-link:focus,.pushy-link:hover{border-color:#cc4135;background-color:#cc4135}.navActive>a{margin-bottom:0;padding-top:15px;border-bottom:5px solid #cc4135}#navbar,#navbar>.navbar-nav,#navbar>.navbar-nav>li>a,.navbar-header,.navbar-static-top{background-color:#fff}.navbar-default .navbar-nav>li>a{color:#838b8f;font-size:20px}.global-result-type{color:#8d8d8d}.global-result:hover{background-color:#007bff}.global-result:hover .global-result-detail,.global-result:hover .global-result-type,.global-result:hover .global-result-type .fal{color:#fff!important}.global-result a{text-decoration:none!important}.global-result:hover{cursor:pointer}.global-result:first-child{border-top-left-radius:0;border-top-right-radius:0}.global-result{border-left:0;border-right:0}.global-search-modal-content,.global-search-modal-header{background-color:transparent;border:none}#global-search-results{padding-right:0;border-bottom-left-radius:.3rem;border-bottom-right-radius:.3rem}.global-search-form{margin-bottom:0}#global-search-value{border-bottom-right-radius:0}.search-input-addon{border-bottom-left-radius:0}@media only screen and (max-width:768px){.navbar-default .navbar-brand{padding-top:10px}.navbar-default .navbar-nav>li>a{font-size:16px}.searchBarWrapper{padding-top:10px}.navbarMenuWrapper{padding-left:0;padding-right:0}.navbarMenuOuter{padding-left:0;padding-right:0}.navActive>a{color:#fff!important}.navbarMenu{padding-right:7.5px;padding-left:7.5px}.navActive>a{color:#fff!important;background-color:#cc4135;border-bottom:none}.footer{padding-top:10px}}
.btn-outline-primary,.btn-warning{color:#fff!important;background-color:#000;border-color:#000}.has-error div,.has-error input,.has-error textarea{border-color:#cc4135}#frm_search,.search-bar-input,.search-bar-input .btn{padding-top:10px;height:45px}.productsWrapper{padding-right:10px;padding-left:10px}.searchBarWrapper{padding-right:0;padding-left:0}.footer{padding-top:20px}.product-price{padding-bottom:0}.navbarMenuWrapper{background-color:#f5f5f5}.navbarMenu>ul>li>a:hover{color:#cc4135!important}.navbarMenu{padding-right:0;padding-left:0}.product-wrapper>a:hover{color:#cc4135!important}#navbar,#navbar>.navbar-nav,#navbar>.navbar-nav>li>a,.navbar-header,.navbar-static-top{margin-bottom:0;height:100px!important}#navbar>.navbar-nav>li>a{padding-top:35px}.pagination>li>a{background-color:#cc4135!important}body .popover{display:none!important}.navbar-brand{color:#cc4135!important;letter-spacing:4px;padding-left:20px!important;padding-top:0!important;height:80px!important;font-size:55px!important}.navbar-brand,.navbar-brand-image{height:80px;padding-left:10px;padding-top:10px}.navbar-default .badge{background-color:#cc4135}#empty-cart:active,#empty-cart:active:hover,#empty-cart:focus,#empty-cart:hover,.pushy-link:active,.pushy-link:active:hover,.pushy-link:focus,.pushy-link:hover{border-color:#cc4135;background-color:#cc4135}.navActive>a{margin-bottom:0;padding-top:15px;border-bottom:5px solid #cc4135}#navbar,#navbar>.navbar-nav,#navbar>.navbar-nav>li>a,.navbar-header,.navbar-static-top{background-color:#fff}.navbar-default .navbar-nav>li>a{color:#838b8f;font-size:20px}.global-result-type{color:#8d8d8d}.global-result:hover{background-color:#007bff}.global-result:hover .global-result-detail,.global-result:hover .global-result-type,.global-result:hover .global-result-type .fal{color:#fff!important}.global-result a{text-decoration:none!important}.global-result:hover{cursor:pointer}.global-result:first-child{border-top-left-radius:0;border-top-right-radius:0}.global-result{border-left:0;border-right:0}.global-search-modal-content,.global-search-modal-header{background-color:transparent;border:none}#global-search-results{padding-right:0;border-bottom-left-radius:.3rem;border-bottom-right-radius:.3rem}.global-search-form{margin-bottom:0}#global-search-value{border-bottom-right-radius:0}.search-input-addon{border-bottom-left-radius:0}@media only screen and (max-width:768px){.navbar-default .navbar-brand{padding-top:10px}.navbar-default .navbar-nav>li>a{font-size:16px}.searchBarWrapper{padding-top:10px}.navbarMenuWrapper{padding-left:0;padding-right:0}.navbarMenuOuter{padding-left:0;padding-right:0}.navActive>a{color:#fff!important}.navbarMenu{padding-right:7.5px;padding-left:7.5px}.navActive>a{color:#fff!important;background-color:#cc4135;border-bottom:none}.footer{padding-top:10px}}

View File

@ -10,12 +10,6 @@
border-color: @btn-danger-color-border;
}
.btn-outline-danger {
color: @btn-danger-color-txt !important;
background-color: @accent-color;
border-color: @accent-color;
}
.has-error input, .has-error textarea, .has-error div {
border-color: @accent-color;
}

View File

@ -490,26 +490,29 @@ input.form-control.customerDetails{
color: @primary-btn-color;
}
.btn-warning, .btn-outline-primary {
.btn-warning, .btn-primary {
color: #ffffff;
background-color: @primary-btn-color;
border-color: @primary-btn-color;
}
.btn-warning:hover, .btn-outline-primary:hover {
.btn-primary:hover,
.btn-primary:active,
.btn-primary.focus,
.btn-primary:focus {
color: #ffffff !important;
background-color: @primary-btn-color !important;
border-color: @primary-btn-color !important;
opacity: 0.65;
}
.btn-outline-danger{
.btn-danger{
color: #ffffff;
background-color: @secondary-btn-color;
border-color: @secondary-btn-color;
}
.btn-outline-danger:hover{
.btn-danger:hover{
color: #ffffff !important;
background-color: @secondary-btn-color !important;
border-color: @secondary-btn-color !important;

View File

@ -391,24 +391,26 @@ input.form-control.customerDetails {
color: #000000;
}
.btn-warning,
.btn-outline-primary {
.btn-primary {
color: #ffffff;
background-color: #000000;
border-color: #000000;
}
.btn-warning:hover,
.btn-outline-primary:hover {
.btn-primary:hover,
.btn-primary:active,
.btn-primary.focus,
.btn-primary:focus {
color: #ffffff !important;
background-color: #000000 !important;
border-color: #000000 !important;
opacity: 0.65;
}
.btn-outline-danger {
.btn-danger {
color: #ffffff;
background-color: #cc3a2c;
border-color: #cc3a2c;
}
.btn-outline-danger:hover {
.btn-danger:hover {
color: #ffffff !important;
background-color: #cc3a2c !important;
border-color: #cc3a2c !important;

File diff suppressed because one or more lines are too long

View File

@ -4,10 +4,12 @@ const { restrict, checkAccess } = require('../lib/auth');
const escape = require('html-entities').AllHtmlEntities;
const colors = require('colors');
const bcrypt = require('bcryptjs');
const moment = require('moment');
const fs = require('fs');
const path = require('path');
const multer = require('multer');
const mime = require('mime-type/with-db');
const { validateJson } = require('../lib/schema');
const ObjectId = require('mongodb').ObjectID;
const router = express.Router();
@ -401,7 +403,7 @@ router.post('/admin/settings/menu/delete', restrict, checkAccess, (req, res) =>
});
// We call this via a Ajax call to save the order from the sortable list
router.post('/admin/settings/menu/save_order', restrict, checkAccess, (req, res) => {
router.post('/admin/settings/menu/saveOrder', restrict, checkAccess, (req, res) => {
const result = common.orderMenu(req, res);
if(result === false){
res.status(400).json({ message: 'Failed saving menu order' });
@ -411,7 +413,7 @@ router.post('/admin/settings/menu/save_order', restrict, checkAccess, (req, res)
});
// validate the permalink
router.post('/admin/api/validate_permalink', async (req, res) => {
router.post('/admin/validatePermalink', async (req, res) => {
// if doc id is provided it checks for permalink in any products other that one provided,
// else it just checks for any products with that permalink
const db = req.app.db;
@ -431,6 +433,168 @@ router.post('/admin/api/validate_permalink', async (req, res) => {
res.status(200).json({ message: 'Permalink validated successfully' });
});
// Discount codes
router.get('/admin/settings/discounts', restrict, checkAccess, async (req, res) => {
const db = req.app.db;
const discounts = await db.discounts.find({}).toArray();
res.render('settings-discounts', {
title: 'Discount code',
config: req.app.config,
session: req.session,
discounts,
admin: true,
message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers
});
});
// Edit a discount code
router.get('/admin/settings/discount/edit/:id', restrict, checkAccess, async (req, res) => {
const db = req.app.db;
const discount = await db.discounts.findOne({ _id: common.getId(req.params.id) });
res.render('settings-discount-edit', {
title: 'Discount code edit',
session: req.session,
admin: true,
discount,
message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers,
config: req.app.config
});
});
// Update discount code
router.post('/admin/settings/discount/update', restrict, checkAccess, async (req, res) => {
const db = req.app.db;
// Doc to insert
const discountDoc = {
discountId: req.body.discountId,
code: req.body.code,
type: req.body.type,
value: parseInt(req.body.value),
start: moment(req.body.start, 'DD/MM/YYYY HH:mm').toDate(),
end: moment(req.body.end, 'DD/MM/YYYY HH:mm').toDate()
};
// Validate the body again schema
const schemaValidate = validateJson('editDiscount', discountDoc);
if(!schemaValidate.result){
res.status(400).json(schemaValidate.errors);
return;
}
// Check start is after today
if(moment(discountDoc.start).isBefore(moment())){
res.status(400).json({ message: 'Discount start date needs to be after today' });
return;
}
// Check end is after the start
if(!moment(discountDoc.end).isAfter(moment(discountDoc.start))){
res.status(400).json({ message: 'Discount end date needs to be after start date' });
return;
}
// Check if code exists
const checkCode = await db.discounts.countDocuments({
code: discountDoc.code,
_id: { $ne: common.getId(discountDoc.discountId) }
});
if(checkCode){
res.status(400).json({ message: 'Discount code already exists' });
return;
}
// Remove discountID
delete discountDoc.discountId;
try{
await db.discounts.updateOne({ _id: common.getId(req.body.discountId) }, { $set: discountDoc }, {});
res.status(200).json({ message: 'Successfully saved', discount: discountDoc });
}catch(ex){
res.status(400).json({ message: 'Failed to save. Please try again' });
}
});
// Create a discount code
router.get('/admin/settings/discount/new', restrict, checkAccess, async (req, res) => {
res.render('settings-discount-new', {
title: 'Discount code create',
session: req.session,
admin: true,
message: common.clearSessionValue(req.session, 'message'),
messageType: common.clearSessionValue(req.session, 'messageType'),
helpers: req.handlebars.helpers,
config: req.app.config
});
});
// Create a discount code
router.post('/admin/settings/discount/create', restrict, checkAccess, async (req, res) => {
const db = req.app.db;
// Doc to insert
const discountDoc = {
code: req.body.code,
type: req.body.type,
value: parseInt(req.body.value),
start: moment(req.body.start, 'DD/MM/YYYY HH:mm').toDate(),
end: moment(req.body.end, 'DD/MM/YYYY HH:mm').toDate()
};
// Validate the body again schema
const schemaValidate = validateJson('newDiscount', discountDoc);
if(!schemaValidate.result){
res.status(400).json(schemaValidate.errors);
return;
}
// Check if code exists
const checkCode = await db.discounts.countDocuments({
code: discountDoc.code
});
if(checkCode){
res.status(400).json({ message: 'Discount code already exists' });
return;
}
// Check start is after today
if(moment(discountDoc.start).isBefore(moment())){
res.status(400).json({ message: 'Discount start date needs to be after today' });
return;
}
// Check end is after the start
if(!moment(discountDoc.end).isAfter(moment(discountDoc.start))){
res.status(400).json({ message: 'Discount end date needs to be after start date' });
return;
}
// Insert discount code
const discount = await db.discounts.insertOne(discountDoc);
res.status(200).json({ message: 'Discount code created successfully', discountId: discount.insertedId });
});
// Delete discount code
router.delete('/admin/settings/discount/delete', restrict, checkAccess, async (req, res) => {
const db = req.app.db;
try{
await db.discounts.deleteOne({ _id: common.getId(req.body.discountId) }, {});
res.status(200).json({ message: 'Discount code successfully deleted' });
return;
}catch(ex){
res.status(400).json({ message: 'Error deleting discount code. Please try again.' });
}
});
// upload the file
const upload = multer({ dest: 'public/uploads/' });
router.post('/admin/file/upload', restrict, checkAccess, upload.single('uploadFile'), async (req, res) => {

View File

@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const colors = require('colors');
const moment = require('moment');
const _ = require('lodash');
const {
getId,
@ -173,7 +174,7 @@ router.get('/checkout/cartdata', (req, res) => {
});
});
router.get('/checkout/payment', (req, res) => {
router.get('/checkout/payment', async (req, res) => {
const config = req.app.config;
// if there is no items in the cart then render a failure
@ -189,6 +190,9 @@ router.get('/checkout/payment', (req, res) => {
paymentType = '_subscription';
}
// update total cart amount one last time before payment
await updateTotalCart(req, res);
res.render(`${config.themeViews}checkout-payment`, {
title: 'Checkout',
config: req.app.config,
@ -207,6 +211,84 @@ router.get('/checkout/payment', (req, res) => {
});
});
router.post('/checkout/adddiscountcode', async (req, res) => {
const config = req.app.config;
const db = req.app.db;
// if there is no items in the cart return a failure
if(!req.session.cart){
res.status(400).json({
message: 'The are no items in your cart.'
});
return;
}
// Check if the discount module is loaded
if(!config.modules.loaded.discount){
res.status(400).json({
message: 'Access denied.'
});
return;
}
// Check defined or null
if(!req.body.discountCode || req.body.discountCode === ''){
res.status(400).json({
message: 'Discount code is invalid or expired'
});
return;
}
// Validate discount code
const discount = await db.discounts.findOne({ code: req.body.discountCode });
if(!discount){
res.status(400).json({
message: 'Discount code is invalid or expired'
});
return;
}
// Validate date validity
if(!moment().isBetween(moment(discount.start), moment(discount.end))){
res.status(400).json({
message: 'Discount is expired'
});
return;
}
// Set the discount code
req.session.discountCode = discount.code;
// Update the cart amount
await updateTotalCart(req, res);
// Return the message
res.status(200).json({
message: 'Discount code applied'
});
});
router.post('/checkout/removediscountcode', async (req, res) => {
// if there is no items in the cart return a failure
if(!req.session.cart){
res.status(400).json({
message: 'The are no items in your cart.'
});
return;
}
// Delete the discount code
delete req.session.discountCode;
// update total cart amount
await updateTotalCart(req, res);
// Return the message
res.status(200).json({
message: 'Discount code removed'
});
});
// show an individual product
router.get('/product/:id', async (req, res) => {
const db = req.app.db;
@ -310,7 +392,7 @@ router.post('/product/updatecart', async (req, res, next) => {
req.session.cart[cartItem.productId].totalItemPrice = productPrice * productQuantity;
// update total cart amount
updateTotalCart(req, res);
await updateTotalCart(req, res);
// Update checking cart for subscription
updateSubscriptionCheck(req, res);
@ -345,7 +427,7 @@ router.post('/product/removefromcart', async (req, res, next) => {
$set: { cart: req.session.cart }
});
// update total cart
updateTotalCart(req, res);
await updateTotalCart(req, res);
// Update checking cart for subscription
updateSubscriptionCheck(req, res);
@ -489,7 +571,7 @@ router.post('/product/addtocart', async (req, res, next) => {
}, { upsert: true });
// update total cart amount
updateTotalCart(req, res);
await updateTotalCart(req, res);
// Update checking cart for subscription
updateSubscriptionCheck(req, res);

View File

@ -305,7 +305,7 @@ router.post('/admin/product/delete', restrict, checkAccess, async (req, res) =>
});
// update the published state based on an ajax call from the frontend
router.post('/admin/product/published_state', restrict, checkAccess, async (req, res) => {
router.post('/admin/product/publishedState', restrict, checkAccess, async (req, res) => {
const db = req.app.db;
try{