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

27
app.js
View File

@ -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) => {

View File

@ -78,7 +78,7 @@
},
"paymentGateway": {
"type": "string",
"enum": ["paypal", "stripe", "authorizenet", "adyen"]
"enum": ["paypal", "stripe", "authorizenet", "adyen", "instore"]
},
"databaseConnectionString": {
"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",
"authorise.net",
"adyen",
"instore",
"lunr",
"cart",
"shopping"

View File

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

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'}}
{{> partials/payments/adyen}}
{{/ifCond}}
{{#ifCond config.paymentGateway '==' 'instore'}}
{{> partials/payments/instore}}
{{/ifCond}}
{{/if}}
</form>
</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>