From eafb690445db2b5d2eba1bfcff884ff5f5cbfa78 Mon Sep 17 00:00:00 2001 From: Mark Moffat Date: Wed, 1 Jan 2020 14:57:42 +1030 Subject: [PATCH] Added instore payment type. --- README.md | 19 ++++-- app.js | 27 +++++++++ config/baseSchema.json | 2 +- config/instore.json | 5 ++ config/instoreSchema.json | 20 ++++++ package.json | 1 + routes/index.js | 2 +- routes/payments/instore.js | 81 +++++++++++++++++++++++++ views/partials/payments/instore.hbs | 3 + views/themes/Cloth/checkout-payment.hbs | 3 + views/themes/Cloth/payment-complete.hbs | 13 ++++ views/themes/Cloth/payment_complete.hbs | 21 ------- 12 files changed, 170 insertions(+), 27 deletions(-) create mode 100644 config/instore.json create mode 100644 config/instoreSchema.json create mode 100644 routes/payments/instore.js create mode 100644 views/partials/payments/instore.hbs create mode 100644 views/themes/Cloth/payment-complete.hbs delete mode 100644 views/themes/Cloth/payment_complete.hbs diff --git a/README.md b/README.md index 148b67c..e9519d3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![expressCart](https://raw.githubusercontent.com/mrvautin/expressCart/master/public/images/logo.png) -`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. [![Github stars](https://img.shields.io/github/stars/mrvautin/expressCart.svg?style=social&label=Star)](https://github.com/mrvautin/expressCart) [![Build Status](https://travis-ci.org/mrvautin/expressCart.svg?branch=master)](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 diff --git a/app.js b/app.js index d4c56a1..8ef5c3f 100644 --- a/app.js +++ b/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'

Your payment has been successfully processed

'; + } + if(status === 'Pending'){ + const paymentConfig = common.getPaymentConfig(); + if(config.paymentGateway === 'instore'){ + return`

${paymentConfig.resultMessage}

`; + } + return'

The payment for this order is pending. We will be in contact shortly.

'; + } + return'

Your payment has failed. Please try again or contact us.

'; + }, + paymentOutcome: (status) => { + if(status === 'Paid' || status === 'Pending'){ + return'

Please retain the details above as a reference of payment

'; + } + 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) => { diff --git a/config/baseSchema.json b/config/baseSchema.json index 18b21e2..7679051 100644 --- a/config/baseSchema.json +++ b/config/baseSchema.json @@ -78,7 +78,7 @@ }, "paymentGateway": { "type": "string", - "enum": ["paypal", "stripe", "authorizenet", "adyen"] + "enum": ["paypal", "stripe", "authorizenet", "adyen", "instore"] }, "databaseConnectionString": { "type": "string" diff --git a/config/instore.json b/config/instore.json new file mode 100644 index 0000000..bc3e7b7 --- /dev/null +++ b/config/instore.json @@ -0,0 +1,5 @@ +{ + "orderStatus": "Pending", + "buttonText": "Place order, pay instore", + "resultMessage": "The order is place. Please pay for your order instore on pickup." +} \ No newline at end of file diff --git a/config/instoreSchema.json b/config/instoreSchema.json new file mode 100644 index 0000000..95e0151 --- /dev/null +++ b/config/instoreSchema.json @@ -0,0 +1,20 @@ +{ + "properties": { + "orderStatus": { + "type": "string", + "enum": ["Completed", "Paid", "Pending"] + }, + "buttonText": { + "type": "string" + }, + "resultMessage": { + "type": "string" + } + }, + "required": [ + "orderStatus", + "buttonText", + "resultMessage" + ], + "additionalProperties": false +} diff --git a/package.json b/package.json index d32e8df..230f1e7 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "stripe", "authorise.net", "adyen", + "instore", "lunr", "cart", "shopping" diff --git a/routes/index.js b/routes/index.js index bcf5e4a..5fa3465 100644 --- a/routes/index.js +++ b/routes/index.js @@ -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, diff --git a/routes/payments/instore.js b/routes/payments/instore.js new file mode 100644 index 0000000..6f95bdf --- /dev/null +++ b/routes/payments/instore.js @@ -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 = `

Order ID: ${newId}

+

Transaction ID: ${orderDoc.orderPaymentId}

`; + + // 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; diff --git a/views/partials/payments/instore.hbs b/views/partials/payments/instore.hbs new file mode 100644 index 0000000..289977b --- /dev/null +++ b/views/partials/payments/instore.hbs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/views/themes/Cloth/checkout-payment.hbs b/views/themes/Cloth/checkout-payment.hbs index 1206b2c..da34397 100644 --- a/views/themes/Cloth/checkout-payment.hbs +++ b/views/themes/Cloth/checkout-payment.hbs @@ -53,6 +53,9 @@ {{#ifCond config.paymentGateway '==' 'adyen'}} {{> partials/payments/adyen}} {{/ifCond}} + {{#ifCond config.paymentGateway '==' 'instore'}} + {{> partials/payments/instore}} + {{/ifCond}} {{/if}} diff --git a/views/themes/Cloth/payment-complete.hbs b/views/themes/Cloth/payment-complete.hbs new file mode 100644 index 0000000..061215e --- /dev/null +++ b/views/themes/Cloth/payment-complete.hbs @@ -0,0 +1,13 @@ +
+
+
+ {{#paymentMessage result.orderStatus}}{{/paymentMessage}} +
+

{{ @root.__ "Order ID" }}: {{result._id}}

+

{{ @root.__ "Payment ID" }}: {{result.orderPaymentId}}

+
+ {{#paymentOutcome result.orderStatus}}{{/paymentOutcome}} + Home +
+
+
diff --git a/views/themes/Cloth/payment_complete.hbs b/views/themes/Cloth/payment_complete.hbs deleted file mode 100644 index 2fbfe07..0000000 --- a/views/themes/Cloth/payment_complete.hbs +++ /dev/null @@ -1,21 +0,0 @@ -
-
-
- {{#ifCond result.orderStatus '==' 'Paid'}} -

{{ @root.__ "Your payment has been successfully processed" }}

- {{else}} -

{{ @root.__ "Your payment has failed. Please try again or contact us." }}

- {{/ifCond}} - {{#if result}} -
-

{{ @root.__ "Order ID" }}: {{result._id}}

-

{{ @root.__ "Payment ID" }}: {{result.orderPaymentId}}

-
- {{/if}} - {{#ifCond result.orderStatus '==' 'Paid'}} -

{{ @root.__ "Please retain the details above as a reference of payment." }}

- {{/ifCond}} - Home -
-
-