parent
e9e7625789
commit
c3c29d04f1
19
README.md
19
README.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
![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 and Authorize.net payments.
|
||||
`expressCart` is a fully functional shopping cart built in Node.js (Express, MongoDB) with Stripe, PayPal, Authorize.net and Adyen payments.
|
||||
|
||||
[![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)
|
||||
|
@ -263,6 +263,23 @@ The Authorize.net config file is located: `/config/authorizenet.json`. A example
|
|||
|
||||
Note: The credentials are obtained from your Authorize.net account dashboard.
|
||||
|
||||
##### Adyen (Payments)
|
||||
|
||||
The Adyen config file is located: `/config/adyen.json`. A example Adyen settings file is provided:
|
||||
|
||||
```
|
||||
{
|
||||
"environment": "TEST",
|
||||
"apiKey": "this_is_not_real",
|
||||
"publicKey": "this_is_not_real",
|
||||
"merchantAccount": "this_is_not_real",
|
||||
"statementDescriptor": "a_statement_descriptor",
|
||||
"currency": "AUD"
|
||||
}
|
||||
```
|
||||
|
||||
Note: The `publicKey`, `apiKey` and `merchantAccount` is obtained from your Adyen account dashboard.
|
||||
|
||||
## Email settings
|
||||
|
||||
You will need to configure your SMTP details for expressCart to send email receipts to your customers.
|
||||
|
|
9
app.js
9
app.js
|
@ -54,6 +54,13 @@ switch(config.paymentGateway){
|
|||
process.exit(2);
|
||||
}
|
||||
break;
|
||||
|
||||
case'adyen':
|
||||
if(ajv.validate(require('./config/adyenSchema'), require('./config/adyen.json')) === false){
|
||||
console.log(colors.red(`adyen config is incorrect: ${ajv.errorsText()}`));
|
||||
process.exit(2);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// require the routes
|
||||
|
@ -66,6 +73,7 @@ const user = require('./routes/user');
|
|||
const paypal = require('./routes/payments/paypal');
|
||||
const stripe = require('./routes/payments/stripe');
|
||||
const authorizenet = require('./routes/payments/authorizenet');
|
||||
const adyen = require('./routes/payments/adyen');
|
||||
|
||||
const app = express();
|
||||
|
||||
|
@ -324,6 +332,7 @@ app.use('/', admin);
|
|||
app.use('/paypal', paypal);
|
||||
app.use('/stripe', stripe);
|
||||
app.use('/authorizenet', authorizenet);
|
||||
app.use('/adyen', adyen);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use((req, res, next) => {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"environment": "TEST",
|
||||
"apiKey": "this_is_not_real",
|
||||
"publicKey": "this_is_not_real",
|
||||
"merchantAccount": "this_is_not_real",
|
||||
"statementDescriptor": "a_statement_descriptor",
|
||||
"currency": "AUD"
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"properties": {
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"enum": ["TEST", "LIVE"]
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"merchantAccount": {
|
||||
"type": "string"
|
||||
},
|
||||
"statementDescriptor": {
|
||||
"type": "string"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"environment",
|
||||
"apiKey",
|
||||
"publicKey",
|
||||
"merchantAccount",
|
||||
"statementDescriptor",
|
||||
"currency"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
|
@ -83,7 +83,7 @@
|
|||
},
|
||||
"paymentGateway": {
|
||||
"type": "string",
|
||||
"enum": ["paypal", "stripe", "authorizenet"]
|
||||
"enum": ["paypal", "stripe", "authorizenet", "adyen"]
|
||||
},
|
||||
"databaseConnectionString": {
|
||||
"type": "string"
|
||||
|
|
|
@ -163,5 +163,6 @@
|
|||
"List": "List",
|
||||
"Order type": "Order type",
|
||||
"New user": "New user",
|
||||
"Payment ID": "Payment ID"
|
||||
"Payment ID": "Payment ID",
|
||||
"Payment Message": "Payment Message"
|
||||
}
|
|
@ -4,6 +4,14 @@
|
|||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@adyen/api-library": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@adyen/api-library/-/api-library-2.1.7.tgz",
|
||||
"integrity": "sha512-EQXOyXJ6A4ceqWYIiAwXdISVWVZfvQEPhf4roYkLiOL01GSTL0waWucUvS8tXOBOtSzrhTw1zUqC0YGBjULaRA==",
|
||||
"requires": {
|
||||
"https-proxy-agent": "3.0.1"
|
||||
}
|
||||
},
|
||||
"@ava/babel-plugin-throws-helper": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ava/babel-plugin-throws-helper/-/babel-plugin-throws-helper-4.0.0.tgz",
|
||||
|
@ -508,6 +516,14 @@
|
|||
"integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==",
|
||||
"dev": true
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
|
||||
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
|
||||
"requires": {
|
||||
"es6-promisify": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
|
||||
|
@ -2865,6 +2881,19 @@
|
|||
"es6-symbol": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
|
||||
"requires": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"es6-symbol": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
|
||||
|
@ -5236,6 +5265,30 @@
|
|||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz",
|
||||
"integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==",
|
||||
"requires": {
|
||||
"agent-base": "^4.3.0",
|
||||
"debug": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/i18n/-/i18n-0.8.4.tgz",
|
||||
|
|
|
@ -18,13 +18,14 @@
|
|||
],
|
||||
"verbose": true,
|
||||
"environmentVariables": {
|
||||
"NODE_ENV": "test"
|
||||
}
|
||||
"NODE_ENV": "test"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": "10.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adyen/api-library": "^2.1.7",
|
||||
"ajv": "^6.10.2",
|
||||
"async": "^2.6.3",
|
||||
"axios": "^0.19.0",
|
||||
|
@ -87,6 +88,9 @@
|
|||
"nodejs",
|
||||
"ecommerce",
|
||||
"paypal",
|
||||
"stripe",
|
||||
"authorise.net",
|
||||
"adyen",
|
||||
"lunr",
|
||||
"cart",
|
||||
"shopping"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable prefer-arrow-callback, no-var, no-tabs */
|
||||
/* eslint-disable prefer-arrow-callback, no-var, no-tabs */
|
||||
/* globals AdyenCheckout */
|
||||
$(document).ready(function (){
|
||||
// setup if material theme
|
||||
if($('#cartTheme').val() === 'Material'){
|
||||
|
@ -310,6 +311,63 @@ $(document).ready(function (){
|
|||
}
|
||||
});
|
||||
|
||||
if($('#adyen-dropin').length > 0){
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/adyen/setup'
|
||||
})
|
||||
.done(function(response){
|
||||
const configuration = {
|
||||
locale: 'en-AU',
|
||||
environment: response.environment.toLowerCase(),
|
||||
originKey: response.publicKey,
|
||||
paymentMethodsResponse: response.paymentsResponse
|
||||
};
|
||||
const checkout = new AdyenCheckout(configuration);
|
||||
checkout
|
||||
.create('dropin', {
|
||||
paymentMethodsConfiguration: {
|
||||
card: {
|
||||
hasHolderName: false,
|
||||
holderNameRequired: false,
|
||||
enableStoreDetails: false,
|
||||
groupTypes: ['mc', 'visa'],
|
||||
name: 'Credit or debit card'
|
||||
}
|
||||
},
|
||||
onSubmit: (state, dropin) => {
|
||||
if($('#shipping-form').validator('validate').has('.has-error').length === 0){
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/adyen/checkout_action',
|
||||
data: {
|
||||
shipEmail: $('#shipEmail').val(),
|
||||
shipFirstname: $('#shipFirstname').val(),
|
||||
shipLastname: $('#shipLastname').val(),
|
||||
shipAddr1: $('#shipAddr1').val(),
|
||||
shipAddr2: $('#shipAddr2').val(),
|
||||
shipCountry: $('#shipCountry').val(),
|
||||
shipState: $('#shipState').val(),
|
||||
shipPostcode: $('#shipPostcode').val(),
|
||||
shipPhoneNumber: $('#shipPhoneNumber').val(),
|
||||
payment: JSON.stringify(state.data.paymentMethod)
|
||||
}
|
||||
}).done((response) => {
|
||||
window.location = '/payment/' + response.paymentId;
|
||||
}).fail((response) => {
|
||||
console.log('Response', response);
|
||||
showNotification('Failed to complete transaction', 'danger', true);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.mount('#adyen-dropin');
|
||||
})
|
||||
.fail(function(msg){
|
||||
showNotification(msg.responseJSON.message, 'danger');
|
||||
});
|
||||
};
|
||||
|
||||
// call update settings API
|
||||
$('#settingsForm').validator().on('submit', function(e){
|
||||
if(!e.isDefaultPrevented()){
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,138 @@
|
|||
const express = require('express');
|
||||
const common = require('../../lib/common');
|
||||
const { indexOrders } = require('../../lib/indexing');
|
||||
const numeral = require('numeral');
|
||||
const { Client, CheckoutAPI } = require('@adyen/api-library');
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/setup', async (req, res, next) => {
|
||||
const adyenConfig = common.getPaymentConfig();
|
||||
|
||||
const client = new Client({
|
||||
apiKey: adyenConfig.apiKey,
|
||||
environment: adyenConfig.environment
|
||||
});
|
||||
const checkout = new CheckoutAPI(client);
|
||||
let paymentsResponse;
|
||||
try{
|
||||
paymentsResponse = await checkout.paymentMethods({
|
||||
amount: {
|
||||
currency: 'AUD',
|
||||
value: 0
|
||||
},
|
||||
countryCode: 'AU',
|
||||
channel: 'Web',
|
||||
merchantAccount: adyenConfig.merchantAccount
|
||||
});
|
||||
}catch(ex){
|
||||
console.log('Exception getting supported payment methods', ex.message);
|
||||
res.status(400).json({ message: 'Failed to retrieve payment methods.' + ex.message });
|
||||
}
|
||||
res.status(200).json({
|
||||
paymentsResponse,
|
||||
environment: adyenConfig.environment,
|
||||
publicKey: adyenConfig.publicKey
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/checkout_action', async (req, res, next) => {
|
||||
const db = req.app.db;
|
||||
const config = req.app.config;
|
||||
const adyenConfig = common.getPaymentConfig();
|
||||
|
||||
const client = new Client({
|
||||
apiKey: adyenConfig.apiKey,
|
||||
environment: adyenConfig.environment
|
||||
});
|
||||
const checkout = new CheckoutAPI(client);
|
||||
let response;
|
||||
try{
|
||||
response = await checkout.payments({
|
||||
shopperInteraction: 'Ecommerce',
|
||||
amount: {
|
||||
currency: adyenConfig.currency,
|
||||
value: numeral(req.session.totalCartAmount).format('0.00').replace('.', '')
|
||||
},
|
||||
paymentMethod: JSON.parse(req.body.payment),
|
||||
reference: adyenConfig.statementDescriptor,
|
||||
merchantAccount: adyenConfig.merchantAccount,
|
||||
shopperStatement: adyenConfig.statementDescriptor
|
||||
});
|
||||
}catch(ex){
|
||||
console.log('Payment exception', ex.message);
|
||||
req.session.messageType = 'danger';
|
||||
req.session.message = 'Card declined. Contact card issuer';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update response
|
||||
let paymentStatus = 'Paid';
|
||||
if(response && response.resultCode !== 'Authorised'){
|
||||
paymentStatus = 'Declined';
|
||||
}
|
||||
|
||||
// new order doc
|
||||
const orderDoc = {
|
||||
orderPaymentId: response.pspReference,
|
||||
orderPaymentGateway: 'Adyen',
|
||||
orderPaymentMessage: response.refusalReason,
|
||||
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,
|
||||
orderComment: req.body.orderComment,
|
||||
orderStatus: paymentStatus,
|
||||
orderDate: new Date(),
|
||||
orderProducts: req.session.cart,
|
||||
orderType: 'Single'
|
||||
};
|
||||
|
||||
// insert order into DB
|
||||
const newOrder = await db.orders.insertOne(orderDoc);
|
||||
|
||||
// get the new ID
|
||||
const newId = newOrder.insertedId;
|
||||
|
||||
// add to lunr index
|
||||
indexOrders(req.app)
|
||||
.then(() => {
|
||||
// Process the result
|
||||
if(paymentStatus === 'Paid'){
|
||||
// set the results
|
||||
req.session.messageType = 'success';
|
||||
req.session.message = 'Your payment was successfully completed';
|
||||
req.session.paymentEmailAddr = orderDoc.orderEmail;
|
||||
req.session.paymentApproved = true;
|
||||
req.session.paymentDetails = '<p><strong>Order ID: </strong>' + newId + '</p><p><strong>Transaction ID: </strong>' + response.pspReference + '</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 payment with ' + config.cartTitle, common.getEmailTemplate(paymentResults));
|
||||
}
|
||||
res.status(200).json({ paymentId: newId });
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -28,6 +28,9 @@
|
|||
<li class="list-group-item"><strong> {{ @root.__ "Order ID" }}: </strong><span class="pull-right">{{result._id}}</span></li>
|
||||
<li class="list-group-item"><strong> {{ @root.__ "Payment Gateway ref" }}: </strong><span class="pull-right">{{result.orderPaymentId}}</span></li>
|
||||
<li class="list-group-item"><strong> {{ @root.__ "Payment Gateway" }}: </strong><span class="pull-right">{{result.orderPaymentGateway}}</span></li>
|
||||
{{#if result.orderPaymentMessage}}
|
||||
<li class="list-group-item"><strong> {{ @root.__ "Payment Message" }}: </strong><span class="pull-right">{{result.orderPaymentMessage}}</span></li>
|
||||
{{/if}}
|
||||
<li class="list-group-item"><strong> {{ @root.__ "Order total amount" }}: </strong><span class="pull-right">{{currencySymbol config.currencySymbol}}{{formatAmount result.orderTotal}}</span></li>
|
||||
<li class="list-group-item"><strong> {{ @root.__ "Email address" }}: </strong><span class="pull-right">{{result.orderEmail}}</span></li>
|
||||
<li class="list-group-item"><strong> {{ @root.__ "First name" }}: </strong><span class="pull-right">{{result.orderFirstname}}</span></li>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="col-lg-3">
|
||||
<h2>Menu</h2>
|
||||
<h2> </h2>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item"><strong>Products</strong></li>
|
||||
{{#ifCond session.isAdmin '===' true}}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<div class="col-xs-12 col s12 text-center">
|
||||
<div id="adyen-dropin"></div>
|
||||
<link rel="stylesheet" href="https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/3.2.0/adyen.css"/>
|
||||
<script src="https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/3.2.0/adyen.js"></script>
|
||||
<input type="hidden" id="environment" value="{{@root.paymentConfig.environment}}">
|
||||
<input type="hidden" id="publicKey" value="{{@root.paymentConfig.publicKey}}">
|
||||
</div>
|
|
@ -60,6 +60,9 @@
|
|||
{{#ifCond config.paymentGateway '==' 'authorizenet'}}
|
||||
{{> partials/payments/authorizenet}}
|
||||
{{/ifCond}}
|
||||
{{#ifCond config.paymentGateway '==' 'adyen'}}
|
||||
{{> partials/payments/adyen}}
|
||||
{{/ifCond}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -54,6 +54,9 @@
|
|||
{{#ifCond config.paymentGateway '==' 'authorizenet'}}
|
||||
{{> partials/payments/authorizenet}}
|
||||
{{/ifCond}}
|
||||
{{#ifCond config.paymentGateway '==' 'adyen'}}
|
||||
{{> partials/payments/adyen}}
|
||||
{{/ifCond}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -54,6 +54,9 @@
|
|||
{{#ifCond config.paymentGateway '==' 'authorizenet'}}
|
||||
{{> partials/payments/authorizenet}}
|
||||
{{/ifCond}}
|
||||
{{#ifCond config.paymentGateway '==' 'adyen'}}
|
||||
{{> partials/payments/adyen}}
|
||||
{{/ifCond}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue