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. | ||||
| 
 | ||||
| ## 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 | ||||
| 
 | ||||
| `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", | ||||
|             "productOptions" : "{\"Size\":{\"optName\":\"Size\",\"optLabel\":\"Select size\",\"optType\":\"select\",\"optOptions\":[\"S\",\"M\",\"L\"]}}", | ||||
|             "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": [ | ||||
|  |  | |||
|  | @ -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) => { | ||||
|     try{ | ||||
|         fs.statSync(directory); | ||||
|  | @ -551,6 +567,7 @@ module.exports = { | |||
|     addSitemapProducts, | ||||
|     clearSessionValue, | ||||
|     updateTotalCartAmount, | ||||
|     updateSubscriptionCheck, | ||||
|     checkDirectorySync, | ||||
|     getThemes, | ||||
|     getImages, | ||||
|  |  | |||
|  | @ -160,5 +160,6 @@ | |||
| 	"Cart contents": "Cart contents", | ||||
| 	"Shipping": "Shipping:", | ||||
| 	"Empty cart": "Empty cart", | ||||
| 	"List": "List" | ||||
| 	"List": "List", | ||||
| 	"Order type": "Order type" | ||||
| } | ||||
|  | @ -289,6 +289,9 @@ $(document).ready(function (){ | |||
|                 image: $('#stripeButton').data('image'), | ||||
|                 locale: 'auto', | ||||
|                 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').submit(); | ||||
|                 } | ||||
|  | @ -301,7 +304,8 @@ $(document).ready(function (){ | |||
|                 description: $('#stripeButton').data('description'), | ||||
|                 zipCode: $('#stripeButton').data('zipCode'), | ||||
|                 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, | ||||
|     getImages, | ||||
|     updateTotalCartAmount, | ||||
|     updateSubscriptionCheck, | ||||
|     getData, | ||||
|     addSitemapProducts | ||||
| } = 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) => { | ||||
|     const config = req.app.config; | ||||
| 
 | ||||
|  | @ -105,6 +110,11 @@ router.get('/pay', async (req, res, next) => { | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     let paymentType = ''; | ||||
|     if(req.session.cartSubscription){ | ||||
|         paymentType = '_subscription'; | ||||
|     } | ||||
| 
 | ||||
|     // render the payment page
 | ||||
|     res.render(`${config.themeViews}pay`, { | ||||
|         title: 'Pay', | ||||
|  | @ -113,6 +123,7 @@ router.get('/pay', async (req, res, next) => { | |||
|         pageCloseBtn: showCartCloseBtn('pay'), | ||||
|         session: req.session, | ||||
|         paymentPage: true, | ||||
|         paymentType, | ||||
|         page: 'pay', | ||||
|         message: clearSessionValue(req.session, 'message'), | ||||
|         messageType: clearSessionValue(req.session, 'messageType'), | ||||
|  | @ -220,6 +231,9 @@ router.post('/product/updatecart', (req, res, next) => { | |||
|         // update total cart amount
 | ||||
|         updateTotalCartAmount(req, res); | ||||
| 
 | ||||
|         // Update checking cart for subscription
 | ||||
|         updateSubscriptionCheck(req, res); | ||||
| 
 | ||||
|         // Update cart to the DB
 | ||||
|         await db.cart.updateOne({ sessionId: req.session.id }, { | ||||
|             $set: { cart: req.session.cart } | ||||
|  | @ -239,36 +253,42 @@ router.post('/product/updatecart', (req, res, next) => { | |||
| }); | ||||
| 
 | ||||
| // 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; | ||||
|     let itemRemoved = false; | ||||
| 
 | ||||
|     // remove item from cart
 | ||||
|     async.each(req.session.cart, (item, callback) => { | ||||
|     req.session.cart.forEach((item) => { | ||||
|         if(item){ | ||||
|             if(item.productId === req.body.cartId){ | ||||
|                 itemRemoved = true; | ||||
|                 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
 | ||||
| router.post('/product/emptycart', async (req, res, next) => { | ||||
|     emptyCart(req, res, 'json'); | ||||
| }); | ||||
| 
 | ||||
| const emptyCart = async (req, res, type) => { | ||||
|     const db = req.app.db; | ||||
| 
 | ||||
|     // Remove from session
 | ||||
|  | @ -280,8 +300,20 @@ router.post('/product/emptycart', async (req, res, next) => { | |||
| 
 | ||||
|     // update total cart amount
 | ||||
|     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
 | ||||
| router.post('/product/addtocart', async (req, res, next) => { | ||||
|  | @ -300,13 +332,25 @@ router.post('/product/addtocart', async (req, res, next) => { | |||
|         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) }); | ||||
|     // No product found
 | ||||
|     if(!product){ | ||||
|         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(config.trackStock && product.productStock){ | ||||
|         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.
 | ||||
|     let options = {}; | ||||
|     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 = { | ||||
|         productId: req.body.productId, | ||||
|  | @ -376,6 +426,7 @@ router.post('/product/addtocart', async (req, res, next) => { | |||
|         productObj.options = options; | ||||
|         productObj.productImage = product.productImage; | ||||
|         productObj.productComment = productComment; | ||||
|         productObj.productSubscription = product.productSubscription; | ||||
|         if(product.productPermalink){ | ||||
|             productObj.link = product.productPermalink; | ||||
|         }else{ | ||||
|  | @ -394,6 +445,13 @@ router.post('/product/addtocart', async (req, res, next) => { | |||
|     // update total cart amount
 | ||||
|     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
 | ||||
|     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 }); | ||||
|  |  | |||
|  | @ -52,7 +52,8 @@ router.post('/checkout_action', (req, res, next) => { | |||
|             orderComment: req.body.orderComment, | ||||
|             orderStatus: paymentStatus, | ||||
|             orderDate: new Date(), | ||||
|             orderProducts: req.session.cart | ||||
|             orderProducts: req.session.cart, | ||||
|             orderType: 'Single' | ||||
|         }; | ||||
| 
 | ||||
|         // 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; | ||||
|  |  | |||
							
								
								
									
										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'); | ||||
| }); | ||||
| 
 | ||||
| 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 => { | ||||
|     const res = await request | ||||
|         .post('/product/addtocart') | ||||
|  | @ -157,6 +192,18 @@ test.serial('[Success] Add product to cart', async t => { | |||
|     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 => { | ||||
|     const res = await request | ||||
|         .post('/product/addtocart') | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
|                 <div class="pull-right col-md-2"> | ||||
|                     <select class="form-control input-sm" id="orderStatus"> | ||||
|                         <option>{{ @root.__ "Completed" }}</option> | ||||
|                         <option>{{ @root.__ "Paid" }}</option> | ||||
|                         <option>{{ @root.__ "Pending" }}</option> | ||||
|                         <option>{{ @root.__ "Cancelled" }}</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.__ "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.__ "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"> </li> | ||||
|  | @ -57,7 +59,7 @@ | |||
|                         {{/each}} | ||||
|                         ) | ||||
|                     {{/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}} | ||||
|                     <h4><span class="text-danger">Comment:</span> {{this.productComment}}</h4> | ||||
|                     {{/if}} | ||||
|  |  | |||
|  | @ -12,6 +12,9 @@ | |||
|             data-description="{{@root.config.cartTitle}} Payment" | ||||
|             data-image="{{@root.paymentConfig.stripeLogoURL}}" | ||||
|             data-email="{{@root.session.customer.email}}" | ||||
|             {{#if @root.session.cartSubscription}} | ||||
|             data-subscription="{{@root.session.cartSubscription}}" | ||||
|             {{/if}} | ||||
|             data-locale="auto" | ||||
|             data-zip-code="false" | ||||
|             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> | ||||
|                 </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"> | ||||
|                 <label for="productComment" class="col-sm-2 control-label">{{ @root.__ "Allow comment" }}</label> | ||||
|                 <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> | ||||
|                 </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"> | ||||
|                 <label for="productComment" class="col-sm-2 control-label">{{ @root.__ "Allow comment" }}</label> | ||||
|                 <div class="col-sm-10"> | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ | |||
|                         <button id="customerLogout" class="btn btn-sm btn-success pull-right">{{ @root.__ "Change customer" }}</button> | ||||
|                     </div> | ||||
|                     {{/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}} | ||||
|                         {{#if session.customer}} | ||||
|                         {{#ifCond config.paymentGateway '==' 'paypal'}} | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ | |||
|                         <button id="customerLogout" class="btn waves-effect waves-light blue darken-3 pull-right">{{ @root.__ "Change customer" }}</button> | ||||
|                     </div> | ||||
|                     {{/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}} | ||||
|                         {{#if session.customer}} | ||||
|                         {{#ifCond config.paymentGateway '==' 'paypal'}} | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ | |||
|                         <button id="customerLogout" class="btn waves-effect waves-light black pull-right">{{ @root.__ "Change customer" }}</button> | ||||
|                     </div> | ||||
|                     {{/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}} | ||||
|                         {{#if session.customer}} | ||||
|                         {{#ifCond config.paymentGateway '==' 'paypal'}} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue