Custom payment integration (BPOINT example)
Storeganise allows not to integrate with any payment platform.The only requirement is to develop a middleware responding to the following webhook events sent by your Storeganise API:
Here's the full request handler:
The checkout view:
| Event (webhook.type) | Body (webhook.data) | Description |
|---|---|---|
billing.list | { userId, siteId } | Use to list a user's active payment methods |
billing.charge | { userId, siteId, invoiceId, amount } | Used to charge or refund (if amount is negative) a user, amount is in cents |
billing.checkout | { userId, siteId, returnUrl } | Used to display the checkout view where user enters his payment details |
import crypto from 'crypto';
export function _createApi(businessCode) {
if (!businessCode) throw Object.assign(new Error('missing businessCode'), { status: 400 });
const apiKey = process.env[`API_KEY_${businessCode}`];
if (!apiKey) throw Object.assign(new Error(`missing API_KEY for ${businessCode}`), { status: 400 });
const sgApiUrl = `https://${businessCode}.storeganise.com/api`;
return function fetchSg(path, { method, body, params } = {}) {
return fetch(`${sgApiUrl}/v1/admin/${path}` + (params ? '?' + Object.entries(params || {}).map(([k, v]) => [k, v && encodeURIComponent(v)].filter(x => x != null).join('=')).join('&') : ''), {
method,
headers: {
Authorization: `ApiKey ${apiKey}`,
...body && { 'Content-Type': 'application/json' },
},
body: body && JSON.stringify(body),
})
.then(async r => {
const data = await r.json().catch(() => ({}));
if (r.ok) return data;
throw Object.assign(new Error('sg'), { status: r.status, ...data.error });
});
};
}
export function _createBpoint(business) {
if (!business.customFields.bpoint_username) throw Object.assign(new Error(`missing business custom fields for bpoint`), { status: 400 });
return function fetchBpoint(path, { method = 'GET', body } = {}) {
return fetch('https://www.bpoint.com.au/rest/v5/' + path, {
method,
headers: {
Authorization: `Basic ${Buffer.from(`${business.customFields.bpoint_username}|${business.customFields.bpoint_merchantId}:${business.customFields.bpoint_password}`).toString('base64')}`,
'Content-Type': 'application/json'
},
body: body && JSON.stringify(body)
})
.then(async r => {
if (r.ok) return r.json().catch(() => null);
const text = await r.text();
throw new Error(`Bpoint error: ${text}`);
});
}
}
export default async (req, res) => {
try {
const { type, returnUrl } = req.query || {};
if (req.method !== 'POST') {
return res.status(404).send();
}
const { businessCode, userId, amount } = req.body;
const fetchSg = _createApi(businessCode);
const business = await fetchSg('settings?include=customFields');
const fetchBpoint = _createBpoint(business);
const currency = business.customFields.bpoint_currency || 'AUD';
switch (req.path) {
case '/payment-methods': {
const user = await fetchSg(`users/${userId}?include=billing`);
if (!user.billing.id) return res.json([]);
try {
const data = await fetchBpoint(`tokens/${user.billing.id}`);
return res.json([data.paymentMethod]);
} catch (err) {
return res.status(400).json({ message: err.message });
}
}
case '/delete-account': {
const user = await fetchSg(`users/${userId}?include=billing`);
if (user.billing.id) {
await fetchBpoint(`tokens/${user.billing.id}`, { method: 'DELETE' });
}
return res.status(user.billing.id ? 200 : 204).send();
}
case '/charge': {
const user = await fetchSg(`users/${userId}?include=billing`);
if (!user.billing.id) {
return res.status(204).send();
}
if (amount < 0) {
// ... refund logic
}
const { authkey } = await fetchBpoint('txns/authkeys', { method: 'POST' });
await fetchBpoint(`txns/authkeys/${authkey}/payment-method`, {
method: 'PUT',
body: { token: user.billing.id }
});
await fetchBpoint(`txns/authkeys/${authkey}/txn-details`, {
method: 'PUT',
body: {
action: 'Payment',
type: 'Internet',
subType: 'Single',
amount, // in cents
crn1: user.email,
crn2: user.id,
crn3: req.body.invoiceId,
currency,
emailAddress: user.email,
testMode: business.customFields.bpoint_testMode ?? false // false in prod
}
});
const r = await fetchBpoint(`txns/authkeys/${authkey}/process`, {
method: 'POST',
body: {
webhook: {
url: `https://storeganise.npkn.net/bpoint-test/webhook?businessCode=${businessCode}`,
version: '5',
}
}
});
return res.send({
id: r.txn.txnNumber,
amount: r.txn.amount,
currency: r.txn.currency,
status: r.txn.responseCode === '0' ? 'succeeded' : r.txn.bankResponseCode === '09' ? 'processing' : 'failed',
bankResponseCode: r.txn.bankResponseCode,
responseText: r.txn.responseText,
paymentMethod: r.txn.paymentMethod,
receipt: r.txn.receiptNumber,
isTest: r.txn.isTestTxn,
});
}
case '/setup': {
const { authkey } = req.body;
const data = await fetchBpoint(`tokens/authkeys/${authkey}/process`, {
method: 'POST',
body: {
webhook: {
url: `https://storeganise.npkn.net/bpoint-test/webhook?businessCode=${businessCode}`,
version: '5',
}
}
});
if (data.token.crn2 !== userId) throw new Error('invalid crn2');
const user = await fetchSg(`users/${userId}?include=billing`);
if (user.billing.id) {
await fetchBpoint(`tokens/${user.billing.id}`, { method: 'DELETE' });
}
const r = await fetchSg(`users/${userId}`, { method: 'PUT', body: { billing: { id: data.token.token, type: 'custom' } } });
res.set({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS, POST, GET',
'Access-Control-Max-Age': 2592000,
});
return res.json({ ok: true });
}
case '/': {
const { authkey } = await fetchBpoint('tokens/authkeys', { method: 'POST' });
const user = await fetchSg(`users/${userId}?include=billing`);
const r = await fetchBpoint(`tokens/authkeys/${authkey}/token-details`, {
method: 'PUT',
body: {
crn1: user.email,
crn2: user.id,
EmailAddress: user.email,
}
});
return res.render('checkout', { business, authkey, type, userId, businessCode, returnUrl });
}
default:
return res.status(404).send(`Unknown path ${req.method} ${path}`);
}
} catch (err) {
console.log(err);
res.status(400).send(err.message);
}
}
<html>
<head>
<title>{business.companyName} - Payment</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="https://www.bpoint.com.au/rest/clientscripts/api.css" rel="stylesheet" />
</head>
<body>
<h1 style="font-family:sans-serif; text-align:center; margin-bottom:2em">Pay with <a href="https://www.bpoint.com.au/" target="_blank">BPOINT</a></h1>
<div id="paymentMethodForm" data-authkey="{authkey}"></div>
<script src="https://www.bpoint.com.au/rest/clientscripts/api.js"></script>
<script>
BPOINT.token.authkey.setupPaymentMethodForm(document.getElementById('paymentMethodForm').dataset.authkey, {
appendToElementId: "paymentMethodForm",
bank: {
enabled: {type === 'bank'},
bsb: {
label: 'BSB'
},
account: {
label: 'Account number'
},
name: {
label: 'Account name'
}
},
card: {
enabled: {type !== 'bank'},
number: {
label: "Card number"
},
expiry: {
label: "Expiry"
},
cvn: {
label: "Cvn"
},
name: {
label: "Name",
hide: false
}
},
clientsideValidation: true,
callback: function (code, data) {
if (code === "success") {
return fetch('https://storeganise.npkn.net/bpoint-test/setup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ authkey: '{authkey}', businessCode: '{businessCode}', userId: '{userId}' }),
})
.then(async r => {
submitBtn.disabled = false;
if (!r.ok) return alert(await r.text());
location.href = '{returnUrl}';
});
}
}
});
</script>
<style>
.bpoint-btn:disabled {
filter: opacity(.5);
}
</style>
</body>
</html>