Added instore payment type.
parent
f2e6b32384
commit
eafb690445
19
README.md
19
README.md
|
@ -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
27
app.js
|
@ -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) => {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
"stripe",
|
||||||
"authorise.net",
|
"authorise.net",
|
||||||
"adyen",
|
"adyen",
|
||||||
|
"instore",
|
||||||
"lunr",
|
"lunr",
|
||||||
"cart",
|
"cart",
|
||||||
"shopping"
|
"shopping"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'}}
|
{{#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>
|
||||||
|
|
|
@ -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