const prisma = require('../config/db'); const { safeErrorMessage } = require('../utils/errorUtils'); const { encrypt } = require('../utils/encryption'); const { invalidate: invalidateSettingsCache, warmCache } = require('../utils/settingsCache'); const { logAdminAction } = require('../utils/adminAudit'); const { getClientIp } = require('../utils/requestUtils'); const { maskSecret } = require('../utils/yoco'); const { KEY_MERCHANT_ID, KEY_MERCHANT_KEY, KEY_PASSPHRASE, KEY_MODE, getPayfastConfig, payfastNotifyUrl, notifyUrlProblem, } = require('../utils/payfast'); const fail = (res, e) => res.status(e.statusCode || (res.statusCode === 200 ? 400 : res.statusCode)).json({ message: safeErrorMessage(e) }); // @desc PayFast settings (passphrase masked) and the ITN notify URL // @route GET /api/payfast/config // @access Admin const getConfig = async (req, res) => { try { const c = await getPayfastConfig(); const notifyUrl = payfastNotifyUrl(); res.json({ merchantId: c.merchantId, merchantKey: c.merchantKey, passphraseMasked: c.passphrase ? maskSecret(c.passphrase) : '', hasPassphrase: !!c.passphrase, mode: c.mode, notifyUrl, notifyUrlProblem: notifyUrlProblem(notifyUrl), configured: !!(c.merchantId && c.merchantKey), }); } catch (e) { fail(res, e); } }; // @desc Save PayFast settings ('' clears the passphrase; omitted fields stay as they are) // @route PUT /api/payfast/config { merchantId?, merchantKey?, passphrase?, mode? } // @access Admin const updateConfig = async (req, res) => { try { const { merchantId, merchantKey, passphrase, mode } = req.body || {}; const updates = {}; if (typeof merchantId === 'string') { const v = merchantId.trim(); if (v && !/^\d{5,12}$/.test(v)) { res.status(400); throw new Error('The merchant ID is a number, e.g. 10000100.'); } updates[KEY_MERCHANT_ID] = v; } if (typeof merchantKey === 'string') { const v = merchantKey.trim(); if (v && !/^[A-Za-z0-9]{8,32}$/.test(v)) { res.status(400); throw new Error('The merchant key is letters and numbers, as shown on your PayFast dashboard.'); } updates[KEY_MERCHANT_KEY] = v; } if (typeof passphrase === 'string') { // PayFast passphrases are 32 characters at most; spaces at the ends are trimmed by PayFast. if (passphrase.trim().length > 32) { res.status(400); throw new Error('The passphrase can be at most 32 characters.'); } updates[KEY_PASSPHRASE] = passphrase.trim(); } if (mode !== undefined) { if (!['sandbox', 'live'].includes(mode)) { res.status(400); throw new Error('Mode is sandbox or live.'); } updates[KEY_MODE] = mode; } if (!Object.keys(updates).length) { res.status(400); throw new Error('Nothing to save'); } await prisma.$transaction(Object.entries(updates).map(([key, value]) => { const stored = value && key === KEY_PASSPHRASE ? encrypt(value) : value; return prisma.appSetting.upsert({ where: { key }, update: { value: stored }, create: { key, value: stored } }); })); invalidateSettingsCache(); await warmCache(); logAdminAction({ actorId: req.user.id, actorRole: req.user.role, action: 'payfast_config_updated', targetType: 'AppSetting', metadata: { changedKeys: Object.keys(updates), ...(updates[KEY_MODE] ? { mode: updates[KEY_MODE] } : {}) }, ip: getClientIp(req), }); res.json({ message: 'PayFast settings saved' }); } catch (e) { fail(res, e); } }; // @desc Check the saved merchant ID and passphrase with PayFast (a signed API ping) // @route POST /api/payfast/test // @access Admin const testConnection = async (req, res) => { try { const c = await getPayfastConfig(); if (!c.merchantId) { res.status(400); throw new Error('Save the merchant ID first.'); } await require('../payments/providers/payfast').testConnection(); res.json({ message: `Connected to PayFast (${c.mode === 'live' ? 'live' : 'sandbox'}).` }); } catch (e) { fail(res, e); } }; module.exports = { getConfig, updateConfig, testConnection };