Adding subscriptions (#95)
* Adding subscriptions * Adding in ability to use webhook for subscriptions * Add docs and ability to create subscriptions * Fixed populating value * Adding subscription tests * Language updatemaster
parent
c92194dd56
commit
a87d2fbf0a
|
@ -118,6 +118,13 @@ Note: An `Options` value is not required when `Type` is set to `Checkbox`.
|
||||||
|
|
||||||
Tags are used when indexing the products for search. It's advised to set tags (keywords) so that customers can easily find the products they are searching for.
|
Tags are used when indexing the products for search. It's advised to set tags (keywords) so that customers can easily find the products they are searching for.
|
||||||
|
|
||||||
|
## Subscriptions (Stripe only)
|
||||||
|
|
||||||
|
You are able to setup product subscriptions through Stripe. First setup the `Plan` in the [Stripe dashboard](https://dashboard.stripe.com/) then enter the Plan ID (Formatted: plan_XXXXXXXXXXXXXX) when creating or editing a product. When purchasing, a customer can only add a single subscription to their cart at one time. Subscriptions cannot be combined with other products in their cart. On Checkout/Payment the customer and subscription is created in Stripe and the billing cycle commences based on the plan setup.
|
||||||
|
|
||||||
|
##### Subscription Webhooks (Stripe only)
|
||||||
|
You are able to configure a Webhook in Stripe to receive subscription updates on successful/failed payments [here](https://dashboard.stripe.com/webhooks). The `expressCart` Webhook endpoint should be set to: `https://<example.com>/stripe/subscription_update`. You will need to set the `Events to send` value to both: `invoice.payment_failed` and `invoice.payment_succeeded`.
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
`expressCart` uses a MongoDB for storing all the data. Setting of the database connection string is done through the `/config/settings.json` file. There are two properties relating to the database connection:
|
`expressCart` uses a MongoDB for storing all the data. Setting of the database connection string is done through the `/config/settings.json` file. There are two properties relating to the database connection:
|
||||||
|
|
|
@ -69,6 +69,17 @@
|
||||||
"productTags" : "shirt",
|
"productTags" : "shirt",
|
||||||
"productOptions" : "{\"Size\":{\"optName\":\"Size\",\"optLabel\":\"Select size\",\"optType\":\"select\",\"optOptions\":[\"S\",\"M\",\"L\"]}}",
|
"productOptions" : "{\"Size\":{\"optName\":\"Size\",\"optLabel\":\"Select size\",\"optType\":\"select\",\"optOptions\":[\"S\",\"M\",\"L\"]}}",
|
||||||
"productStock": 10
|
"productStock": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"productPermalink" : "gym-membership",
|
||||||
|
"productTitle" : "Gym membership",
|
||||||
|
"productPrice" : "15",
|
||||||
|
"productDescription" : "This a monthly recurring Gym membership subscription.",
|
||||||
|
"productPublished": true,
|
||||||
|
"productTags" : "subscription",
|
||||||
|
"productOptions" : "",
|
||||||
|
"productStock": null,
|
||||||
|
"productSubscription": "plan_XXXXXXXXXXXXXX"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"customers": [
|
"customers": [
|
||||||
|
|
|
@ -131,6 +131,22 @@ const updateTotalCartAmount = (req, res) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSubscriptionCheck = (req, res) => {
|
||||||
|
// If cart is empty
|
||||||
|
if(!req.session.cart || req.session.cart.length === 0){
|
||||||
|
req.session.cartSubscription = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.cart.forEach((item) => {
|
||||||
|
if(item.productSubscription){
|
||||||
|
req.session.cartSubscription = item.productSubscription;
|
||||||
|
}else{
|
||||||
|
req.session.cartSubscription = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const checkDirectorySync = (directory) => {
|
const checkDirectorySync = (directory) => {
|
||||||
try{
|
try{
|
||||||
fs.statSync(directory);
|
fs.statSync(directory);
|
||||||
|
@ -551,6 +567,7 @@ module.exports = {
|
||||||
addSitemapProducts,
|
addSitemapProducts,
|
||||||
clearSessionValue,
|
clearSessionValue,
|
||||||
updateTotalCartAmount,
|
updateTotalCartAmount,
|
||||||
|
updateSubscriptionCheck,
|
||||||
checkDirectorySync,
|
checkDirectorySync,
|
||||||
getThemes,
|
getThemes,
|
||||||
getImages,
|
getImages,
|
||||||
|
|
|
@ -160,5 +160,6 @@
|
||||||
"Cart contents": "Cart contents",
|
"Cart contents": "Cart contents",
|
||||||
"Shipping": "Shipping:",
|
"Shipping": "Shipping:",
|
||||||
"Empty cart": "Empty cart",
|
"Empty cart": "Empty cart",
|
||||||
"List": "List"
|
"List": "List",
|
||||||
}
|
"Order type": "Order type"
|
||||||
|
}
|
|
@ -289,6 +289,9 @@ $(document).ready(function (){
|
||||||
image: $('#stripeButton').data('image'),
|
image: $('#stripeButton').data('image'),
|
||||||
locale: 'auto',
|
locale: 'auto',
|
||||||
token: function(token){
|
token: function(token){
|
||||||
|
if($('#stripeButton').data('subscription')){
|
||||||
|
$('#shipping-form').append('<input type="hidden" name="stripePlan" value="' + $('#stripeButton').data('subscription') + '" />');
|
||||||
|
}
|
||||||
$('#shipping-form').append('<input type="hidden" name="stripeToken" value="' + token.id + '" />');
|
$('#shipping-form').append('<input type="hidden" name="stripeToken" value="' + token.id + '" />');
|
||||||
$('#shipping-form').submit();
|
$('#shipping-form').submit();
|
||||||
}
|
}
|
||||||
|
@ -301,7 +304,8 @@ $(document).ready(function (){
|
||||||
description: $('#stripeButton').data('description'),
|
description: $('#stripeButton').data('description'),
|
||||||
zipCode: $('#stripeButton').data('zipCode'),
|
zipCode: $('#stripeButton').data('zipCode'),
|
||||||
amount: $('#stripeButton').data('amount'),
|
amount: $('#stripeButton').data('amount'),
|
||||||
currency: $('#stripeButton').data('currency')
|
currency: $('#stripeButton').data('currency'),
|
||||||
|
subscription: $('#stripeButton').data('subscription')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -13,6 +13,7 @@ const {
|
||||||
getPaymentConfig,
|
getPaymentConfig,
|
||||||
getImages,
|
getImages,
|
||||||
updateTotalCartAmount,
|
updateTotalCartAmount,
|
||||||
|
updateSubscriptionCheck,
|
||||||
getData,
|
getData,
|
||||||
addSitemapProducts
|
addSitemapProducts
|
||||||
} = require('../lib/common');
|
} = require('../lib/common');
|
||||||
|
@ -68,6 +69,10 @@ router.get('/payment/:orderId', async (req, res, next) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/emptycart', async (req, res, next) => {
|
||||||
|
emptyCart(req, res, '');
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/checkout', async (req, res, next) => {
|
router.get('/checkout', async (req, res, next) => {
|
||||||
const config = req.app.config;
|
const config = req.app.config;
|
||||||
|
|
||||||
|
@ -105,6 +110,11 @@ router.get('/pay', async (req, res, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let paymentType = '';
|
||||||
|
if(req.session.cartSubscription){
|
||||||
|
paymentType = '_subscription';
|
||||||
|
}
|
||||||
|
|
||||||
// render the payment page
|
// render the payment page
|
||||||
res.render(`${config.themeViews}pay`, {
|
res.render(`${config.themeViews}pay`, {
|
||||||
title: 'Pay',
|
title: 'Pay',
|
||||||
|
@ -113,6 +123,7 @@ router.get('/pay', async (req, res, next) => {
|
||||||
pageCloseBtn: showCartCloseBtn('pay'),
|
pageCloseBtn: showCartCloseBtn('pay'),
|
||||||
session: req.session,
|
session: req.session,
|
||||||
paymentPage: true,
|
paymentPage: true,
|
||||||
|
paymentType,
|
||||||
page: 'pay',
|
page: 'pay',
|
||||||
message: clearSessionValue(req.session, 'message'),
|
message: clearSessionValue(req.session, 'message'),
|
||||||
messageType: clearSessionValue(req.session, 'messageType'),
|
messageType: clearSessionValue(req.session, 'messageType'),
|
||||||
|
@ -220,6 +231,9 @@ router.post('/product/updatecart', (req, res, next) => {
|
||||||
// update total cart amount
|
// update total cart amount
|
||||||
updateTotalCartAmount(req, res);
|
updateTotalCartAmount(req, res);
|
||||||
|
|
||||||
|
// Update checking cart for subscription
|
||||||
|
updateSubscriptionCheck(req, res);
|
||||||
|
|
||||||
// Update cart to the DB
|
// Update cart to the DB
|
||||||
await db.cart.updateOne({ sessionId: req.session.id }, {
|
await db.cart.updateOne({ sessionId: req.session.id }, {
|
||||||
$set: { cart: req.session.cart }
|
$set: { cart: req.session.cart }
|
||||||
|
@ -239,36 +253,42 @@ router.post('/product/updatecart', (req, res, next) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove single product from cart
|
// Remove single product from cart
|
||||||
router.post('/product/removefromcart', (req, res, next) => {
|
router.post('/product/removefromcart', async (req, res, next) => {
|
||||||
const db = req.app.db;
|
const db = req.app.db;
|
||||||
let itemRemoved = false;
|
let itemRemoved = false;
|
||||||
|
|
||||||
// remove item from cart
|
// remove item from cart
|
||||||
async.each(req.session.cart, (item, callback) => {
|
req.session.cart.forEach((item) => {
|
||||||
if(item){
|
if(item){
|
||||||
if(item.productId === req.body.cartId){
|
if(item.productId === req.body.cartId){
|
||||||
itemRemoved = true;
|
itemRemoved = true;
|
||||||
req.session.cart = _.pull(req.session.cart, item);
|
req.session.cart = _.pull(req.session.cart, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback();
|
|
||||||
}, async () => {
|
|
||||||
// Update cart in DB
|
|
||||||
await db.cart.updateOne({ sessionId: req.session.id }, {
|
|
||||||
$set: { cart: req.session.cart }
|
|
||||||
});
|
|
||||||
// update total cart amount
|
|
||||||
updateTotalCartAmount(req, res);
|
|
||||||
|
|
||||||
if(itemRemoved === false){
|
|
||||||
return res.status(400).json({ message: 'Product not found in cart' });
|
|
||||||
}
|
|
||||||
return res.status(200).json({ message: 'Product successfully removed', totalCartItems: Object.keys(req.session.cart).length });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update cart in DB
|
||||||
|
await db.cart.updateOne({ sessionId: req.session.id }, {
|
||||||
|
$set: { cart: req.session.cart }
|
||||||
|
});
|
||||||
|
// update total cart amount
|
||||||
|
updateTotalCartAmount(req, res);
|
||||||
|
|
||||||
|
// Update checking cart for subscription
|
||||||
|
updateSubscriptionCheck(req, res);
|
||||||
|
|
||||||
|
if(itemRemoved === false){
|
||||||
|
return res.status(400).json({ message: 'Product not found in cart' });
|
||||||
|
}
|
||||||
|
return res.status(200).json({ message: 'Product successfully removed', totalCartItems: Object.keys(req.session.cart).length });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Totally empty the cart
|
// Totally empty the cart
|
||||||
router.post('/product/emptycart', async (req, res, next) => {
|
router.post('/product/emptycart', async (req, res, next) => {
|
||||||
|
emptyCart(req, res, 'json');
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyCart = async (req, res, type) => {
|
||||||
const db = req.app.db;
|
const db = req.app.db;
|
||||||
|
|
||||||
// Remove from session
|
// Remove from session
|
||||||
|
@ -280,8 +300,20 @@ router.post('/product/emptycart', async (req, res, next) => {
|
||||||
|
|
||||||
// update total cart amount
|
// update total cart amount
|
||||||
updateTotalCartAmount(req, res);
|
updateTotalCartAmount(req, res);
|
||||||
res.status(200).json({ message: 'Cart successfully emptied', totalCartItems: 0 });
|
|
||||||
});
|
// Update checking cart for subscription
|
||||||
|
updateSubscriptionCheck(req, res);
|
||||||
|
|
||||||
|
// If POST, return JSON else redirect nome
|
||||||
|
if(type === 'json'){
|
||||||
|
res.status(200).json({ message: 'Cart successfully emptied', totalCartItems: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.message = 'Cart successfully emptied.';
|
||||||
|
req.session.messageType = 'success';
|
||||||
|
res.redirect('/');
|
||||||
|
};
|
||||||
|
|
||||||
// Add item to cart
|
// Add item to cart
|
||||||
router.post('/product/addtocart', async (req, res, next) => {
|
router.post('/product/addtocart', async (req, res, next) => {
|
||||||
|
@ -300,13 +332,25 @@ router.post('/product/addtocart', async (req, res, next) => {
|
||||||
req.session.cart = [];
|
req.session.cart = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the item from the DB
|
// Get the product from the DB
|
||||||
const product = await db.products.findOne({ _id: getId(req.body.productId) });
|
const product = await db.products.findOne({ _id: getId(req.body.productId) });
|
||||||
// No product found
|
// No product found
|
||||||
if(!product){
|
if(!product){
|
||||||
return res.status(400).json({ message: 'Error updating cart. Please try again.' });
|
return res.status(400).json({ message: 'Error updating cart. Please try again.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If cart already has a subscription you cannot add anything else
|
||||||
|
if(req.session.cartSubscription){
|
||||||
|
return res.status(400).json({ message: 'Subscription already existing in cart. You cannot add more.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If existing cart isn't empty check if product is a subscription
|
||||||
|
if(req.session.cart.length !== 0){
|
||||||
|
if(product.productSubscription){
|
||||||
|
return res.status(400).json({ message: 'You cannot combine scubscription products with existing in your cart. Empty your cart and try again.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If stock management on check there is sufficient stock for this product
|
// If stock management on check there is sufficient stock for this product
|
||||||
if(config.trackStock && product.productStock){
|
if(config.trackStock && product.productStock){
|
||||||
const stockHeld = await db.cart.aggregate(
|
const stockHeld = await db.cart.aggregate(
|
||||||
|
@ -346,7 +390,13 @@ router.post('/product/addtocart', async (req, res, next) => {
|
||||||
// Doc used to test if existing in the cart with the options. If not found, we add new.
|
// Doc used to test if existing in the cart with the options. If not found, we add new.
|
||||||
let options = {};
|
let options = {};
|
||||||
if(req.body.productOptions){
|
if(req.body.productOptions){
|
||||||
options = JSON.parse(req.body.productOptions);
|
try{
|
||||||
|
if(typeof req.body.productOptions === 'object'){
|
||||||
|
options = req.body.productOptions;
|
||||||
|
}else{
|
||||||
|
options = JSON.parse(req.body.productOptions);
|
||||||
|
}
|
||||||
|
}catch(ex){}
|
||||||
}
|
}
|
||||||
const findDoc = {
|
const findDoc = {
|
||||||
productId: req.body.productId,
|
productId: req.body.productId,
|
||||||
|
@ -376,6 +426,7 @@ router.post('/product/addtocart', async (req, res, next) => {
|
||||||
productObj.options = options;
|
productObj.options = options;
|
||||||
productObj.productImage = product.productImage;
|
productObj.productImage = product.productImage;
|
||||||
productObj.productComment = productComment;
|
productObj.productComment = productComment;
|
||||||
|
productObj.productSubscription = product.productSubscription;
|
||||||
if(product.productPermalink){
|
if(product.productPermalink){
|
||||||
productObj.link = product.productPermalink;
|
productObj.link = product.productPermalink;
|
||||||
}else{
|
}else{
|
||||||
|
@ -394,6 +445,13 @@ router.post('/product/addtocart', async (req, res, next) => {
|
||||||
// update total cart amount
|
// update total cart amount
|
||||||
updateTotalCartAmount(req, res);
|
updateTotalCartAmount(req, res);
|
||||||
|
|
||||||
|
// Update checking cart for subscription
|
||||||
|
updateSubscriptionCheck(req, res);
|
||||||
|
|
||||||
|
if(product.productSubscription){
|
||||||
|
req.session.cartSubscription = product.productSubscription;
|
||||||
|
}
|
||||||
|
|
||||||
// update how many products in the shopping cart
|
// update how many products in the shopping cart
|
||||||
req.session.cartTotalItems = req.session.cart.reduce((a, b) => +a + +b.quantity, 0);
|
req.session.cartTotalItems = req.session.cart.reduce((a, b) => +a + +b.quantity, 0);
|
||||||
return res.status(200).json({ message: 'Cart successfully updated', totalCartItems: req.session.cartTotalItems });
|
return res.status(200).json({ message: 'Cart successfully updated', totalCartItems: req.session.cartTotalItems });
|
||||||
|
|
|
@ -52,7 +52,8 @@ router.post('/checkout_action', (req, res, next) => {
|
||||||
orderComment: req.body.orderComment,
|
orderComment: req.body.orderComment,
|
||||||
orderStatus: paymentStatus,
|
orderStatus: paymentStatus,
|
||||||
orderDate: new Date(),
|
orderDate: new Date(),
|
||||||
orderProducts: req.session.cart
|
orderProducts: req.session.cart,
|
||||||
|
orderType: 'Single'
|
||||||
};
|
};
|
||||||
|
|
||||||
// insert order into DB
|
// insert order into DB
|
||||||
|
@ -111,4 +112,149 @@ router.post('/checkout_action', (req, res, next) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscription hook from Stripe
|
||||||
|
router.all('/subscription_update', async (req, res, next) => {
|
||||||
|
const db = req.app.db;
|
||||||
|
|
||||||
|
if(!req.body.data.object.customer){
|
||||||
|
return res.status(400).json({ message: 'Customer not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = await db.orders.findOne({
|
||||||
|
orderCustomer: req.body.data.object.customer,
|
||||||
|
orderType: 'Subscription'
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!order){
|
||||||
|
return res.status(400).json({ message: 'Order not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let orderStatus = 'Paid';
|
||||||
|
if(req.body.type === 'invoice.payment_failed'){
|
||||||
|
orderStatus = 'Declined';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update order status
|
||||||
|
await db.orders.updateOne({
|
||||||
|
_id: common.getId(order._id),
|
||||||
|
orderType: 'Subscription'
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
orderStatus: orderStatus
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: 'Status successfully updated' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/checkout_action_subscription', async (req, res, next) => {
|
||||||
|
const db = req.app.db;
|
||||||
|
const config = req.app.config;
|
||||||
|
|
||||||
|
try{
|
||||||
|
const plan = await stripe.plans.retrieve(req.body.stripePlan);
|
||||||
|
if(!plan){
|
||||||
|
req.session.messageType = 'danger';
|
||||||
|
req.session.message = 'The plan connected to this product doesn\'t exist';
|
||||||
|
res.redirect('/pay/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}catch(ex){
|
||||||
|
req.session.messageType = 'danger';
|
||||||
|
req.session.message = 'The plan connected to this product doesn\'t exist';
|
||||||
|
res.redirect('/pay/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create customer
|
||||||
|
const customer = await stripe.customers.create({
|
||||||
|
source: req.body.stripeToken,
|
||||||
|
plan: req.body.stripePlan,
|
||||||
|
email: req.body.shipEmail,
|
||||||
|
name: `${req.body.shipFirstname} ${req.body.shipLastname}`,
|
||||||
|
phone: req.body.shipPhoneNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!customer){
|
||||||
|
req.session.messageType = 'danger';
|
||||||
|
req.session.message = 'Your subscripton has declined. Please try again';
|
||||||
|
req.session.paymentApproved = false;
|
||||||
|
req.session.paymentDetails = '';
|
||||||
|
res.redirect('/pay');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a subscription
|
||||||
|
if(customer.subscriptions.data && customer.subscriptions.data.length === 0){
|
||||||
|
req.session.messageType = 'danger';
|
||||||
|
req.session.message = 'Your subscripton has declined. Please try again';
|
||||||
|
req.session.paymentApproved = false;
|
||||||
|
req.session.paymentDetails = '';
|
||||||
|
res.redirect('/pay');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = customer.subscriptions.data[0];
|
||||||
|
|
||||||
|
// Create the new order document
|
||||||
|
const orderDoc = {
|
||||||
|
orderPaymentId: subscription.id,
|
||||||
|
orderPaymentGateway: 'Stripe',
|
||||||
|
orderPaymentMessage: subscription.collection_method,
|
||||||
|
orderTotal: req.session.totalCartAmount,
|
||||||
|
orderEmail: req.body.shipEmail,
|
||||||
|
orderFirstname: req.body.shipFirstname,
|
||||||
|
orderLastname: req.body.shipLastname,
|
||||||
|
orderAddr1: req.body.shipAddr1,
|
||||||
|
orderAddr2: req.body.shipAddr2,
|
||||||
|
orderCountry: req.body.shipCountry,
|
||||||
|
orderState: req.body.shipState,
|
||||||
|
orderPostcode: req.body.shipPostcode,
|
||||||
|
orderPhoneNumber: req.body.shipPhoneNumber,
|
||||||
|
orderComment: req.body.orderComment,
|
||||||
|
orderStatus: 'Pending',
|
||||||
|
orderDate: new Date(),
|
||||||
|
orderProducts: req.session.cart,
|
||||||
|
orderType: 'Subscription',
|
||||||
|
orderCustomer: customer.id
|
||||||
|
};
|
||||||
|
|
||||||
|
// insert order into DB
|
||||||
|
const order = await db.orders.insertOne(orderDoc);
|
||||||
|
const orderId = order.insertedId;
|
||||||
|
|
||||||
|
indexOrders(req.app)
|
||||||
|
.then(() => {
|
||||||
|
// set the results
|
||||||
|
req.session.messageType = 'success';
|
||||||
|
req.session.message = 'Your subscription was successfully created';
|
||||||
|
req.session.paymentEmailAddr = req.body.shipEmail;
|
||||||
|
req.session.paymentApproved = true;
|
||||||
|
req.session.paymentDetails = '<p><strong>Order ID: </strong>' + orderId + '</p><p><strong>Subscription ID: </strong>' + subscription.id + '</p>';
|
||||||
|
|
||||||
|
// set payment results for email
|
||||||
|
const paymentResults = {
|
||||||
|
message: req.session.message,
|
||||||
|
messageType: req.session.messageType,
|
||||||
|
paymentEmailAddr: req.session.paymentEmailAddr,
|
||||||
|
paymentApproved: true,
|
||||||
|
paymentDetails: req.session.paymentDetails
|
||||||
|
};
|
||||||
|
|
||||||
|
// clear the cart
|
||||||
|
if(req.session.cart){
|
||||||
|
req.session.cartSubscription = null;
|
||||||
|
req.session.cart = null;
|
||||||
|
req.session.orderId = null;
|
||||||
|
req.session.totalCartAmount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send the email with the response
|
||||||
|
common.sendEmail(req.session.paymentEmailAddr, 'Your payment with ' + config.cartTitle, common.getEmailTemplate(paymentResults));
|
||||||
|
|
||||||
|
// redirect to outcome
|
||||||
|
res.redirect('/payment/' + orderId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
47
test/test.js
47
test/test.js
|
@ -141,6 +141,41 @@ test.serial('[Success] Customer login with correct email', async t => {
|
||||||
t.deepEqual(res.body.message, 'Successfully logged in');
|
t.deepEqual(res.body.message, 'Successfully logged in');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.serial('[Success] Add subscripton product to cart', async t => {
|
||||||
|
const res = await request
|
||||||
|
.post('/product/addtocart')
|
||||||
|
.send({
|
||||||
|
productId: products[7]._id,
|
||||||
|
productQuantity: 1,
|
||||||
|
productOptions: {}
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
const sessions = await db.cart.find({}).toArray();
|
||||||
|
if(!sessions || sessions.length === 0){
|
||||||
|
t.fail();
|
||||||
|
}
|
||||||
|
t.deepEqual(res.body.message, 'Cart successfully updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('[Fail] Add product to cart when subscription already added', async t => {
|
||||||
|
const res = await request
|
||||||
|
.post('/product/addtocart')
|
||||||
|
.send({
|
||||||
|
productId: products[1]._id,
|
||||||
|
productQuantity: 100,
|
||||||
|
productOptions: JSON.stringify(products[1].productOptions)
|
||||||
|
})
|
||||||
|
.expect(400);
|
||||||
|
t.deepEqual(res.body.message, 'Subscription already existing in cart. You cannot add more.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('[Success] Empty cart', async t => {
|
||||||
|
const res = await request
|
||||||
|
.post('/product/emptycart')
|
||||||
|
.expect(200);
|
||||||
|
t.deepEqual(res.body.message, 'Cart successfully emptied');
|
||||||
|
});
|
||||||
|
|
||||||
test.serial('[Success] Add product to cart', async t => {
|
test.serial('[Success] Add product to cart', async t => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.post('/product/addtocart')
|
.post('/product/addtocart')
|
||||||
|
@ -157,6 +192,18 @@ test.serial('[Success] Add product to cart', async t => {
|
||||||
t.deepEqual(res.body.message, 'Cart successfully updated');
|
t.deepEqual(res.body.message, 'Cart successfully updated');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.serial('[Fail] Cannot add subscripton when other product in cart', async t => {
|
||||||
|
const res = await request
|
||||||
|
.post('/product/addtocart')
|
||||||
|
.send({
|
||||||
|
productId: products[7]._id,
|
||||||
|
productQuantity: 1,
|
||||||
|
productOptions: {}
|
||||||
|
})
|
||||||
|
.expect(400);
|
||||||
|
t.deepEqual(res.body.message, 'You cannot combine scubscription products with existing in your cart. Empty your cart and try again.');
|
||||||
|
});
|
||||||
|
|
||||||
test.serial('[Fail] Add product to cart with not enough stock', async t => {
|
test.serial('[Fail] Add product to cart with not enough stock', async t => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.post('/product/addtocart')
|
.post('/product/addtocart')
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<div class="pull-right col-md-2">
|
<div class="pull-right col-md-2">
|
||||||
<select class="form-control input-sm" id="orderStatus">
|
<select class="form-control input-sm" id="orderStatus">
|
||||||
<option>{{ @root.__ "Completed" }}</option>
|
<option>{{ @root.__ "Completed" }}</option>
|
||||||
|
<option>{{ @root.__ "Paid" }}</option>
|
||||||
<option>{{ @root.__ "Pending" }}</option>
|
<option>{{ @root.__ "Pending" }}</option>
|
||||||
<option>{{ @root.__ "Cancelled" }}</option>
|
<option>{{ @root.__ "Cancelled" }}</option>
|
||||||
<option>{{ @root.__ "Declined" }}</option>
|
<option>{{ @root.__ "Declined" }}</option>
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
<li class="list-group-item"><strong> {{ @root.__ "State" }}: </strong><span class="pull-right">{{result.orderState}}</span></li>
|
<li class="list-group-item"><strong> {{ @root.__ "State" }}: </strong><span class="pull-right">{{result.orderState}}</span></li>
|
||||||
<li class="list-group-item"><strong> {{ @root.__ "Postcode" }}: </strong><span class="pull-right">{{result.orderPostcode}}</span></li>
|
<li class="list-group-item"><strong> {{ @root.__ "Postcode" }}: </strong><span class="pull-right">{{result.orderPostcode}}</span></li>
|
||||||
<li class="list-group-item"><strong> {{ @root.__ "Phone number" }}: </strong><span class="pull-right">{{result.orderPhoneNumber}}</span></li>
|
<li class="list-group-item"><strong> {{ @root.__ "Phone number" }}: </strong><span class="pull-right">{{result.orderPhoneNumber}}</span></li>
|
||||||
|
<li class="list-group-item"><strong> {{ @root.__ "Order type" }}: </strong><span class="pull-right">{{result.orderType}}</span></li>
|
||||||
<li class="list-group-item"><strong> {{ @root.__ "Order comment" }}: </strong><span class="pull-right">{{result.orderComment}}</span></li>
|
<li class="list-group-item"><strong> {{ @root.__ "Order comment" }}: </strong><span class="pull-right">{{result.orderComment}}</span></li>
|
||||||
|
|
||||||
<li class="list-group-item"> </li>
|
<li class="list-group-item"> </li>
|
||||||
|
@ -57,7 +59,7 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
)
|
)
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="pull-right">{{currencySymbol config.currencySymbol}}{{formatAmount this.totalItemPrice}}</div>
|
<div class="pull-right">{{currencySymbol @root.config.currencySymbol}}{{formatAmount this.totalItemPrice}}</div>
|
||||||
{{#if productComment}}
|
{{#if productComment}}
|
||||||
<h4><span class="text-danger">Comment:</span> {{this.productComment}}</h4>
|
<h4><span class="text-danger">Comment:</span> {{this.productComment}}</h4>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
data-description="{{@root.config.cartTitle}} Payment"
|
data-description="{{@root.config.cartTitle}} Payment"
|
||||||
data-image="{{@root.paymentConfig.stripeLogoURL}}"
|
data-image="{{@root.paymentConfig.stripeLogoURL}}"
|
||||||
data-email="{{@root.session.customer.email}}"
|
data-email="{{@root.session.customer.email}}"
|
||||||
|
{{#if @root.session.cartSubscription}}
|
||||||
|
data-subscription="{{@root.session.cartSubscription}}"
|
||||||
|
{{/if}}
|
||||||
data-locale="auto"
|
data-locale="auto"
|
||||||
data-zip-code="false"
|
data-zip-code="false"
|
||||||
data-currency="{{@root.paymentConfig.stripeCurrency}}">
|
data-currency="{{@root.paymentConfig.stripeCurrency}}">
|
||||||
|
|
|
@ -107,6 +107,15 @@
|
||||||
<p class="help-block">{{ @root.__ "Here you can set options for your product. Eg: Size, color, style" }}</p>
|
<p class="help-block">{{ @root.__ "Here you can set options for your product. Eg: Size, color, style" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{#ifCond config.paymentGateway '==' 'stripe'}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-2 control-label">Subscription plan</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" name="productSubscription" id="productSubscription" placeholder="plan_XXXXXXXXXXXXXX" value={{@root.result.productSubscription}}>
|
||||||
|
<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}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="productComment" class="col-sm-2 control-label">{{ @root.__ "Allow comment" }}</label>
|
<label for="productComment" class="col-sm-2 control-label">{{ @root.__ "Allow comment" }}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
|
@ -106,6 +106,15 @@
|
||||||
<p class="help-block">{{ @root.__ "Here you can set options for your product. Eg: Size, color, style" }}</p>
|
<p class="help-block">{{ @root.__ "Here you can set options for your product. Eg: Size, color, style" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{#ifCond config.paymentGateway '==' 'stripe'}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-2 control-label">Subscription plan</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" name="productSubscription" 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}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="productComment" class="col-sm-2 control-label">{{ @root.__ "Allow comment" }}</label>
|
<label for="productComment" class="col-sm-2 control-label">{{ @root.__ "Allow comment" }}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<button id="customerLogout" class="btn btn-sm btn-success pull-right">{{ @root.__ "Change customer" }}</button>
|
<button id="customerLogout" class="btn btn-sm btn-success pull-right">{{ @root.__ "Change customer" }}</button>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action" method="post" role="form" data-toggle="validator" novalidate="false">
|
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action{{@root.paymentType}}" method="post" role="form" data-toggle="validator" novalidate="false">
|
||||||
{{> partials/payments/shipping-form}}
|
{{> partials/payments/shipping-form}}
|
||||||
{{#if session.customer}}
|
{{#if session.customer}}
|
||||||
{{#ifCond config.paymentGateway '==' 'paypal'}}
|
{{#ifCond config.paymentGateway '==' 'paypal'}}
|
||||||
|
@ -68,4 +68,4 @@
|
||||||
{{> (getTheme 'cart')}}
|
{{> (getTheme 'cart')}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -28,7 +28,7 @@
|
||||||
<button id="customerLogout" class="btn waves-effect waves-light blue darken-3 pull-right">{{ @root.__ "Change customer" }}</button>
|
<button id="customerLogout" class="btn waves-effect waves-light blue darken-3 pull-right">{{ @root.__ "Change customer" }}</button>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action" method="post" role="form" data-toggle="validator" novalidate="false">
|
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action{{@root.paymentType}}" method="post" role="form" data-toggle="validator" novalidate="false">
|
||||||
{{> themes/Material/shipping-form}}
|
{{> themes/Material/shipping-form}}
|
||||||
{{#if session.customer}}
|
{{#if session.customer}}
|
||||||
{{#ifCond config.paymentGateway '==' 'paypal'}}
|
{{#ifCond config.paymentGateway '==' 'paypal'}}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
<button id="customerLogout" class="btn waves-effect waves-light black pull-right">{{ @root.__ "Change customer" }}</button>
|
<button id="customerLogout" class="btn waves-effect waves-light black pull-right">{{ @root.__ "Change customer" }}</button>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action" method="post" role="form" data-toggle="validator" novalidate="false">
|
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action{{@root.paymentType}}" method="post" role="form" data-toggle="validator" novalidate="false">
|
||||||
{{> themes/Mono/shipping-form}}
|
{{> themes/Mono/shipping-form}}
|
||||||
{{#if session.customer}}
|
{{#if session.customer}}
|
||||||
{{#ifCond config.paymentGateway '==' 'paypal'}}
|
{{#ifCond config.paymentGateway '==' 'paypal'}}
|
||||||
|
|
Loading…
Reference in New Issue