Added instore payment type.

master
Mark Moffat 2020-01-01 14:57:42 +10:30
parent f2e6b32384
commit eafb690445
12 changed files with 170 additions and 27 deletions

View File

@ -2,7 +2,7 @@
![expressCart](https://raw.githubusercontent.com/mrvautin/expressCart/master/public/images/logo.png) ![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) [![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) [![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. 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 ## Email settings
You will need to configure your SMTP details for expressCart to send email receipts to your customers. 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 ## TODO
- Add some tests... - Modernize the frontend of the admin
- Separate API and frontend
- Modernize the frontend
## Contributing ## Contributing

27
app.js
View File

@ -66,6 +66,12 @@ switch(config.paymentGateway){
process.exit(2); process.exit(2);
} }
break; 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 // require the routes
@ -79,6 +85,7 @@ const paypal = require('./routes/payments/paypal');
const stripe = require('./routes/payments/stripe'); const stripe = require('./routes/payments/stripe');
const authorizenet = require('./routes/payments/authorizenet'); const authorizenet = require('./routes/payments/authorizenet');
const adyen = require('./routes/payments/adyen'); const adyen = require('./routes/payments/adyen');
const instore = require('./routes/payments/instore');
const app = express(); const app = express();
@ -259,6 +266,25 @@ handlebars = handlebars.create({
return options.fn(this); return options.fn(this);
} }
return options.inverse(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('/stripe', stripe);
app.use('/authorizenet', authorizenet); app.use('/authorizenet', authorizenet);
app.use('/adyen', adyen); app.use('/adyen', adyen);
app.use('/instore', instore);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use((req, res, next) => { app.use((req, res, next) => {

View File

@ -78,7 +78,7 @@
}, },
"paymentGateway": { "paymentGateway": {
"type": "string", "type": "string",
"enum": ["paypal", "stripe", "authorizenet", "adyen"] "enum": ["paypal", "stripe", "authorizenet", "adyen", "instore"]
}, },
"databaseConnectionString": { "databaseConnectionString": {
"type": "string" "type": "string"

5
config/instore.json Normal file
View File

@ -0,0 +1,5 @@
{
"orderStatus": "Pending",
"buttonText": "Place order, pay instore",
"resultMessage": "The order is place. Please pay for your order instore on pickup."
}

20
config/instoreSchema.json Normal file
View File

@ -0,0 +1,20 @@
{
"properties": {
"orderStatus": {
"type": "string",
"enum": ["Completed", "Paid", "Pending"]
},
"buttonText": {
"type": "string"
},
"resultMessage": {
"type": "string"
}
},
"required": [
"orderStatus",
"buttonText",
"resultMessage"
],
"additionalProperties": false
}

View File

@ -98,6 +98,7 @@
"stripe", "stripe",
"authorise.net", "authorise.net",
"adyen", "adyen",
"instore",
"lunr", "lunr",
"cart", "cart",
"shopping" "shopping"

View File

@ -55,7 +55,7 @@ router.get('/payment/:orderId', async (req, res, next) => {
await hooker(order); await hooker(order);
}; };
res.render(`${config.themeViews}payment_complete`, { res.render(`${config.themeViews}payment-complete`, {
title: 'Payment complete', title: 'Payment complete',
config: req.app.config, config: req.app.config,
session: req.session, session: req.session,

View File

@ -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;

View File

@ -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>

View File

@ -53,6 +53,9 @@
{{#ifCond config.paymentGateway '==' 'adyen'}} {{#ifCond config.paymentGateway '==' 'adyen'}}
{{> partials/payments/adyen}} {{> partials/payments/adyen}}
{{/ifCond}} {{/ifCond}}
{{#ifCond config.paymentGateway '==' 'instore'}}
{{> partials/payments/instore}}
{{/ifCond}}
{{/if}} {{/if}}
</form> </form>
</div> </div>

View File

@ -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>

View File

@ -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>