Files
hope-events/backend/src/utils/payfast.js
T
joshuaandClaude Opus 5.5 f2b51dbd88 PayFast provider, Pay online page, amount checks on payments
- PayFast (custom integration): signed form (PHP-style encoding, checked
  against a real sandbox), ITN checks (signature over all posted fields,
  PayFast host, VALID from /eng/query/validate, amount vs checkout).
- ProviderCheckout records every checkout (amount, metadata, request);
  webhooks match payments to it and hold a different amount for review
  (ProviderTransaction.note).
- /pay/<id>: payers choose a provider when several are on, and the
  browser posts PayFast's form from there, so payment links work for
  every provider.
- Site settings → Payments: PayFast card (merchant ID/key, encrypted
  passphrase, sandbox/live) while PayFast is enabled.
- Payer-facing wording no longer names Yoco; legal pages name both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:25:49 +02:00

58 lines
2.5 KiB
JavaScript

/**
* PayFast configuration (Admin → Site Settings → Payments → PayFast).
*
* Stored in AppSetting: payfast_merchant_id, payfast_merchant_key, payfast_passphrase
* (AES-256-GCM encrypted — it signs every payment) and payfast_mode ('sandbox' | 'live',
* default sandbox). PAYFAST_MERCHANT_ID / PAYFAST_MERCHANT_KEY / PAYFAST_PASSPHRASE /
* PAYFAST_MODE in .env are fallbacks when the database has no value. The merchant key isn't
* secret: it travels in the payment form the payer's browser posts to PayFast.
*/
const { getSetting } = require('./settingsCache');
const KEY_MERCHANT_ID = 'payfast_merchant_id';
const KEY_MERCHANT_KEY = 'payfast_merchant_key';
const KEY_PASSPHRASE = 'payfast_passphrase';
const KEY_MODE = 'payfast_mode';
const HOSTS = { live: 'www.payfast.co.za', sandbox: 'sandbox.payfast.co.za' };
async function getPayfastConfig() {
const [merchantId, merchantKey, passphrase, mode] = await Promise.all([
getSetting(KEY_MERCHANT_ID, process.env.PAYFAST_MERCHANT_ID),
getSetting(KEY_MERCHANT_KEY, process.env.PAYFAST_MERCHANT_KEY),
getSetting(KEY_PASSPHRASE, process.env.PAYFAST_PASSPHRASE),
getSetting(KEY_MODE, process.env.PAYFAST_MODE),
]);
return {
merchantId: String(merchantId || '').trim(),
merchantKey: String(merchantKey || '').trim(),
passphrase: String(passphrase || ''),
mode: mode === 'live' ? 'live' : 'sandbox',
};
}
/** PayFast's host for a mode: where payers are sent and where ITNs are confirmed. */
const payfastHost = (mode) => HOSTS[mode === 'live' ? 'live' : 'sandbox'];
/** Where PayFast should post ITNs: BACKEND_URL (or the request's origin) + /api/webhooks/payfast. */
function payfastNotifyUrl(req) {
const base = (process.env.BACKEND_URL || (req ? `${req.protocol}://${req.get('host')}` : '')).replace(/\/$/, '');
return base ? `${base}/api/webhooks/payfast` : '';
}
/** Why PayFast can't reach the notify URL (null when it can). */
function notifyUrlProblem(url) {
let u;
try { u = new URL(url); } catch { return 'Set BACKEND_URL on the server so PayFast knows where to send payment notifications.'; }
if (['localhost', '127.0.0.1', '::1', '[::1]'].includes(u.hostname)) {
return 'PayFast can\'t reach localhost, so payments won\'t be confirmed automatically. Use a public BACKEND_URL (or a tunnel) to test notifications.';
}
return null;
}
module.exports = {
KEY_MERCHANT_ID, KEY_MERCHANT_KEY, KEY_PASSPHRASE, KEY_MODE,
getPayfastConfig, payfastHost, payfastNotifyUrl, notifyUrlProblem,
};