Added authorize.net payments

react_convert
Mark Moffat 2018-02-04 22:39:42 +01:00
parent 2f9911fd1e
commit 70105e73bc
10 changed files with 302 additions and 58 deletions

9
app.js
View File

@ -43,6 +43,13 @@ if(config.paymentGateway === 'stripe'){
process.exit(2); process.exit(2);
} }
} }
if(config.paymentGateway === 'authorizenet'){
const authorizenetConfig = ajv.validate(require('./config/authorizenetSchema'), require('./config/authorizenet.json'));
if(authorizenetConfig === false){
console.log(colors.red(`Authorizenet config is incorrect: ${ajv.errorsText()}`));
process.exit(2);
}
}
// require the routes // require the routes
const index = require('./routes/index'); const index = require('./routes/index');
@ -50,6 +57,7 @@ const admin = require('./routes/admin');
const customer = require('./routes/customer'); const customer = require('./routes/customer');
const paypal = require('./routes/payments/paypal'); 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 app = express(); const app = express();
@ -224,6 +232,7 @@ app.use('/', customer);
app.use('/admin', admin); app.use('/admin', admin);
app.use('/paypal', paypal); app.use('/paypal', paypal);
app.use('/stripe', stripe); app.use('/stripe', stripe);
app.use('/authorizenet', authorizenet);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use((req, res, next) => { app.use((req, res, next) => {

6
config/authorizenet.json Normal file
View File

@ -0,0 +1,6 @@
{
"loginId": "loginId",
"transactionKey": "transactionKey",
"clientKey": "clientKey",
"mode": "test"
}

View File

@ -0,0 +1,24 @@
{
"properties": {
"loginId": {
"type": "string"
},
"transactionKey": {
"type": "string"
},
"clientKey": {
"type": "string"
},
"mode": {
"type": "string",
"enum": ["test", "live"]
}
},
"required": [
"loginId",
"transactionKey",
"clientKey",
"mode"
],
"additionalProperties": false
}

View File

@ -83,7 +83,7 @@
}, },
"paymentGateway": { "paymentGateway": {
"type": "string", "type": "string",
"enum": ["paypal", "stripe"] "enum": ["paypal", "stripe", "authorizenet"]
}, },
"databaseConnectionString": { "databaseConnectionString": {
"format": "uri-template", "format": "uri-template",

View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"ajv": "^6.0.0", "ajv": "^6.0.0",
"async": "^2.6.0", "async": "^2.6.0",
"axios": "^0.17.1",
"bcrypt-nodejs": "0.0.3", "bcrypt-nodejs": "0.0.3",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.17.2", "body-parser": "^1.17.2",
@ -36,6 +37,7 @@
"rand-token": "^0.4.0", "rand-token": "^0.4.0",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"sitemap": "^1.6.0", "sitemap": "^1.6.0",
"strip-bom": "^3.0.0",
"stripe": "^5.4.0", "stripe": "^5.4.0",
"uglifycss": "0.0.27" "uglifycss": "0.0.27"
}, },

View File

@ -50,6 +50,9 @@
{{#ifCond config.paymentGateway '==' 'stripe'}} {{#ifCond config.paymentGateway '==' 'stripe'}}
{{> payments/stripe}} {{> payments/stripe}}
{{/ifCond}} {{/ifCond}}
{{#ifCond config.paymentGateway '==' 'authorizenet'}}
{{> payments/authorizenet}}
{{/ifCond}}
{{/if}} {{/if}}
</div> </div>
</div> </div>

View File

@ -0,0 +1,138 @@
const express = require('express');
const axios = require('axios');
const stripBom = require('strip-bom');
const common = require('../common');
const router = express.Router();
// The homepage of the site
router.post('/checkout_action', (req, res, next) => {
const db = req.app.db;
const config = common.getConfig();
const authorizenetConfig = common.getPaymentConfig();
let authorizeUrl = 'https://api.authorize.net/xml/v1/request.api';
if(authorizenetConfig.mode === 'test'){
authorizeUrl = 'https://apitest.authorize.net/xml/v1/request.api';
}
const chargeJson = {
createTransactionRequest: {
merchantAuthentication: {
name: authorizenetConfig.loginId,
transactionKey: authorizenetConfig.transactionKey
},
transactionRequest: {
transactionType: 'authCaptureTransaction',
amount: req.session.totalCartAmount,
payment: {
opaqueData: {
dataDescriptor: req.body.opaqueData.dataDescriptor,
dataValue: req.body.opaqueData.dataValue
}
}
}
}
};
axios.post(authorizeUrl, chargeJson, {responseType: 'text'})
.then((response) => {
// This is crazy but the Authorize.net API returns a string with BOM and totally
// screws the JSON response being parsed. So many hours wasted!
const txn = JSON.parse(stripBom(response.data)).transactionResponse;
if(!txn){
console.log('Declined request payload', chargeJson);
console.log('Declined response payload', response.data);
res.status(400).json({err: 'Your payment has declined. Please try again'});
return;
}
// order status if approved
let orderStatus = 'Paid';
if(txn && txn.responseCode !== '1'){
console.log('Declined response payload', response.data);
orderStatus = 'Declined';
}
let orderDoc = {
orderPaymentId: txn.transHash,
orderPaymentGateway: 'AuthorizeNet',
orderPaymentMessage: 'Your payment was successfully completed',
orderTotal: req.session.totalCartAmount,
orderEmail: req.body.shipEmail,
orderFirstname: req.body.shipFirstname,
orderLastname: req.body.shipLastname,
orderAddr1: req.body.shipAddr1,
orderAddr2: req.body.shipAddr2,
orderCountry: req.body.shipCountry,
orderState: req.body.shipState,
orderPostcode: req.body.shipPostcode,
orderPhoneNumber: req.body.shipPhoneNumber,
orderStatus: orderStatus,
orderDate: new Date(),
orderProducts: req.session.cart
};
// insert order into DB
db.orders.insert(orderDoc, (err, newDoc) => {
if(err){
console.info(err.stack);
}
// get the new ID
let newId = newDoc.insertedIds['0'];
// add to lunr index
common.indexOrders(req.app)
.then(() => {
// if approved, send email etc
if(orderStatus === 'Paid'){
// set the results
req.session.messageType = 'success';
req.session.message = 'Your payment was successfully completed';
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>${txn.transHash}</p>`;
// set payment results for email
let 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 payment with ${config.cartTitle}`, common.getEmailTemplate(paymentResults));
// redirect to outcome
res.status(200).json({orderId: newId});
}else{
// redirect to failure
req.session.messageType = 'danger';
req.session.message = 'Your payment has declined. Please try again';
req.session.paymentApproved = false;
req.session.paymentDetails = `<p><strong>Order ID: </strong>${newId}
</p><p><strong>Transaction ID: </strong> ${txn.transHash}</p>`;
res.status(400).json({err: true, orderId: newId});
}
});
});
})
.catch((err) => {
console.log('Error sending payment to API', err);
res.status(400).json({err: 'Your payment has declined. Please try again'});
});
});
module.exports = router;

View File

@ -15,21 +15,15 @@
<div class="row cart-row"> <div class="row cart-row">
<div class="col-xs-4 col-md-2"> <div class="col-xs-4 col-md-2">
{{#if productImage}} {{#if productImage}}
<img class="img-responsive" src="{{this.productImage}}" alt="{{this.title}} product image"> <img class="img-responsive" src="{{this.productImage}}" alt="{{this.title}} product image"> {{else}}
{{else}} <img class="img-responsive" src="/uploads/placeholder.png" alt="{{this.title}} product image"> {{/if}}
<img class="img-responsive" src="/uploads/placeholder.png" alt="{{this.title}} product image">
{{/if}}
</div> </div>
<div class="cart-item-row text-left col-xs-12 col-md-7"> <div class="cart-item-row text-left col-xs-12 col-md-7">
<p><a class="cart-link" href="/product/{{this.link}}">{{this.title}}</a></p>
<p> <p>
&nbsp;{{#each this.options}} <a class="cart-link" href="/product/{{this.link}}">{{this.title}}</a>
{{#if @last}} </p>
{{this}} <p>
{{else}} &nbsp;{{#each this.options}} {{#if @last}} {{this}} {{else}} {{this}} / {{/if}} {{/each}}
{{this}} /
{{/if}}
{{/each}}
</p> </p>
<p> <p>
<div class="form-group"> <div class="form-group">
@ -38,7 +32,8 @@
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-sm btn-primary btn-qty-minus" type="button">-</button> <button class="btn btn-sm btn-primary btn-qty-minus" type="button">-</button>
</span> </span>
<input type="number" class="form-control cart-product-quantity input-sm text-center" data-id="{{this.productId}}" id="{{@key}}" maxlength="2" value="{{this.quantity}}"> <input type="number" class="form-control cart-product-quantity input-sm text-center" data-id="{{this.productId}}" id="{{@key}}"
maxlength="2" value="{{this.quantity}}">
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-sm btn-primary btn-qty-add" type="button">+</button> <button class="btn btn-sm btn-primary btn-qty-add" type="button">+</button>
</span> </span>
@ -59,15 +54,18 @@
<div class="cart-contents-shipping col-md-12 no-pad-right"> <div class="cart-contents-shipping col-md-12 no-pad-right">
{{#ifCond session.shippingCostApplied '===' true}} {{#ifCond session.shippingCostApplied '===' true}}
<div class="text-right"> <div class="text-right">
Shipping: <strong>{{currencySymbol config.currencySymbol}}{{formatAmount config.flatShipping}}</strong> Shipping:
<strong>{{currencySymbol config.currencySymbol}}{{formatAmount config.flatShipping}}</strong>
</div> </div>
{{else}} {{else}}
<div class="text-right"> <div class="text-right">
Shipping: <strong>FREE</strong> Shipping:
<strong>FREE</strong>
</div> </div>
{{/ifCond}} {{/ifCond}}
<div class="text-right"> <div class="text-right">
Total: <strong>{{currencySymbol config.currencySymbol}}{{formatAmount session.totalCartAmount}}</strong> Total:
<strong>{{currencySymbol config.currencySymbol}}{{formatAmount session.totalCartAmount}}</strong>
</div> </div>
</div> </div>
</div> </div>
@ -94,8 +92,7 @@
<a href="/checkout" class="btn btn-default">Checkout</a> <a href="/checkout" class="btn btn-default">Checkout</a>
{{/ifCond}} {{/ifCond}}
</div> </div>
{{/ifCond}} {{/ifCond}} {{/if}}
{{/if}}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,61 @@
{{#ifCond paymentConfig.mode '==' 'test'}}
<script type="text/javascript"
src="https://jstest.authorize.net/v3/AcceptUI.js"
charset="utf-8">
</script>
{{else}}
<script type="text/javascript"
src="https://js.authorize.net/v3/AcceptUI.js"
charset="utf-8">
</script>
{{/ifCond}}
<script>
function responseHandler(data){
// Set fields from the form
data.shipEmail = $('#shipEmail').val();
data.shipFirstname = $('#shipFirstname').val();
data.shipLastname = $('#shipLastname').val();
data.shipAddr1 = $('#shipAddr1').val();
data.shipAddr2 = $('#shipAddr2').val();
data.shipCountry = $('#shipCountry').val();
data.shipState = $('#shipState').val();
data.shipPostcode = $('#shipPostcode').val();
data.shipPhoneNumber = $('#shipPhoneNumber').val();
$.ajax({
method: 'POST',
url: '/authorizenet/checkout_action',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(data)
})
.done(function(data){
if(data.orderId){
window.location = '/payment/' + data.orderId;
}else{
showNotification('Transaction approved', 'success', true);
}
})
.fail(function(data){
if(data.orderId){
window.location = '/payment/' + data.orderId;
}else{
showNotification('Your payment has declined. Please try again.', 'danger');
}
});
}
</script>
<div class="col-xs-12 col-md-12 text-center">
<form id="paymentForm" method="POST">
<input type="hidden" name="dataValue" id="dataValue" />
<input type="hidden" name="dataDescriptor" id="dataDescriptor" />
<button type="button"
class="AcceptUI btn btn-success"
data-billingAddressOptions='{"show":false, "required":false}'
data-apiLoginID="{{paymentConfig.loginId}}"
data-clientKey="{{paymentConfig.clientKey}}"
data-acceptUIFormBtnTxt="Submit"
data-acceptUIFormHeaderTxt="Card Information"
data-responseHandler="responseHandler">Process payment
</button>
</form>
</div>

View File

@ -31,6 +31,7 @@
{{/if}} {{/if}}
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action" method="post" role="form" data-toggle="validator" novalidate="false"> <form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action" method="post" role="form" data-toggle="validator" novalidate="false">
{{> payments/shipping-form}} {{> payments/shipping-form}}
{{#if session.customer}} {{#if session.customer}}
{{#ifCond config.paymentGateway '==' 'paypal'}} {{#ifCond config.paymentGateway '==' 'paypal'}}
{{> payments/paypal}} {{> payments/paypal}}
@ -50,6 +51,9 @@
{{#ifCond config.paymentGateway '==' 'stripe'}} {{#ifCond config.paymentGateway '==' 'stripe'}}
{{> payments/stripe}} {{> payments/stripe}}
{{/ifCond}} {{/ifCond}}
{{#ifCond config.paymentGateway '==' 'authorizenet'}}
{{> payments/authorizenet}}
{{/ifCond}}
{{/if}} {{/if}}
</div> </div>
</div> </div>