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{

View File

@ -1,6 +1,7 @@
const fs = require('fs');
const _ = require('lodash');
const session = require('supertest-session');
const moment = require('moment');
const supertest = require('supertest');
const app = require('../app.js');
const { newId } = require('../lib/common');
const { runIndexing } = require('../lib/indexing');
@ -14,6 +15,7 @@ const g = {
db: {},
config: {},
products: {},
discounts: {},
customers: {},
users: {},
request: null,
@ -26,20 +28,23 @@ const setup = (db) => {
db.users.deleteMany({}, {}),
db.customers.deleteMany({}, {}),
db.products.deleteMany({}, {}),
db.orders.deleteMany({}, {})
db.discounts.deleteMany({}, {}),
db.orders.deleteMany({}, {}),
db.sessions.deleteMany({}, {})
])
.then(() => {
return Promise.all([
db.users.insertMany(addApiKey(jsonData.users)),
db.customers.insertMany(jsonData.customers),
db.products.insertMany(fixProductDates(jsonData.products))
db.products.insertMany(fixProductDates(jsonData.products)),
db.discounts.insertMany(fixDiscountDates(jsonData.discounts))
]);
});
};
const runBefore = async () => {
// Create a session
g.request = session(app);
g.request = supertest.agent(app);
await new Promise(resolve => {
app.on('appStarted', async () => {
// Set some stuff now we have the app started
@ -51,6 +56,7 @@ const runBefore = async () => {
// Get some data from DB to use in compares
g.products = await g.db.products.find({}).toArray();
g.customers = await g.db.customers.find({}).toArray();
g.discounts = await g.db.discounts.find({}).toArray();
g.users = await g.db.users.find({}).toArray();
// Insert orders using product ID's
@ -87,6 +93,36 @@ const fixProductDates = (products) => {
return products;
};
const fixDiscountDates = (discounts) => {
let index = 0;
discounts.forEach(() => {
let startDate = moment().subtract(1, 'days').toDate();
let endDate = moment().add(7, 'days').toDate();
const expiredStart = moment().subtract(14, 'days').toDate();
const expiredEnd = moment().subtract(7, 'days').toDate();
const futureStart = moment().add(7, 'days').toDate();
const futureEnd = moment().add(14, 'days').toDate();
// If code is expired, make sure the dates are correct
if(discounts[index].code.substring(0, 7) === 'expired'){
startDate = expiredStart;
endDate = expiredEnd;
}
// If code is future, make sure the dates are correct
if(discounts[index].code.substring(0, 6) === 'future'){
startDate = futureStart;
endDate = futureEnd;
}
// Set the expiry dates
discounts[index].start = startDate;
discounts[index].end = endDate;
index++;
});
return discounts;
};
const addApiKey = (users) => {
let index = 0;
users.forEach(() => {
@ -100,5 +136,6 @@ module.exports = {
runBefore,
setup,
g,
fixProductDates
fixProductDates,
fixDiscountDates
};

233
test/specs/discounts.js Normal file
View File

@ -0,0 +1,233 @@
import{ serial as test }from'ava';
const {
runBefore,
g
} = require('../helper');
const moment = require('moment');
test.before(async () => {
await runBefore();
});
test('[Success] Add valid amount discount', async t => {
// Remove any sessions
await g.db.sessions.deleteMany({}, {});
await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(200);
const res = await g.request
.post('/checkout/adddiscountcode')
.send({
discountCode: g.discounts[0].code
})
.expect(200);
t.deepEqual(res.body.message, 'Discount code applied');
// Get our session
const sessions = await g.db.sessions.find({}).toArray();
if(!sessions || sessions.length === 0){
t.fail();
}
// Calculate what we expect
const totalCartAmount = g.products[0].productPrice * 1;
const session = sessions[0].session;
t.deepEqual(session.discountCode, g.discounts[0].code);
t.deepEqual(session.totalCartDiscount, g.discounts[0].value);
t.deepEqual(session.totalCartAmount, totalCartAmount - g.discounts[0].value);
});
test('[Success] Add valid percent discount', async t => {
// Remove any sessions
await g.db.sessions.deleteMany({}, {});
await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(200);
const res = await g.request
.post('/checkout/adddiscountcode')
.send({
discountCode: g.discounts[1].code
})
.expect(200);
t.deepEqual(res.body.message, 'Discount code applied');
// Get our session
const sessions = await g.db.sessions.find({}).toArray();
if(!sessions || sessions.length === 0){
t.fail();
}
// Calculate what we expect - percent
const totalCartAmount = g.products[0].productPrice * 1;
const expectedDiscount = (g.discounts[1].value / 100) * totalCartAmount;
const session = sessions[0].session;
t.deepEqual(session.discountCode, g.discounts[1].code);
t.deepEqual(session.totalCartAmount, totalCartAmount - expectedDiscount);
t.deepEqual(session.totalCartDiscount, expectedDiscount);
});
test('[Fail] Add an expired discount code', async t => {
await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(200);
const res = await g.request
.post('/checkout/adddiscountcode')
.send({
discountCode: g.discounts[2].code
})
.expect(400);
t.deepEqual(res.body.message, 'Discount is expired');
});
test('[Fail] Add a future discount code', async t => {
await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(200);
const res = await g.request
.post('/checkout/adddiscountcode')
.send({
discountCode: g.discounts[3].code
})
.expect(400);
t.deepEqual(res.body.message, 'Discount is expired');
});
test('[Fail] Add a bogus code', async t => {
await g.request
.post('/product/addtocart')
.send({
productId: g.products[0]._id,
productQuantity: 1,
productOptions: JSON.stringify(g.products[0].productOptions)
})
.expect(200);
const res = await g.request
.post('/checkout/adddiscountcode')
.send({
discountCode: 'some_bogus_code'
})
.expect(400);
t.deepEqual(res.body.message, 'Discount code is invalid or expired');
});
test('[Success] Create a new discount code', async t => {
// Add a discount code
const res = await g.request
.post('/admin/settings/discount/create')
.send({
code: 'TEST_CODE_5',
type: 'amount',
value: 10,
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
})
.set('apiKey', g.users[0].apiKey)
.expect(200);
t.deepEqual(res.body.message, 'Discount code created successfully');
});
test('[Fail] Create a new discount code with invalid type', async t => {
// Add a discount code
const res = await g.request
.post('/admin/settings/discount/create')
.send({
code: 'TEST_CODE_1',
type: 'bogus_type',
value: 10,
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
})
.set('apiKey', g.users[0].apiKey)
.expect(400);
t.deepEqual(res.body[0].message, 'should be equal to one of the allowed values');
});
test('[Fail] Create a new discount code with existing code', async t => {
// Add a discount code
const res = await g.request
.post('/admin/settings/discount/create')
.send({
code: 'valid_10_amount_code',
type: 'amount',
value: 10,
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
})
.set('apiKey', g.users[0].apiKey)
.expect(400);
t.deepEqual(res.body.message, 'Discount code already exists');
});
test('[Success] Update a discount code', async t => {
// Add a discount code
const res = await g.request
.post('/admin/settings/discount/update')
.send({
discountId: g.discounts[0]._id,
code: 'TEST_CODE_99',
type: 'amount',
value: 20,
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
})
.set('apiKey', g.users[0].apiKey)
.expect(200);
t.deepEqual(res.body.discount.value, 20);
t.deepEqual(res.body.message, 'Successfully saved');
});
test('[Fail] Update a discount with same code as existing', async t => {
// Add a discount code
const res = await g.request
.post('/admin/settings/discount/update')
.send({
discountId: g.discounts[1]._id,
code: 'TEST_CODE_99',
type: 'amount',
value: 20,
start: moment().add(1, 'days').format('DD/MM/YYYY HH:mm'),
end: moment().add(7, 'days').format('DD/MM/YYYY HH:mm')
})
.set('apiKey', g.users[0].apiKey)
.expect(400);
t.deepEqual(res.body.message, 'Discount code already exists');
});

View File

@ -22,7 +22,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha256-L/W5Wfqfa0sdBNIKN9cG6QA5F2qx4qICmU2VgLruv9Y=" crossorigin="anonymous" />
<link rel="stylesheet" href="/stylesheets/pushy{{config.env}}.css">
{{#if admin}}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/codemirror.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/gijgo/1.9.13/combined/css/gijgo.min.css" integrity="sha256-bH0WSMuCFoG/dxeox/5aOWmaZl729yDg4ylckwSRTfU=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/codemirror.min.css" integrity="sha256-MdzaXfGXzZdeHw/XEV2LNNycipsLk4uZ0FYzO3hbuvI=" crossorigin="anonymous" />
{{/if}}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css" integrity="sha256-+N4/V/SbAFiW1MPBCXnfnP9QSN3+Keu+NlB+0ev/YKQ=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-tokenfield/0.12.0/css/bootstrap-tokenfield.min.css" integrity="sha256-4qBzeX420hElp9/FzsuqUNqVobcClz1BjnXoxUDSYQ0=" crossorigin="anonymous" />
@ -36,6 +37,8 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js" integrity="sha256-x3YZWtRjM8bJqf48dFAv/qmgL68SI4jqNWeSLMZaMGA=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha256-WqU1JavFxSAMcLP2WIOI+GB2zWmShMI82mTpLDcqFUg=" crossorigin="anonymous"></script>
{{#if admin}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js" integrity="sha256-4iQZ6BVL4qNKlQ27TExEhBN1HFPvAvAMbFavKKosSWQ=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gijgo/1.9.13/combined/js/gijgo.min.js" integrity="sha256-YZhUu69bCn9uTqQyKwwQ3GyRypS7eaxp/wmVS282sDI=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/codemirror.min.js" integrity="sha256-K1exjHe1X4MP24jRizgBaSbUDUrNhFDRSwGoEYGmtJE=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/mode/css/css.min.js" integrity="sha256-D5oJ11cOmRhXSYWELwG2U/XYH3YveZJr9taRYLZ2DSM=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.20.2/mode/xml/xml.min.js" integrity="sha256-ERFGS58tayDq5kkyNwd/89iZZ+HglMH7eYXxG1hxTvA=" crossorigin="anonymous"></script>
@ -100,7 +103,7 @@
{{#unless admin}}
{{#ifCond @root.config.enableLanguages "!=" false}}
<div class="dropdown d-none d-sm-block">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-globe-americas"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
@ -134,7 +137,7 @@
<div id="cart" class="col-md-12 pad-left-12 top-pad-10 pushy pushy-right">
<div class="row {{checkout}}">
<div class="col-sm-12 text-right">
<button class="pushy-link btn btn-outline-primary" type="button">X</button>
<button class="pushy-link btn btn-primary" type="button">X</button>
</div>
</div>
<div class="row">
@ -142,8 +145,8 @@
{{> (getTheme 'cart')}}
<div class="row">
<div class="col-sm-12 {{showCartButtons @root.session.cart}} cart-buttons">
<button class="btn btn-outline-danger float-left" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
<a href="/checkout/information" class="btn btn-outline-primary float-right">Checkout</a>
<button class="btn btn-danger float-left" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
<a href="/checkout/information" class="btn btn-primary float-right">Checkout</a>
</div>
</div>
</div>

View File

@ -22,5 +22,8 @@
<li class="list-group-item"><i class="fas fa-cog fa-icon"></i> &nbsp; <a href="/admin/settings">{{ @root.__ "General settings" }}</a></li>
<li class="list-group-item"><i class="fas fa-bars fa-icon"></i> &nbsp; <a href="/admin/settings/menu">Menu</a></li>
<li class="list-group-item"><i class="far fa-file fa-icon"></i> &nbsp; <a href="/admin/settings/pages">{{ @root.__ "Static pages" }}</a></li>
{{#ifCond session.isAdmin '===' true}}
<li class="list-group-item"><i class="fas fa-tag"></i> &nbsp; <a href="/admin/settings/discounts">{{ @root.__ "Discount codes" }}</a></li>
{{/ifCond}}
</ul>
</div>

View File

@ -56,7 +56,7 @@
<div class="input-group">
<input type="text" class="form-control" id="productPermalink" placeholder="Permalink for the article" value={{result.productPermalink}}>
<div class="input-group-append">
<button class="btn btn-outline-success" id="validate_permalink" type="button">{{ @root.__ "Validate" }}</button>
<button class="btn btn-outline-success" id="validatePermalink" type="button">{{ @root.__ "Validate" }}</button>
</div>
</div>
<p class="help-block">{{ @root.__ "This sets a readable URL for the product" }}</p>

View File

@ -1,139 +1,138 @@
{{> partials/menu}}
<form class="form-horizontal" id="productNewForm" data-toggle="validator">
<div class="col-sm-12">
<div class="page-header">
<div class="float-right">
<button id="frm_edit_product_save" class="btn btn-outline-success" type="submit">Add product <i class="far fa-plus-square"></i></button>
<div class="col-sm-12">
<div class="page-header">
<div class="float-right">
<button id="frm_edit_product_save" class="btn btn-outline-success" type="submit">Add product <i class="far fa-plus-square"></i></button>
</div>
<h2>{{ @root.__ "New product" }}</h2>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="productTitle" class="control-label">{{ @root.__ "Product title" }} *</label>
<input type="text" id="productTitle" class="form-control" minlength="5" maxlength="200" value="{{productTitle}}" required/>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="productPrice" class="control-label">{{ @root.__ "Product price" }} *</label>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">{{currencySymbol config.currencySymbol}}</label>
</div>
<h2>{{ @root.__ "New product" }}</h2>
<input type="number" id="productPrice" class="form-control" step="any" value="{{productPrice}}" required/>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="productTitle" class="control-label">{{ @root.__ "Product title" }} *</label>
<input type="text" id="productTitle" class="form-control" minlength="5" maxlength="200" value="{{productTitle}}" required/>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="productPublished" class="control-label">{{ @root.__ "Status" }}</label>
<select class="form-control" id="productPublished">
<option value="true" selected>{{ @root.__ "Published" }}</option>
<option value="false">{{ @root.__ "Draft" }}</option>
</select>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="productPrice" class="control-label">{{ @root.__ "Product price" }} *</label>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">{{currencySymbol config.currencySymbol}}</label>
</div>
{{#if config.trackStock}}
<div class="col-sm-6">
<div class="form-group">
<label for="productStock" class="control-label">{{ @root.__ "Stock level" }}</label>
<input type="number" id="productStock" class="form-control" value="{{productStock}}" step="any" />
</div>
</div>
{{/if}}
<div class="col-sm-10">
<div class="form-group" id="editor-wrapper">
<label for="editor" class="control-label">{{ @root.__ "Product description" }} *</label>
<textarea minlength="5" rows="10" id="productDescription" class="form-control" required>{{productDescription}}</textarea>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label class="control-label">Permalink</label>
<div class="input-group">
<input type="text" class="form-control" id="productPermalink" placeholder="Permalink for the article" value={{productPermalink}}>
<div class="input-group-append">
<button class="btn btn-outline-success" id="validatePermalink" type="button">Validate</button>
</div>
</div>
<p class="help-block">{{ @root.__ "This sets a readable URL for the product" }}</p>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label class="control-label">{{ @root.__ "Product options" }}</label>
<input type="hidden" id="productOptions" value="{{result.productOptions}}" />
<ul class="list-group" id="product_opt_wrapper">
<li class="list-group-item">
<div class="row">
<div class="col-sm-2">
<strong>{{ @root.__ "Name" }}:</strong>
<input type="text" id="product_optName" class="form-control" placeholder="Size" />
</div>
<input type="number" id="productPrice" class="form-control" step="any" value="{{productPrice}}" required/>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="productPublished" class="control-label">{{ @root.__ "Status" }}</label>
<select class="form-control" id="productPublished">
<option value="true" selected>{{ @root.__ "Published" }}</option>
<option value="false">{{ @root.__ "Draft" }}</option>
</select>
</div>
</div>
{{#if config.trackStock}}
<div class="col-sm-6">
<div class="form-group">
<label for="productStock" class="control-label">{{ @root.__ "Stock level" }}</label>
<input type="number" id="productStock" class="form-control" value="{{productStock}}" step="any" />
</div>
</div>
{{/if}}
<div class="col-sm-10">
<div class="form-group" id="editor-wrapper">
<label for="editor" class="control-label">{{ @root.__ "Product description" }} *</label>
<textarea minlength="5" rows="10" id="productDescription" class="form-control" required>{{productDescription}}</textarea>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label class="control-label">Permalink</label>
<div class="input-group">
<input type="text" class="form-control" id="productPermalink" placeholder="Permalink for the article" value={{productPermalink}}>
<div class="input-group-append">
<button class="btn btn-outline-success" id="validate_permalink" type="button">Validate</button>
<div class="col-sm-2">
<strong>{{ @root.__ "Label" }}:</strong>
<input type="text" id="product_optLabel" class="form-control" placeholder="Select size"/>
</div>
</div>
<p class="help-block">{{ @root.__ "This sets a readable URL for the product" }}</p>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label class="control-label">{{ @root.__ "Product options" }}</label>
<input type="hidden" id="productOptions" value="{{result.productOptions}}" />
<ul class="list-group" id="product_opt_wrapper">
<div class="col-sm-2">
<strong>{{ @root.__ "Type" }}:</strong>
<select id="product_optType" class="form-control">
<option value="select">Select</option>
<option value="radio">Radio</option>
<option value="checkbox">Checkbox</option>
</select>
</div>
<div class="col-sm-4">
<strong>{{ @root.__ "Options" }}:</strong>
<input type="text" id="product_optOptions" class="form-control" placeholder="comma, seporated, list"/>
</div>
<div class="col-sm-2 text-right"></br>
<button id="product_opt_add" class="btn btn-outline-success">{{ @root.__ "Add" }}</button>
</div></div>
</li>
{{#each options}}
<li class="list-group-item">
<div class="row">
<div class="col-sm-2">
<strong>{{ @root.__ "Name" }}:</strong>
<input type="text" id="product_optName" class="form-control" placeholder="Size" />
</div>
<div class="col-sm-2">
<strong>{{ @root.__ "Label" }}:</strong>
<input type="text" id="product_optLabel" class="form-control" placeholder="Select size"/>
</div>
<div class="col-sm-2">
<strong>{{ @root.__ "Type" }}:</strong>
<select id="product_optType" class="form-control">
<option value="select">Select</option>
<option value="radio">Radio</option>
<option value="checkbox">Checkbox</option>
</select>
</div>
<div class="col-sm-4">
<strong>{{ @root.__ "Options" }}:</strong>
<input type="text" id="product_optOptions" class="form-control" placeholder="comma, seporated, list"/>
</div>
<div class="col-sm-2 text-right"></br>
<button id="product_opt_add" class="btn btn-outline-success">{{ @root.__ "Add" }}</button>
</div></div>
</li>
{{#each options}}
<li class="list-group-item">
<div class="row">
<div class='col-sm-2 opt-name'>{{this.optName}}</div>
<div class='col-sm-2'>{{this.optLabel}}</div>
<div class='col-sm-2'>{{this.optType}}</div>
<div class='col-sm-4'>{{{this.optOptions}}}</div>
<div class='col-sm-2 text-right'>
<button class='product_opt_remove btn btn-outline-danger'>{{ @root.__ "Remove" }}</button>
</div>
<div class='col-sm-2 opt-name'>{{this.optName}}</div>
<div class='col-sm-2'>{{this.optLabel}}</div>
<div class='col-sm-2'>{{this.optType}}</div>
<div class='col-sm-4'>{{{this.optOptions}}}</div>
<div class='col-sm-2 text-right'>
<button class='product_opt_remove btn btn-outline-danger'>{{ @root.__ "Remove" }}</button>
</div>
</li>
{{/each}}
</ul>
<p class="help-block">{{ @root.__ "Here you can set options for your product. Eg: Size, color, style" }}</p>
</div>
</div>
</li>
{{/each}}
</ul>
<p class="help-block">{{ @root.__ "Here you can set options for your product. Eg: Size, color, style" }}</p>
</div>
{{#ifCond config.paymentGateway '==' 'stripe'}}
<div class="col-sm-10">
<div class="form-group">
<label class="control-label">Subscription plan</label>
<input type="text" class="form-control" id="productSubscription" placeholder="plan_XXXXXXXXXXXXXX">
<p class="help-block">First setup the plan in <strong>Stripe</strong> dashboard and enter the Plan ID. Format: plan_XXXXXXXXXXXXXX</p>
</div>
</div>
{{#ifCond config.paymentGateway '==' 'stripe'}}
<div class="col-sm-10">
<div class="form-group">
<label class="control-label">Subscription plan</label>
<input type="text" class="form-control" id="productSubscription" placeholder="plan_XXXXXXXXXXXXXX">
<p class="help-block">First setup the plan in <strong>Stripe</strong> dashboard and enter the Plan ID. Format: plan_XXXXXXXXXXXXXX</p>
</div>
{{/ifCond}}
<div class="col-sm-10">
<div class="form-group">
<label for="productComment" class="control-label">{{ @root.__ "Allow comment" }}</label>
<div class="checkbox">
<label>
<input class="productComment" type="checkbox" {{checkedState result.productComment}} id="productComment">
</label>
</div>
<p class="help-block">{{ @root.__ "Allow free form comments when adding products to cart" }}</p>
</div>
{{/ifCond}}
<div class="col-sm-10">
<div class="form-group">
<label for="productComment" class="control-label">{{ @root.__ "Allow comment" }}</label>
<div class="checkbox">
<label>
<input class="productComment" type="checkbox" {{checkedState result.productComment}} id="productComment">
</label>
</div>
<p class="help-block">{{ @root.__ "Allow free form comments when adding products to cart" }}</p>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="productTags" class="control-label">Product tag words</label>
<input type="text" class="form-control" id="productTags">
<p class="help-block">{{ @root.__ "Tag words used to indexed products, making them easier to find and filter." }}</p>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="productTags" class="control-label">Product tag words</label>
<input type="text" class="form-control" id="productTags">
<p class="help-block">{{ @root.__ "Tag words used to indexed products, making them easier to find and filter." }}</p>
</div>
</div>
</form>

View File

@ -26,7 +26,7 @@
{{#each results}}
<li class="list-group-item">
<button class="float-right btn text-danger btn-delete-product" data-id="{{this._id}}"> <i class="far fa-trash-alt"></i></button>
<h4 class="float-right"><input id="{{this._id}}" class="published_state" type="checkbox" {{checkedState this.productPublished}}></h4>
<h4 class="float-right"><input id="{{this._id}}" class="publishedState" type="checkbox" {{checkedState this.productPublished}}></h4>
<div class="top-pad-8"><a href="/admin/product/edit/{{this._id}}">{{this.productTitle}}</a></div>
</li>
{{/each}}

View File

@ -0,0 +1,47 @@
{{> partials/menu}}
<form class="form-horizontal col-sm-7" id="discountEditForm" data-toggle="validator">
<div class="row">
<div class="col-sm-12">
<div class="page-header">
<div class="float-right">
<button id="" class="btn btn-outline-success" type="submit">Update discount</button>
</div>
<h2>{{ @root.__ "Edit discount" }}</h2>
</div>
</div>
<div class="col-10">
<div class="form-group">
<label for="discountCode" class="control-label">{{ @root.__ "Discount code" }} *</label>
<input type="text" id="discountCode" class="form-control" minlength="1" maxlength="50" value="{{discount.code}}" required/>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="discountType" class="control-label">{{ @root.__ "Discount type" }} *</label>
<select class="form-control" id="discountType">
<option value="amount" {{selectState discount.type "amount"}}>{{ @root.__ "Amount" }}</option>
<option value="percent" {{selectState discount.type "percent"}}>{{ @root.__ "Percent" }}</option>
</select>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="discountValue" class="control-label">{{ @root.__ "Discount value" }} *</label>
<input type="number" id="discountValue" class="form-control" minlength="1" maxlength="50" value="{{discount.value}}" required/>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="discountStart" class="control-label">{{ @root.__ "Discount start" }} *</label>
<input id="discountStart" value="{{formatDate discount.start 'DD/MM/YYYY kk:mm'}}" />
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="discountEnd" class="control-label">{{ @root.__ "Discount end" }} *</label>
<input id="discountEnd" value="{{formatDate discount.end 'DD/MM/YYYY kk:mm'}}" />
</div>
</div>
<input type="hidden" id="discountId" value="{{discount._id}}">
</div>
</form>

View File

@ -0,0 +1,46 @@
{{> partials/menu}}
<form class="form-horizontal col-sm-7" id="discountNewForm" data-toggle="validator">
<div class="row">
<div class="col-sm-12">
<div class="page-header">
<div class="float-right">
<button id="" class="btn btn-outline-success" type="submit">Add discount</button>
</div>
<h2>{{ @root.__ "New discount" }}</h2>
</div>
</div>
<div class="col-10">
<div class="form-group">
<label for="discountCode" class="control-label">{{ @root.__ "Discount code" }} *</label>
<input type="text" id="discountCode" class="form-control" minlength="1" maxlength="50" placeholder="CODE20" required/>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="discountType" class="control-label">{{ @root.__ "Discount type" }} *</label>
<select class="form-control" id="discountType">
<option value="amount" selected>{{ @root.__ "Amount" }}</option>
<option value="percent">{{ @root.__ "Percent" }}</option>
</select>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="discountValue" class="control-label">{{ @root.__ "Discount value" }} *</label>
<input type="number" id="discountValue" class="form-control" minlength="1" maxlength="50" placeholder="20" required/>
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="discountStart" class="control-label">{{ @root.__ "Discount start" }} *</label>
<input id="discountStart" />
</div>
</div>
<div class="col-sm-10">
<div class="form-group">
<label for="discountEnd" class="control-label">{{ @root.__ "Discount end" }} *</label>
<input id="discountEnd" />
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,39 @@
{{> partials/menu}}
<div class="col-sm-9">
<div class="row">
<div class="col-md-10">
<h2 class="clearfix">{{ @root.__ "Discount codes" }} <div class="float-right"><a href="/admin/settings/discount/new" class="btn btn-outline-success">{{ @root.__ "New Discount" }}</a></div></h2>
{{#if discounts}}
<ul class="list-group">
{{#each discounts}}
<li class="list-group-item">
<div class="row">
<div class="col-4 mt-2">
<span><strong>{{ @root.__ "Code" }}:</strong>&nbsp; {{this.code}}</span>
</div>
<div class="col-2 mt-2">
<span><strong>{{ @root.__ "Type" }}:</strong>&nbsp; {{this.type}}</span>
</div>
<div class="col-3 mt-2">
<span><strong>{{ @root.__ "Status" }}:</strong>
{{#ifCond (discountExpiry this.start this.end) '===' true}}
<span class="text-success">{{ @root.__ "Running" }}</span>
{{else}}
<span class="text-danger">{{ @root.__ "Not running" }}</span>
{{/ifCond}}
</span>
</div>
<div class="col-3 text-right">
<a class="btn btn-outline-success" href="/admin/settings/discount/edit/{{this._id}}">{{ @root.__ "Edit" }}</a>
<button class="btn btn-outline-danger" id="btnDiscountDelete" data-id="{{this._id}}">{{ @root.__ "Delete" }}</button>
</div>
</div>
</li>
{{/each}}
</ul>
{{else}}
<h4 class="text-warning text-center">{{ @root.__ "There are currently no discount codes setup." }}</h4>
{{/if}}
</div>
</div>
</div>

View File

@ -29,16 +29,16 @@
<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="{{../this.productId}}-qty" data-id="{{../this.productId}}" maxlength="2" value="{{../this.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="{{../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}}" type="button"><i class="far fa-trash-alt" data-id="{{../this.productId}}" aria-hidden="true"></i></button>
</div>
{{else}}
<div class="col-12 col-md-8 no-pad-left mb-2"></div>
@ -66,6 +66,11 @@
<span id="shipping-amount">{{@root.session.shippingMessage}}</span>
</div>
{{/ifCond}}
{{#ifCond @root.session.totalCartDiscount '>' 0}}
<div class="text-right">
Discount: <strong id="discount-amount">{{currencySymbol @root.config.currencySymbol}}{{formatAmount @root.session.totalCartDiscount}}</strong>
</div>
{{/ifCond}}
<div class="text-right">
Total:
<strong id="total-cart-amount">{{currencySymbol @root.config.currencySymbol}}{{formatAmount @root.session.totalCartAmount}}</strong>

View File

@ -4,8 +4,8 @@
{{> (getTheme 'cart')}}
<div class="row">
<div class="col-sm-12 {{showCartButtons @root.session.cart}} cart-buttons">
<button class="btn btn-outline-danger float-left" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
<a href="/checkout/information" class="btn btn-outline-danger float-right">Checkout</a>
<button class="btn btn-danger float-left" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
<a href="/checkout/information" class="btn btn-danger float-right">Checkout</a>
</div>
</div>
</div>

View File

@ -36,10 +36,10 @@
</div>
<div class="form-group">
<a href="/customer/forgotten"
class="btn btn-outline-primary float-left">{{ @root.__ "Forgotten" }}</a>
class="btn btn-primary float-left">{{ @root.__ "Forgotten" }}</a>
</div>
<div class="form-group">
<button id="customerLogin" class="btn btn-outline-primary float-right"
<button id="customerLogin" class="btn btn-primary float-right"
type="submit">Login</button>
</div>
</div>
@ -49,7 +49,7 @@
<div class="row bottom-marg-15">
<div class="col-sm-12">
<button id="customerLogout"
class="btn btn-outline-primary float-right">{{ @root.__ "Change customer" }}</button>
class="btn btn-primary float-right">{{ @root.__ "Change customer" }}</button>
</div>
</div>
{{/if}}
@ -76,8 +76,8 @@
</div>
{{/unless}}
<div class="col-sm-12">
<a href="/checkout/cart" class="btn btn-outline-primary float-left">Return to cart</a>
<a href="/checkout/shipping" id="checkoutInformation" class="btn btn-outline-primary float-right">Continue to shipping</a>
<a href="/checkout/cart" class="btn btn-primary float-left">Return to cart</a>
<a href="/checkout/shipping" id="checkoutInformation" class="btn btn-primary float-right">Continue to shipping</a>
</div>
</div>
</form>
@ -89,7 +89,7 @@
{{#if @root.session.cart}}
<div class="row">
<div class="col-sm-12 {{showCartButtons @root.session.cart}} cart-buttons">
<button class="btn btn-outline-danger float-right" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
<button class="btn btn-danger float-right" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
</div>
{{/if}}
</div>

View File

@ -39,6 +39,17 @@
<li class="list-group-item">FREE shipping <span class="float-right"><a href="/checkout/shipping">Change</a></span></li>
{{/ifCond}}
</ul>
{{#if @root.config.modules.loaded.discount}}
<div class="input-group bottom-pad-15">
<input class="form-control" id="discountCode" type="search" placeholder="{{ @root.__ "Discount code" }}" value="{{@root.session.discountCode}}">
<div class="input-group-append">
<button class="btn btn-outline-success" id="addDiscountCode">{{ @root.__ "Apply" }}</button>
</div>
<div class="input-group-append">
<button class="btn btn-outline-danger" id="removeDiscountCode"><i class="fa fa-times" aria-hidden="true"></i></button>
</div>
</div>
{{/if}}
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action{{@root.paymentType}}" method="post" role="form" data-toggle="validator" novalidate="false">
{{#if session.customerPresent}}
{{#ifCond config.paymentGateway '==' 'paypal'}}

View File

@ -23,14 +23,14 @@
{{/unless}}
</div>
</div>
<a href="/checkout/information" class="btn btn-outline-primary float-left">{{ @root.__ "Return to information" }}</a>
<a href="/checkout/payment" class="btn btn-outline-primary float-right">{{ @root.__ "Proceed to payment" }}</a>
<a href="/checkout/information" class="btn btn-primary float-left">{{ @root.__ "Return to information" }}</a>
<a href="/checkout/payment" class="btn btn-primary float-right">{{ @root.__ "Proceed to payment" }}</a>
</div>
<div id="cart" class="col-md-7 d-none d-sm-block">
{{> (getTheme 'cart')}}
<div class="row">
<div class="col-sm-12 {{showCartButtons @root.session.cart}} cart-buttons">
<button class="btn btn-outline-danger float-right" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
<button class="btn btn-danger float-right" id="empty-cart" type="button">{{ @root.__ "Empty cart" }}</button>
</div>
</div>
</div>

View File

@ -51,7 +51,7 @@
{{currencySymbol ../config.currencySymbol}}{{formatAmount productPrice}}
</h3>
<p class="text-center">
<a class="btn btn-outline-primary add-to-cart" data-id="{{this._id}}" data-link="{{this.productPermalink}}" data-has-options="{{checkProductOptions this.productOptions}}" role="button">{{ @root.__ "Add to cart" }}</a>
<a class="btn btn-primary add-to-cart" data-id="{{this._id}}" data-link="{{this.productPermalink}}" data-has-options="{{checkProductOptions this.productOptions}}" role="button">{{ @root.__ "Add to cart" }}</a>
</p>
</div>
</div>

View File

@ -74,11 +74,11 @@
<p class="product-option-text">{{ @root.__ "Quantity" }}</p>
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-primary qty-btn-minus" type="button">-</button>
<button class="btn btn-primary qty-btn-minus" type="button">-</button>
</div>
<input type="number" class="form-control add-color text-center" id="product_quantity" maxlength="3" value="1">
<div class="input-group-append">
<button class="btn btn-outline-primary qty-btn-plus" type="button">+</button>
<button class="btn btn-primary qty-btn-plus" type="button">+</button>
</div>
</div>
</div>
@ -89,7 +89,7 @@
</div>
{{/if}}
<div class="col-md-10 btnAddToCart">
<button class="btn btn-outline-primary btn-block product-add-to-cart" type="button">{{ @root.__ "Add to cart" }}</button>
<button class="btn btn-primary btn-block product-add-to-cart" type="button">{{ @root.__ "Add to cart" }}</button>
</div>
<div class="col-md-10 body_text">
{{{productDescription}}}