mirror of
https://github.com/janishutz/libreevent.git
synced 2025-11-25 13:24:24 +00:00
add payrexx as payment provider
This commit is contained in:
@@ -74,6 +74,10 @@ class GETHandler {
|
|||||||
} );
|
} );
|
||||||
} else if ( call === 'getCurrency' ) {
|
} else if ( call === 'getCurrency' ) {
|
||||||
resolve( this.settings.currency );
|
resolve( this.settings.currency );
|
||||||
|
} else if ( call === 'getAdminAccounts' ) {
|
||||||
|
// TODO: Finish
|
||||||
|
} else if ( call === 'getSettings' ) {
|
||||||
|
resolve( this.settings );
|
||||||
} else {
|
} else {
|
||||||
reject( { 'code': 404, 'error': 'Route not found' } );
|
reject( { 'code': 404, 'error': 'Route not found' } );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,15 @@ class POSTHandler {
|
|||||||
} ).catch( error => {
|
} ).catch( error => {
|
||||||
reject( { 'code': 500, 'error': error } );
|
reject( { 'code': 500, 'error': error } );
|
||||||
} );
|
} );
|
||||||
|
} else if ( call === 'createAdminAccount' ) {
|
||||||
|
// TODO: Finish
|
||||||
|
} else if ( call === 'updateAdminAccount' ) {
|
||||||
|
// TODO: Finish
|
||||||
|
} else if ( call === 'deleteAdminAccount' ) {
|
||||||
|
// TODO: Finish
|
||||||
|
} else if ( call === 'updateSettings' ) {
|
||||||
|
// TODO: Finish
|
||||||
|
// TODO: Parse all events and update currency
|
||||||
} else {
|
} else {
|
||||||
reject( { 'code': 404, 'error': 'Route not found' } );
|
reject( { 'code': 404, 'error': 'Route not found' } );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ if ( settings.init ) {
|
|||||||
|
|
||||||
console.log( '[ Server ] loading plugins' );
|
console.log( '[ Server ] loading plugins' );
|
||||||
// TODO: load dynamically
|
// TODO: load dynamically
|
||||||
require( './backend/plugins/payments/stripe/stripeRoutes.js' )( app, settings ); // stripe routes
|
// require( './backend/plugins/payments/stripe/stripeRoutes.js' )( app, settings ); // stripe routes
|
||||||
|
require( './backend/plugins/payments/payrexx/payrexxRoutes.js' )( app, settings ); // payrexx routes
|
||||||
require( './backend/payments/paymentRoutes.js' )( app, settings ); // payment routes
|
require( './backend/payments/paymentRoutes.js' )( app, settings ); // payment routes
|
||||||
|
|
||||||
app.use( ( request, response ) => {
|
app.use( ( request, response ) => {
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ module.exports = ( app, settings ) => {
|
|||||||
res.sendFile( path.join( __dirname + '/../../ui/en/payments/canceled.html' ) );
|
res.sendFile( path.join( __dirname + '/../../ui/en/payments/canceled.html' ) );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
app.get( '/payments/failed', ( req, res ) => {
|
||||||
|
res.sendFile( path.join( __dirname + '/../../ui/en/payments/failed.html' ) );
|
||||||
|
} );
|
||||||
|
|
||||||
app.get( '/tickets/tickets.pdf', ( req, res ) => {
|
app.get( '/tickets/tickets.pdf', ( req, res ) => {
|
||||||
if ( req.session.id ) {
|
if ( req.session.id ) {
|
||||||
fs.readFile( path.join( __dirname + '/../tickets/store/' + req.session.id + '.pdf' ), ( error, data ) => {
|
fs.readFile( path.join( __dirname + '/../tickets/store/' + req.session.id + '.pdf' ), ( error, data ) => {
|
||||||
|
|||||||
@@ -4,28 +4,7 @@ All payment gateway integration plugins have to be in their own folders. This fo
|
|||||||
You will also need to add documentation for the user to set up the payment gateway. Read on below to find out how.
|
You will also need to add documentation for the user to set up the payment gateway. Read on below to find out how.
|
||||||
|
|
||||||
## Setting up the routes.js file
|
## Setting up the routes.js file
|
||||||
In the routes.js file you should have at least the following code:
|
Take some inspiration of the stripe or payrexx setup as these are officially supported by the system and have been developed by the original creator.
|
||||||
|
|
||||||
```
|
|
||||||
module.exports = ( app, settings ) => {
|
|
||||||
app.post( '/payments/prepare', ( req, res ) => {
|
|
||||||
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/payments/status', ( request, response ) => {
|
|
||||||
response.writeHead( 200, {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
} );
|
|
||||||
response.status( 200 );
|
|
||||||
response.flushHeaders();
|
|
||||||
response.write( 'data: connected\n\n' );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Take some inspiration of the stripe or adyen setup as these are officially supported by the system and have been developed by the original creator.
|
|
||||||
|
|
||||||
The express.js routes it has to expose are the following:
|
The express.js routes it has to expose are the following:
|
||||||
|
|
||||||
@@ -37,3 +16,7 @@ It can contain any number of (not interfering) routes. Please always use the /pa
|
|||||||
|
|
||||||
## The plugin.json file
|
## The plugin.json file
|
||||||
The plugin.json file should look as follows:
|
The plugin.json file should look as follows:
|
||||||
|
|
||||||
|
## configOption.json
|
||||||
|
This file contains the settings that should be available in the settings page of libreevent. It should contain the following fields, as required by the settings.vue module.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* libreevent - module.payrexx.js
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 08/12/2023, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require( 'fs' );
|
||||||
|
const path = require( 'path' );
|
||||||
|
const qs = require( 'qs' );
|
||||||
|
const axios = require( 'axios' );
|
||||||
|
const Base64 = require( 'crypto-js/enc-base64' );
|
||||||
|
const hmacSHA256 = require( 'crypto-js/hmac-sha256' );
|
||||||
|
const payrexxConfig = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../../../config/payments.config.secret.json' ) ) )[ 'payrexx' ];
|
||||||
|
|
||||||
|
const baseUrl = 'https://api.payrexx.com/v1.0/';
|
||||||
|
const instance = payrexxConfig.instance;
|
||||||
|
const secret = payrexxConfig.APISecret;
|
||||||
|
|
||||||
|
exports.init = function () {
|
||||||
|
function buildSignature ( query = '' ) {
|
||||||
|
return Base64.stringify( hmacSHA256( query, secret ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBaseUrl ( path ) {
|
||||||
|
return baseUrl + path + '?instance=' + instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getGateway: function ( id ) {
|
||||||
|
const baseUrl = buildBaseUrl( 'Gateway/' + id + '/' );
|
||||||
|
const url = baseUrl + '&ApiSignature=' + buildSignature();
|
||||||
|
return axios.get( url );
|
||||||
|
},
|
||||||
|
createGateway: function ( params ) {
|
||||||
|
if ( !params.amount ) {
|
||||||
|
throw new Error( 'Amount required!' );
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultParams = {
|
||||||
|
currency: 'CHF'
|
||||||
|
};
|
||||||
|
|
||||||
|
let queryParams = Object.assign( {}, defaultParams, params );
|
||||||
|
|
||||||
|
const queryStr = qs.stringify( queryParams, { format: 'RFC1738' } );
|
||||||
|
const signature = buildSignature( queryStr );
|
||||||
|
|
||||||
|
queryParams.ApiSignature = signature;
|
||||||
|
const queryStrSigned = qs.stringify( queryParams );
|
||||||
|
|
||||||
|
const baseUrl = buildBaseUrl( 'Gateway/' );
|
||||||
|
return axios.post( baseUrl, queryStrSigned );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* libreevent - payrexxRoutes.js
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 08/12/2023, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require( 'fs' );
|
||||||
|
const path = require( 'path' );
|
||||||
|
const db = require( '../../../db/db.js' );
|
||||||
|
const payrexxConfig = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../../../config/payments.config.secret.json' ) ) )[ 'payrexx' ];
|
||||||
|
const bodyParser = require( 'body-parser' );
|
||||||
|
const ticket = require( '../../../tickets/ticketGenerator.js' );
|
||||||
|
const payrexxModule = require( './module.payrexx.js' );
|
||||||
|
const payrexx = payrexxModule.init();
|
||||||
|
const TicketGenerator = new ticket();
|
||||||
|
|
||||||
|
|
||||||
|
let sessionReference = {};
|
||||||
|
let waitingClients = {};
|
||||||
|
let pendingPayments = {};
|
||||||
|
let paymentOk = {};
|
||||||
|
|
||||||
|
module.exports = ( app, settings ) => {
|
||||||
|
app.post( '/payments/prepare', bodyParser.json(), ( req, res ) => {
|
||||||
|
if ( req.session.loggedInUser ) {
|
||||||
|
let purchase = {
|
||||||
|
'successRedirectUrl': settings.yourDomain + '/payments/success',
|
||||||
|
'cancelRedirectUrl': settings.yourDomain + '/payments/canceled',
|
||||||
|
'failedRedirectUrl': settings.yourDomain + '/payments/failed',
|
||||||
|
'currency': settings.currency,
|
||||||
|
'basket': [],
|
||||||
|
'pm': payrexxConfig.paymentMethods,
|
||||||
|
'amount': 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.getDataSimple( 'temp', 'user_id', req.session.id ).then( dat => {
|
||||||
|
if ( dat[ 0 ] ) {
|
||||||
|
db.getJSONData( 'events' ).then( events => {
|
||||||
|
let data = JSON.parse( dat[ 0 ].data );
|
||||||
|
( async () => {
|
||||||
|
for ( let event in data ) {
|
||||||
|
for ( let item in data[ event ] ) {
|
||||||
|
purchase[ 'basket' ].push( {
|
||||||
|
'name': data[ event ][ item ].name,
|
||||||
|
'quantity': data[ event ][ item ].count ?? 1,
|
||||||
|
'amount': Math.round( parseFloat( events[ event ][ 'categories' ][ data[ event ][ item ].category ].price[ data[ event ][ item ][ 'ticketOption' ] ] ) * 100 ),
|
||||||
|
} );
|
||||||
|
purchase[ 'amount' ] += Math.round( parseFloat( events[ event ][ 'categories' ][ data[ event ][ item ].category ].price[ data[ event ][ item ][ 'ticketOption' ] ] ) * 100 ) * ( data[ event ][ item ].count ?? 1 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await payrexx.createGateway( purchase );
|
||||||
|
if ( response.status === 200 ) {
|
||||||
|
const session = response.data.data[ 0 ];
|
||||||
|
sessionReference[ session.id ] = { 'tok': req.session.id, 'email': req.session.username };
|
||||||
|
pendingPayments[ req.session.id ] = true;
|
||||||
|
res.send( session.link );
|
||||||
|
} else {
|
||||||
|
res.status( 500 ).send( 'ERR_PAYMENT' );
|
||||||
|
}
|
||||||
|
} )();
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
res.status( 400 ).send( 'ERR_UID_NOT_FOUND' );
|
||||||
|
}
|
||||||
|
} ).catch( error => {
|
||||||
|
console.error( '[ STRIPE ] DB ERROR: ' + error );
|
||||||
|
res.status( 500 ).send( 'ERR_DB' );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
res.status( 403 ).send( 'ERR_UNAUTHORIZED' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
app.get( '/payments/status', ( request, response ) => {
|
||||||
|
response.writeHead( 200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
} );
|
||||||
|
response.status( 200 );
|
||||||
|
response.flushHeaders();
|
||||||
|
response.write( 'data: connected\n\n' );
|
||||||
|
waitingClients[ request.session.id ] = response;
|
||||||
|
const ping = setInterval( () => {
|
||||||
|
if ( !pendingPayments[ request.session.id ] ) {
|
||||||
|
const stat = TicketGenerator.getGenerationStatus( request.session.id );
|
||||||
|
if ( stat === 'done' ) {
|
||||||
|
clearInterval( ping );
|
||||||
|
setTimeout( () => {
|
||||||
|
response.write( 'data: ready\n\n' );
|
||||||
|
response.end();
|
||||||
|
delete waitingClients[ request.session.id ];
|
||||||
|
}, 2000 );
|
||||||
|
} else if ( stat === 'noTicket' ) {
|
||||||
|
clearInterval( ping );
|
||||||
|
response.write( 'data: noData\n\n' );
|
||||||
|
response.end();
|
||||||
|
delete waitingClients[ request.session.id ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000 );
|
||||||
|
} );
|
||||||
|
|
||||||
|
app.get( '/user/2fa/ping', ( request, response ) => {
|
||||||
|
if ( paymentOk[ request.session.id ] === 'ok' ) {
|
||||||
|
delete paymentOk[ request.session.id ];
|
||||||
|
response.send( { 'status': 'paymentOk' } );
|
||||||
|
} else {
|
||||||
|
if ( !pendingPayments[ request.session.id ] ) {
|
||||||
|
const stat = TicketGenerator.getGenerationStatus( request.session.id );
|
||||||
|
if ( stat === 'done' ) {
|
||||||
|
response.send( { 'status': 'ticketOk' } );
|
||||||
|
} else if ( stat === 'noTicket' ) {
|
||||||
|
response.send( { 'status': 'noTicket' } );
|
||||||
|
} else {
|
||||||
|
response.send( '' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.send( '' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
app.post( '/payments/webhook', bodyParser.raw( { type: 'application/json' } ), async ( req, res ) => {
|
||||||
|
console.error( req.body );
|
||||||
|
const response = await payrexx.getGateway( req.body.id );
|
||||||
|
|
||||||
|
if ( response.status === 200 ) {
|
||||||
|
const gateway = response.data.data[ 0 ];
|
||||||
|
|
||||||
|
res.status( 200 ).end();
|
||||||
|
if ( gateway.status === 'confirmed' ) {
|
||||||
|
setTimeout( () => {
|
||||||
|
if ( waitingClients[ sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ] ) {
|
||||||
|
waitingClients[ sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ].write( 'data: paymentOk\n\n' );
|
||||||
|
}
|
||||||
|
}, 1000 );
|
||||||
|
db.getDataSimple( 'temp', 'user_id', sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ).then( dat => {
|
||||||
|
db.getDataSimple( 'users', 'email', sessionReference[ response.data.data[ 0 ].id ][ 'email' ] ).then( user => {
|
||||||
|
if ( user[ 0 ] ) {
|
||||||
|
const tickets = JSON.parse( dat[ 0 ].data );
|
||||||
|
db.writeDataSimple( 'orders', 'account_id', user[ 0 ].account_id, { 'account_id': user[ 0 ].account_id, 'tickets': dat[ 0 ].data, 'order_name': sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] } ).then( () => {
|
||||||
|
console.log( sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] );
|
||||||
|
TicketGenerator.generateTickets( sessionReference[ response.data.data[ 0 ].id ] );
|
||||||
|
db.getJSONData( 'booked' ).then( ret => {
|
||||||
|
let booked = ret ?? {};
|
||||||
|
for ( let event in tickets ) {
|
||||||
|
if ( !booked[ String( event ) ] ) {
|
||||||
|
booked[ String( event ) ] = {};
|
||||||
|
}
|
||||||
|
for ( let tik in tickets[ event ] ) {
|
||||||
|
booked[ event ][ tik ] = tickets[ event ][ tik ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.writeJSONData( 'booked', booked ).then( () => {
|
||||||
|
delete pendingPayments[ sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ];
|
||||||
|
} );
|
||||||
|
db.deleteDataSimple( 'temp', 'user_id', sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ).catch( error => {
|
||||||
|
console.error( '[ STRIPE ] ERROR whilst deleting data from DB: ' + error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
console.log( sessionReference[ response.data.data[ 0 ].id ][ 'email' ] );
|
||||||
|
console.error( 'user not found' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} ).catch( err => {
|
||||||
|
console.error( err );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
};
|
||||||
@@ -10,21 +10,19 @@
|
|||||||
const fs = require( 'fs' );
|
const fs = require( 'fs' );
|
||||||
const path = require( 'path' );
|
const path = require( 'path' );
|
||||||
const db = require( '../../../db/db.js' );
|
const db = require( '../../../db/db.js' );
|
||||||
const stripConfig = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../../../config/payments.config.secret.json' ) ) )[ 'stripe' ];
|
const stripeConfig = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../../../config/payments.config.secret.json' ) ) )[ 'stripe' ];
|
||||||
const stripe = require( 'stripe' )( stripConfig[ 'APIKey' ] );
|
const stripe = require( 'stripe' )( stripeConfig[ 'APIKey' ] );
|
||||||
const bodyParser = require( 'body-parser' );
|
const bodyParser = require( 'body-parser' );
|
||||||
const ticket = require( '../../../tickets/ticketGenerator.js' );
|
const ticket = require( '../../../tickets/ticketGenerator.js' );
|
||||||
const TicketGenerator = new ticket();
|
const TicketGenerator = new ticket();
|
||||||
|
|
||||||
const endpointSecret = stripConfig[ 'endpointSecret' ];
|
const endpointSecret = stripeConfig[ 'endpointSecret' ];
|
||||||
|
|
||||||
let sessionReference = {};
|
let sessionReference = {};
|
||||||
let waitingClients = {};
|
let waitingClients = {};
|
||||||
let pendingPayments = {};
|
let pendingPayments = {};
|
||||||
let paymentOk = {};
|
let paymentOk = {};
|
||||||
|
|
||||||
// TODO: Remove all selected tickets if timestamp more than user defined amount ago
|
|
||||||
|
|
||||||
module.exports = ( app, settings ) => {
|
module.exports = ( app, settings ) => {
|
||||||
app.post( '/payments/prepare', bodyParser.json(), ( req, res ) => {
|
app.post( '/payments/prepare', bodyParser.json(), ( req, res ) => {
|
||||||
if ( req.session.loggedInUser ) {
|
if ( req.session.loggedInUser ) {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
"stripe": {
|
"stripe": {
|
||||||
"APIKey": ""
|
"APIKey": "",
|
||||||
|
"endpointSecret": ""
|
||||||
|
},
|
||||||
|
"payrexx": {
|
||||||
|
"APISecret": "",
|
||||||
|
"instance": "",
|
||||||
|
"pm": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"setupKey": "hello world",
|
"setupKey": "hello world",
|
||||||
"twoFAMode": "enhanced",
|
"twoFAMode": "enhanced",
|
||||||
"db": "mysql",
|
"db": "mysql",
|
||||||
"payments": "stripe",
|
"payments": "payrexx",
|
||||||
"name": "libreevent",
|
"name": "libreevent",
|
||||||
"yourDomain": "http://localhost:8080",
|
"yourDomain": "http://localhost:8080",
|
||||||
"mailSender": "libreevent <info@libreevent.janishutz.com>",
|
"mailSender": "libreevent <info@libreevent.janishutz.com>",
|
||||||
|
|||||||
123
src/server/package-lock.json
generated
123
src/server/package-lock.json
generated
@@ -11,9 +11,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pdfme/generator": "^1.2.6",
|
"@pdfme/generator": "^1.2.6",
|
||||||
"@seald-io/nedb": "^4.0.2",
|
"@seald-io/nedb": "^4.0.2",
|
||||||
|
"axios": "^1.4.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
"qs": "^6.11.2",
|
||||||
"serve-favicon": "^2.5.0",
|
"serve-favicon": "^2.5.0",
|
||||||
"serve-static": "^1.15.0",
|
"serve-static": "^1.15.0",
|
||||||
"stripe": "^12.14.0",
|
"stripe": "^12.14.0",
|
||||||
@@ -432,6 +435,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"node_modules/atob": {
|
"node_modules/atob": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||||
@@ -454,6 +462,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -535,6 +553,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser/node_modules/qs": {
|
||||||
|
"version": "6.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
|
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -665,6 +697,17 @@
|
|||||||
"color-support": "bin.js"
|
"color-support": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "9.5.0",
|
"version": "9.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
@@ -774,6 +817,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
|
||||||
|
},
|
||||||
"node_modules/css-b64-images": {
|
"node_modules/css-b64-images": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
|
||||||
@@ -820,6 +868,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delegates": {
|
"node_modules/delegates": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
@@ -934,6 +990,16 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/encoding": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "~0.4.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
@@ -1087,6 +1153,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/express/node_modules/qs": {
|
||||||
|
"version": "6.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
|
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/raw-body": {
|
"node_modules/express/node_modules/raw-body": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||||
@@ -1152,6 +1232,25 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fontkit": {
|
"node_modules/fontkit": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.2.tgz",
|
||||||
@@ -1176,6 +1275,19 @@
|
|||||||
"is-callable": "^1.1.3"
|
"is-callable": "^1.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -2158,10 +2270,15 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,9 +44,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pdfme/generator": "^1.2.6",
|
"@pdfme/generator": "^1.2.6",
|
||||||
"@seald-io/nedb": "^4.0.2",
|
"@seald-io/nedb": "^4.0.2",
|
||||||
|
"axios": "^1.4.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
"qs": "^6.11.2",
|
||||||
"serve-favicon": "^2.5.0",
|
"serve-favicon": "^2.5.0",
|
||||||
"serve-static": "^1.15.0",
|
"serve-static": "^1.15.0",
|
||||||
"stripe": "^12.14.0",
|
"stripe": "^12.14.0",
|
||||||
|
|||||||
57
src/server/ui/en/payments/failed.html
Normal file
57
src/server/ui/en/payments/failed.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Payment Canceled</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgb(41, 40, 40);
|
||||||
|
color: white;
|
||||||
|
font-size: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
margin-top: 2%;
|
||||||
|
background: linear-gradient(90deg, rgb(30, 36, 131), rgb(87, 66, 184), rgb(105, 115, 214), rgb(30, 36, 131), rgb(41, 128, 109), rgb(146, 50, 47));
|
||||||
|
background-size: 300px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 3s;
|
||||||
|
font-size: 75%;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit:hover {
|
||||||
|
background-size: 200%;
|
||||||
|
background-position: -100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
<h1>Payment Failed</h1>
|
||||||
|
<p>Your payment has failed! Please try again or use a different payment method</p>
|
||||||
|
<button onclick="location.href = '/cart'" class="submit">Back to the cart</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<!-- <h2>Setup check</h2> -->
|
||||||
|
<!-- TODO: Maybe add -->
|
||||||
|
<!-- TODO: call config check of payment + check if events are deployed -->
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
<p>Changing any of these settings requires a restart of libreevent.</p>
|
<p>Changing any of these settings requires a restart of libreevent.</p>
|
||||||
<p>Currency codes used must be valid ISO 4217 codes. Read more on <a href="https://libreevent.janishutz.com/docs/admin-panel/settings#currency" target="_blank">this page</a> of the documentation <!-- https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes"--></p>
|
<p>Currency codes used must be valid ISO 4217 codes. Read more on <a href="https://libreevent.janishutz.com/docs/admin-panel/settings#currency" target="_blank">this page</a> of the documentation <!-- https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes"--></p>
|
||||||
@@ -171,9 +174,9 @@
|
|||||||
},
|
},
|
||||||
showPaymentSettings () {
|
showPaymentSettings () {
|
||||||
this.$refs.popup.openPopup( 'Payment gateway settings', {
|
this.$refs.popup.openPopup( 'Payment gateway settings', {
|
||||||
'link': '/payments/settings',
|
'link': '/payments/settings',
|
||||||
}
|
}
|
||||||
, 'iframe' );
|
, 'iframe' );
|
||||||
},
|
},
|
||||||
createAccount() {
|
createAccount() {
|
||||||
this.$refs.popup.openPopup( 'Create new admin user', {
|
this.$refs.popup.openPopup( 'Create new admin user', {
|
||||||
@@ -224,6 +227,7 @@
|
|||||||
},
|
},
|
||||||
handlePopupReturns( data ) {
|
handlePopupReturns( data ) {
|
||||||
console.log( data );
|
console.log( data );
|
||||||
|
// TODO: Delete user
|
||||||
if ( data.status === 'cancel' ) {
|
if ( data.status === 'cancel' ) {
|
||||||
console.log( 'user canceled' );
|
console.log( 'user canceled' );
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ Including many payment methods can help the user choose the one that fits them b
|
|||||||
|
|
||||||
## Advantages / Disadvantages of each payment gateway
|
## Advantages / Disadvantages of each payment gateway
|
||||||
|
|
||||||
|
### payrexx
|
||||||
|
Payrexx is a lesser known payment gateway and it is not ideal for you if you expect to have a low sales volume, as their basic plan costs you EUR 15 a month. If your sales volume is fairly high, it may be worth is as their per-transaction fees are quite a bit lower than Stripe's and they offer more payment options.
|
||||||
|
See payrexx [pricing here](https://www.payrexx.com/en/pricing/) and sign up [here](https://signup.payrexx.com). You can find a full list of supported payment methods as they should be entered in the GUI [here](https://docs.payrexx.com/developer/general-info/payment-method)
|
||||||
|
|
||||||
### Stripe
|
### Stripe
|
||||||
Stripe is one of the most well known payment gateways out there, but is also fairly expensive, but easy to set up.
|
Stripe is one of the most well known payment gateways out there and it is really easy to get started.
|
||||||
See [here](https://stripe.com/en-gb/pricing) for pricing information and sign up [here](https://dashboard.stripe.com/register)
|
See [here](https://stripe.com/en-gb/pricing) for pricing information and sign up [here](https://dashboard.stripe.com/register)
|
||||||
|
|
||||||
### adyen
|
|
||||||
Adyen is used by small and huge businesses alike, but you have to email their support team for account creation, which is easy to do, but takes longer to complete. adyen is fairly cheap, "*only*" charging 0.10$ per transaction plus payment method costs. Detailed pricing information can be found [here](https://www.adyen.com/pricing) and you may sign up [here](https://www.adyen.com/contact/sales)
|
|
||||||
|
|
||||||
|
|
||||||
### payrexx
|
|
||||||
https://www.payrexx.com/en/pricing/
|
|
||||||
Reference in New Issue
Block a user