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);
}
}
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
const index = require('./routes/index');
@ -50,6 +57,7 @@ const admin = require('./routes/admin');
const customer = require('./routes/customer');
const paypal = require('./routes/payments/paypal');
const stripe = require('./routes/payments/stripe');
const authorizenet = require('./routes/payments/authorizenet');
const app = express();
@ -224,6 +232,7 @@ app.use('/', customer);
app.use('/admin', admin);
app.use('/paypal', paypal);
app.use('/stripe', stripe);
app.use('/authorizenet', authorizenet);
// catch 404 and forward to error handler
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": {
"type": "string",
"enum": ["paypal", "stripe"]
"enum": ["paypal", "stripe", "authorizenet"]
},
"databaseConnectionString": {
"format": "uri-template",

View File

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

View File

@ -50,6 +50,9 @@
{{#ifCond config.paymentGateway '==' 'stripe'}}
{{> payments/stripe}}
{{/ifCond}}
{{#ifCond config.paymentGateway '==' 'authorizenet'}}
{{> payments/authorizenet}}
{{/ifCond}}
{{/if}}
</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

@ -1,5 +1,5 @@
<div class="row">
<div class="col-xs-12 col-md-12">
<div class="col-xs-12 col-md-12">
{{#if pageCloseBtn}}
<div class="row {{checkout}}">
<div class="col-lg-12 text-right">
@ -11,91 +11,88 @@
<div class="panel-heading">Cart contents</div>
<div class="panel-body cart-body">
<div class="container-fluid">
{{#each session.cart}}
{{#each session.cart}}
<div class="row cart-row">
<div class="col-xs-4 col-md-2">
{{#if productImage}}
<img class="img-responsive" src="{{this.productImage}}" alt="{{this.title}} product image">
{{else}}
<img class="img-responsive" src="/uploads/placeholder.png" alt="{{this.title}} product image">
{{/if}}
<img class="img-responsive" src="{{this.productImage}}" alt="{{this.title}} product image"> {{else}}
<img class="img-responsive" src="/uploads/placeholder.png" alt="{{this.title}} product image"> {{/if}}
</div>
<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>
&nbsp;{{#each this.options}}
{{#if @last}}
{{this}}
{{else}}
{{this}} /
{{/if}}
{{/each}}
<a class="cart-link" href="/product/{{this.link}}">{{this.title}}</a>
</p>
<p>
<div class="form-group">
<div class="col-lg-5 no-pad-left">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-sm btn-primary btn-qty-minus" type="button">-</button>
</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}}">
<span class="input-group-btn">
<button class="btn btn-sm btn-primary btn-qty-add" type="button">+</button>
</span>
&nbsp;{{#each this.options}} {{#if @last}} {{this}} {{else}} {{this}} / {{/if}} {{/each}}
</p>
<p>
<div class="form-group">
<div class="col-lg-5 no-pad-left">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-sm btn-primary btn-qty-minus" type="button">-</button>
</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}}">
<span class="input-group-btn">
<button class="btn btn-sm btn-primary btn-qty-add" type="button">+</button>
</span>
</div>
</div>
</div>
</div>
</p>
</div>
<div class="col-xs-12 col-lg-4 cart-item-row text-right no-pad-right">
<strong>{{currencySymbol ../config.currencySymbol}}{{formatAmount this.totalItemPrice}}</strong>
</div>
</div>
{{/each}}
{{/each}}
</div>
<div class="container-fluid">
{{#if session.cart}}
{{#if session.cart}}
<div class="row">
<div class="cart-contents-shipping col-md-12 no-pad-right">
{{#ifCond session.shippingCostApplied '===' true}}
<div class="text-right">
Shipping: <strong>{{currencySymbol config.currencySymbol}}{{formatAmount config.flatShipping}}</strong>
</div>
<div class="text-right">
Shipping:
<strong>{{currencySymbol config.currencySymbol}}{{formatAmount config.flatShipping}}</strong>
</div>
{{else}}
<div class="text-right">
Shipping: <strong>FREE</strong>
</div>
<div class="text-right">
Shipping:
<strong>FREE</strong>
</div>
{{/ifCond}}
<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>
{{else}}
<div class="row">
<div class="cart-contents-shipping col-md-12 no-pad-right">
Cart empty
</div>
</div>
{{/if}}
{{else}}
<div class="row">
<div class="cart-contents-shipping col-md-12 no-pad-right">
Cart empty
</div>
</div>
{{/if}}
</div>
</div>
</div>
<div class="row">
{{#if session.cart}}
<div class="col-xs-6 col-lg-6 text-left">
<button class="btn btn-danger" id="empty-cart" type="button">Empty cart</button>
</div>
{{#ifCond page '!=' 'pay'}}
<div class="text-right col-xs-6 col-md-6">
{{#ifCond page '==' 'checkout'}}
<a href="/pay" class="btn btn-default">Pay now</a>
{{else}}
<a href="/checkout" class="btn btn-default">Checkout</a>
{{/ifCond}}
</div>
<div class="col-xs-6 col-lg-6 text-left">
<button class="btn btn-danger" id="empty-cart" type="button">Empty cart</button>
</div>
{{#ifCond page '!=' 'pay'}}
<div class="text-right col-xs-6 col-md-6">
{{#ifCond page '==' 'checkout'}}
<a href="/pay" class="btn btn-default">Pay now</a>
{{else}}
<a href="/checkout" class="btn btn-default">Checkout</a>
{{/ifCond}}
{{/if}}
</div>
{{/ifCond}} {{/if}}
</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}}
<form id="shipping-form" class="shipping-form" action="/{{config.paymentGateway}}/checkout_action" method="post" role="form" data-toggle="validator" novalidate="false">
{{> payments/shipping-form}}
{{#if session.customer}}
{{#ifCond config.paymentGateway '==' 'paypal'}}
{{> payments/paypal}}
@ -50,6 +51,9 @@
{{#ifCond config.paymentGateway '==' 'stripe'}}
{{> payments/stripe}}
{{/ifCond}}
{{#ifCond config.paymentGateway '==' 'authorizenet'}}
{{> payments/authorizenet}}
{{/ifCond}}
{{/if}}
</div>
</div>