Added authorize.net payments
parent
2f9911fd1e
commit
70105e73bc
9
app.js
9
app.js
|
@ -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) => {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"loginId": "loginId",
|
||||
"transactionKey": "transactionKey",
|
||||
"clientKey": "clientKey",
|
||||
"mode": "test"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -83,7 +83,7 @@
|
|||
},
|
||||
"paymentGateway": {
|
||||
"type": "string",
|
||||
"enum": ["paypal", "stripe"]
|
||||
"enum": ["paypal", "stripe", "authorizenet"]
|
||||
},
|
||||
"databaseConnectionString": {
|
||||
"format": "uri-template",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -50,6 +50,9 @@
|
|||
{{#ifCond config.paymentGateway '==' 'stripe'}}
|
||||
{{> payments/stripe}}
|
||||
{{/ifCond}}
|
||||
{{#ifCond config.paymentGateway '==' 'authorizenet'}}
|
||||
{{> payments/authorizenet}}
|
||||
{{/ifCond}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
|
@ -1,5 +1,5 @@
|
|||
<div class="row">
|
||||
<div class="col-xs-12 col-md-12">
|
||||
<div class="row">
|
||||
<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>
|
||||
{{#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>
|
||||
{{#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>
|
||||
</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>
|
||||
{{else}}
|
||||
<div class="text-right">
|
||||
Shipping: <strong>FREE</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>
|
||||
{{/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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue