Added instore payment type.
							parent
							
								
									f2e6b32384
								
							
						
					
					
						commit
						eafb690445
					
				
							
								
								
									
										19
									
								
								README.md
								
								
								
								
							
							
						
						
									
										19
									
								
								README.md
								
								
								
								
							|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
|  | ||||
| 
 | ||||
| `expressCart` is a fully functional shopping cart built in Node.js (Express, MongoDB) with Stripe, PayPal, Authorize.net and Adyen payments. | ||||
| `expressCart` is a fully functional shopping cart built in Node.js (Express, MongoDB) with Stripe, PayPal, Authorize.net, Adyen and Instore payments. | ||||
| 
 | ||||
| [](https://github.com/mrvautin/expressCart) | ||||
| [](https://travis-ci.org/mrvautin/expressCart) | ||||
|  | @ -308,6 +308,19 @@ The Adyen config file is located: `/config/adyen.json`. A example Adyen settings | |||
| 
 | ||||
| Note: The `publicKey`, `apiKey` and `merchantAccount` is obtained from your Adyen account dashboard. | ||||
| 
 | ||||
| ##### Instore (Payments) | ||||
| 
 | ||||
| The Instore config file is located: `/config/instore.json`. A example Instore settings file is provided: | ||||
| 
 | ||||
| ``` | ||||
| { | ||||
|     "orderStatus": "Pending", | ||||
|     "buttonText": "Place order, pay instore", | ||||
|     "resultMessage": "The order is place. Please pay for your order instore on pickup." | ||||
| } | ||||
| ``` | ||||
| Note: No payment is actually processed. The order will move to the `orderStatus` set and the payment is completed instore. | ||||
| 
 | ||||
| ## Email settings | ||||
| 
 | ||||
| You will need to configure your SMTP details for expressCart to send email receipts to your customers. | ||||
|  | @ -362,9 +375,7 @@ New static pages are setup via `/admin/settings/pages`. | |||
| 
 | ||||
| ## TODO | ||||
| 
 | ||||
| - Add some tests... | ||||
| - Separate API and frontend | ||||
| - Modernize the frontend | ||||
| - Modernize the frontend of the admin | ||||
| 
 | ||||
| ## Contributing | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										27
									
								
								app.js
								
								
								
								
							
							
						
						
									
										27
									
								
								app.js
								
								
								
								
							|  | @ -66,6 +66,12 @@ switch(config.paymentGateway){ | |||
|             process.exit(2); | ||||
|         } | ||||
|         break; | ||||
|     case'instore': | ||||
|         if(ajv.validate(require('./config/instoreSchema'), require('./config/instore.json')) === false){ | ||||
|             console.log(colors.red(`instore config is incorrect: ${ajv.errorsText()}`)); | ||||
|             process.exit(2); | ||||
|         } | ||||
|         break; | ||||
| } | ||||
| 
 | ||||
| // require the routes
 | ||||
|  | @ -79,6 +85,7 @@ const paypal = require('./routes/payments/paypal'); | |||
| const stripe = require('./routes/payments/stripe'); | ||||
| const authorizenet = require('./routes/payments/authorizenet'); | ||||
| const adyen = require('./routes/payments/adyen'); | ||||
| const instore = require('./routes/payments/instore'); | ||||
| 
 | ||||
| const app = express(); | ||||
| 
 | ||||
|  | @ -259,6 +266,25 @@ handlebars = handlebars.create({ | |||
|                 return options.fn(this); | ||||
|             } | ||||
|             return options.inverse(this); | ||||
|         }, | ||||
|         paymentMessage: (status) => { | ||||
|             if(status === 'Paid'){ | ||||
|                 return'<h2 class="text-success">Your payment has been successfully processed</h2>'; | ||||
|             } | ||||
|             if(status === 'Pending'){ | ||||
|                 const paymentConfig = common.getPaymentConfig(); | ||||
|                 if(config.paymentGateway === 'instore'){ | ||||
|                     return`<h2 class="text-warning">${paymentConfig.resultMessage}</h2>`; | ||||
|                 } | ||||
|                 return'<h2 class="text-warning">The payment for this order is pending. We will be in contact shortly.</h2>'; | ||||
|             } | ||||
|             return'<h2 class="text-success">Your payment has failed. Please try again or contact us.</h2>'; | ||||
|         }, | ||||
|         paymentOutcome: (status) => { | ||||
|             if(status === 'Paid' || status === 'Pending'){ | ||||
|                 return'<h3 class="text-success">Please retain the details above as a reference of payment</h3>'; | ||||
|             } | ||||
|             return''; | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | @ -338,6 +364,7 @@ app.use('/paypal', paypal); | |||
| app.use('/stripe', stripe); | ||||
| app.use('/authorizenet', authorizenet); | ||||
| app.use('/adyen', adyen); | ||||
| app.use('/instore', instore); | ||||
| 
 | ||||
| // catch 404 and forward to error handler
 | ||||
| app.use((req, res, next) => { | ||||
|  |  | |||
|  | @ -78,7 +78,7 @@ | |||
|         }, | ||||
|         "paymentGateway": { | ||||
|             "type": "string", | ||||
|             "enum": ["paypal", "stripe", "authorizenet", "adyen"] | ||||
|             "enum": ["paypal", "stripe", "authorizenet", "adyen", "instore"] | ||||
|         }, | ||||
|         "databaseConnectionString": { | ||||
|             "type": "string" | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| { | ||||
|     "orderStatus": "Pending", | ||||
|     "buttonText": "Place order, pay instore", | ||||
|     "resultMessage": "The order is place. Please pay for your order instore on pickup." | ||||
| } | ||||
|  | @ -0,0 +1,20 @@ | |||
| { | ||||
|     "properties": { | ||||
|         "orderStatus": { | ||||
|             "type": "string", | ||||
|             "enum": ["Completed", "Paid", "Pending"] | ||||
|         }, | ||||
|         "buttonText": { | ||||
|             "type": "string" | ||||
|         }, | ||||
|         "resultMessage": { | ||||
|             "type": "string" | ||||
|         } | ||||
|     }, | ||||
|     "required": [ | ||||
|         "orderStatus", | ||||
|         "buttonText", | ||||
|         "resultMessage" | ||||
|     ], | ||||
|     "additionalProperties": false | ||||
| } | ||||
|  | @ -98,6 +98,7 @@ | |||
|     "stripe", | ||||
|     "authorise.net", | ||||
|     "adyen", | ||||
|     "instore", | ||||
|     "lunr", | ||||
|     "cart", | ||||
|     "shopping" | ||||
|  |  | |||
|  | @ -55,7 +55,7 @@ router.get('/payment/:orderId', async (req, res, next) => { | |||
|         await hooker(order); | ||||
|     }; | ||||
| 
 | ||||
|     res.render(`${config.themeViews}payment_complete`, { | ||||
|     res.render(`${config.themeViews}payment-complete`, { | ||||
|         title: 'Payment complete', | ||||
|         config: req.app.config, | ||||
|         session: req.session, | ||||
|  |  | |||
|  | @ -0,0 +1,81 @@ | |||
| const express = require('express'); | ||||
| const common = require('../../lib/common'); | ||||
| const { indexOrders } = require('../../lib/indexing'); | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // The homepage of the site
 | ||||
| router.post('/checkout_action', async (req, res, next) => { | ||||
|     const db = req.app.db; | ||||
|     const config = req.app.config; | ||||
|     const instoreConfig = common.getPaymentConfig(); | ||||
| 
 | ||||
|     const orderDoc = { | ||||
|         orderPaymentId: common.getId(), | ||||
|         orderPaymentGateway: 'Instore', | ||||
|         orderPaymentMessage: 'Your payment was successfully completed', | ||||
|         orderTotal: req.session.totalCartAmount, | ||||
|         orderItemCount: req.session.totalCartItems, | ||||
|         orderProductCount: req.session.totalCartProducts, | ||||
|         orderEmail: req.session.customerEmail, | ||||
|         orderFirstname: req.session.customerFirstname, | ||||
|         orderLastname: req.session.customerLastname, | ||||
|         orderAddr1: req.session.customerAddress1, | ||||
|         orderAddr2: req.session.customerAddress2, | ||||
|         orderCountry: req.session.customerCountry, | ||||
|         orderState: req.session.customerState, | ||||
|         orderPostcode: req.session.customerPostcode, | ||||
|         orderPhoneNumber: req.session.customerPhone, | ||||
|         orderComment: req.session.orderComment, | ||||
|         orderStatus: instoreConfig.orderStatus, | ||||
|         orderDate: new Date(), | ||||
|         orderProducts: req.session.cart | ||||
|     }; | ||||
| 
 | ||||
|     // insert order into DB
 | ||||
|     try{ | ||||
|         const newDoc = await db.orders.insertOne(orderDoc); | ||||
| 
 | ||||
|         // get the new ID
 | ||||
|         const newId = newDoc.insertedId; | ||||
| 
 | ||||
|         // add to lunr index
 | ||||
|         indexOrders(req.app) | ||||
|         .then(() => { | ||||
|             // set the results
 | ||||
|             req.session.messageType = 'success'; | ||||
|             req.session.message = 'Your order was successfully placed. Payment for your order will be completed instore.'; | ||||
|             req.session.paymentEmailAddr = newDoc.ops[0].orderEmail; | ||||
|             req.session.paymentApproved = true; | ||||
|             req.session.paymentDetails = `<p><strong>Order ID: </strong>${newId}</p>
 | ||||
|             <p><strong>Transaction ID: </strong>${orderDoc.orderPaymentId}</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.cart = null; | ||||
|                 req.session.orderId = null; | ||||
|                 req.session.totalCartAmount = 0; | ||||
|             } | ||||
| 
 | ||||
|             // send the email with the response
 | ||||
|             // TODO: Should fix this to properly handle result
 | ||||
|             common.sendEmail(req.session.paymentEmailAddr, `Your order with ${config.cartTitle}`, common.getEmailTemplate(paymentResults)); | ||||
| 
 | ||||
|             // redirect to outcome
 | ||||
|             res.redirect('/payment/' + newId); | ||||
|         }); | ||||
|     }catch(ex){ | ||||
|         console.log('Error sending payment to API', ex); | ||||
|         res.status(400).json({ err: 'Your order declined. Please try again' }); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| module.exports = router; | ||||
|  | @ -0,0 +1,3 @@ | |||
| <div class="instore_button col-sm-12 text-center"> | ||||
|     <button id="checkout_instore" class="btn btn-outline-success" type="submit">{{@root.paymentConfig.buttonText}}</button> | ||||
| </div> | ||||
|  | @ -53,6 +53,9 @@ | |||
|                     {{#ifCond config.paymentGateway '==' 'adyen'}} | ||||
|                         {{> partials/payments/adyen}} | ||||
|                     {{/ifCond}} | ||||
|                     {{#ifCond config.paymentGateway '==' 'instore'}} | ||||
|                         {{> partials/payments/instore}} | ||||
|                     {{/ifCond}} | ||||
|                     {{/if}} | ||||
|                     </form> | ||||
|                 </div> | ||||
|  |  | |||
|  | @ -0,0 +1,13 @@ | |||
| <div class="col-md-10 offset-md-1 col-sm-12 top-pad-50"> | ||||
|     <div class="row"> | ||||
|         <div class="text-center col-md-10 offset-md-1"> | ||||
|             {{#paymentMessage result.orderStatus}}{{/paymentMessage}} | ||||
|             <div> | ||||
|                 <p><strong>{{ @root.__ "Order ID" }}:</strong> {{result._id}}</p> | ||||
|                 <p><strong>{{ @root.__ "Payment ID" }}:</strong> {{result.orderPaymentId}}</p> | ||||
|             </div> | ||||
|             {{#paymentOutcome result.orderStatus}}{{/paymentOutcome}} | ||||
|             <a href="/" class="btn btn-outline-warning">Home</a> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | @ -1,21 +0,0 @@ | |||
| <div class="col-md-10 offset-md-1 col-sm-12 top-pad-50"> | ||||
|     <div class="row"> | ||||
|         <div class="text-center col-md-10 offset-md-1"> | ||||
|             {{#ifCond result.orderStatus '==' 'Paid'}} | ||||
|                 <h2 class="text-success">{{ @root.__ "Your payment has been successfully processed" }}</h2> | ||||
|             {{else}} | ||||
|                 <h2 class="text-danger">{{ @root.__ "Your payment has failed. Please try again or contact us." }}</h2> | ||||
|             {{/ifCond}} | ||||
|             {{#if result}} | ||||
|                 <div> | ||||
|                     <p><strong>{{ @root.__ "Order ID" }}:</strong> {{result._id}}</p> | ||||
|                     <p><strong>{{ @root.__ "Payment ID" }}:</strong> {{result.orderPaymentId}}</p> | ||||
|                 </div> | ||||
|             {{/if}}  | ||||
|             {{#ifCond result.orderStatus '==' 'Paid'}} | ||||
|                 <h3 class="text-warning">{{ @root.__ "Please retain the details above as a reference of payment." }}</h3> | ||||
|             {{/ifCond}} | ||||
|             <a href="/" class="btn btn-outline-warning">Home</a> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
		Loading…
	
		Reference in New Issue