diff --git a/app.js b/app.js index eaf9c60..93fc7d3 100644 --- a/app.js +++ b/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) => { diff --git a/config/authorizenet.json b/config/authorizenet.json new file mode 100644 index 0000000..3234774 --- /dev/null +++ b/config/authorizenet.json @@ -0,0 +1,6 @@ +{ + "loginId": "loginId", + "transactionKey": "transactionKey", + "clientKey": "clientKey", + "mode": "test" +} \ No newline at end of file diff --git a/config/authorizenetSchema.json b/config/authorizenetSchema.json new file mode 100644 index 0000000..e1e3800 --- /dev/null +++ b/config/authorizenetSchema.json @@ -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 +} \ No newline at end of file diff --git a/config/baseSchema.json b/config/baseSchema.json index 87b4087..321aa02 100644 --- a/config/baseSchema.json +++ b/config/baseSchema.json @@ -83,7 +83,7 @@ }, "paymentGateway": { "type": "string", - "enum": ["paypal", "stripe"] + "enum": ["paypal", "stripe", "authorizenet"] }, "databaseConnectionString": { "format": "uri-template", diff --git a/package.json b/package.json index 10d6afc..444b63a 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/public/themes/Cloth/pay.hbs b/public/themes/Cloth/pay.hbs index ab4e5b3..fe6d1f0 100644 --- a/public/themes/Cloth/pay.hbs +++ b/public/themes/Cloth/pay.hbs @@ -50,6 +50,9 @@ {{#ifCond config.paymentGateway '==' 'stripe'}} {{> payments/stripe}} {{/ifCond}} + {{#ifCond config.paymentGateway '==' 'authorizenet'}} + {{> payments/authorizenet}} + {{/ifCond}} {{/if}} diff --git a/routes/payments/authorizenet.js b/routes/payments/authorizenet.js new file mode 100644 index 0000000..61db321 --- /dev/null +++ b/routes/payments/authorizenet.js @@ -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 = `

Order ID: ${newId}

+

Transaction ID: ${txn.transHash}

`; + + // 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 = `

Order ID: ${newId} +

Transaction ID: ${txn.transHash}

`; + 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; diff --git a/views/partials/cart.hbs b/views/partials/cart.hbs index 3a9f21e..901d431 100644 --- a/views/partials/cart.hbs +++ b/views/partials/cart.hbs @@ -1,5 +1,5 @@ -
-
+
+
{{#if pageCloseBtn}}
@@ -11,91 +11,88 @@
Cart contents
- {{#each session.cart}} + {{#each session.cart}}
{{#if productImage}} - {{this.title}} product image - {{else}} - {{this.title}} product image - {{/if}} + {{this.title}} product image {{else}} + {{this.title}} product image {{/if}}
-

{{this.title}}

-  {{#each this.options}} - {{#if @last}} - {{this}} - {{else}} - {{this}} / - {{/if}} - {{/each}} + {{this.title}}

-

-
-
- - - - - - - +  {{#each this.options}} {{#if @last}} {{this}} {{else}} {{this}} / {{/if}} {{/each}} +

+

+

+
+
+ + + + + + + +
-

{{currencySymbol ../config.currencySymbol}}{{formatAmount this.totalItemPrice}} -
+
- {{/each}} + {{/each}}
- {{#if session.cart}} + {{#if session.cart}}
{{#ifCond session.shippingCostApplied '===' true}} -
- Shipping: {{currencySymbol config.currencySymbol}}{{formatAmount config.flatShipping}} -
- {{else}} -
- Shipping: FREE -
+
+ Shipping: + {{currencySymbol config.currencySymbol}}{{formatAmount config.flatShipping}} +
+ {{else}} +
+ Shipping: + FREE +
{{/ifCond}}
- Total: {{currencySymbol config.currencySymbol}}{{formatAmount session.totalCartAmount}} + Total: + {{currencySymbol config.currencySymbol}}{{formatAmount session.totalCartAmount}}
- {{else}} -
-
- Cart empty -
-
- {{/if}} + {{else}} +
+
+ Cart empty +
+
+ {{/if}}
{{#if session.cart}} -
- -
- {{#ifCond page '!=' 'pay'}} -
- {{#ifCond page '==' 'checkout'}} - Pay now - {{else}} - Checkout - {{/ifCond}} -
+
+ +
+ {{#ifCond page '!=' 'pay'}} +
+ {{#ifCond page '==' 'checkout'}} + Pay now + {{else}} + Checkout {{/ifCond}} - {{/if}} +
+ {{/ifCond}} {{/if}}
-
+
\ No newline at end of file diff --git a/views/partials/payments/authorizenet.hbs b/views/partials/payments/authorizenet.hbs new file mode 100644 index 0000000..42b6555 --- /dev/null +++ b/views/partials/payments/authorizenet.hbs @@ -0,0 +1,61 @@ +{{#ifCond paymentConfig.mode '==' 'test'}} + +{{else}} + +{{/ifCond}} + +
+
+ + + +
+
\ No newline at end of file diff --git a/views/pay.hbs b/views/pay.hbs index ab4e5b3..03e3ff3 100644 --- a/views/pay.hbs +++ b/views/pay.hbs @@ -31,6 +31,7 @@ {{/if}}
{{> 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}}