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 paths:


POST Path Body Description
/payment-methods { businessCode, userId } Use to list a user's active payment methods
/delete-account { businessCode, userId } Used to delete an account when a Storeganise user is deleted (optional)
/charge { businessCode, userId, amount } Used to charge or refund (if amount is negative) a user, amount is in cents
/ { businessCode, userId } Used to display the checkout view where user enters his payment details

Here's the full request handler:

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);
  }
}

The checkout view:

<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>
Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? Contact Us Contact Us