diff --git a/src/app-simplefin/app-simplefin.js b/src/app-simplefin/app-simplefin.js index 705df5c72..5c0249b81 100644 --- a/src/app-simplefin/app-simplefin.js +++ b/src/app-simplefin/app-simplefin.js @@ -2,150 +2,198 @@ import express from 'express'; import { inspect } from 'util'; import https from 'https'; import { SecretName, secretsService } from '../services/secrets-service.js'; +import { handleError } from '../app-gocardless/util/handle-error.js'; const app = express(); export { app as handlers }; app.use(express.json()); -app.post('/status', async (req, res) => { - let configured = false; +app.post( + '/status', + handleError(async (req, res) => { + const token = secretsService.get(SecretName.simplefin_token); + const configured = token != null && token !== 'Forbidden'; - let token = secretsService.get(SecretName.simplefin_token); - if (token != null && token !== 'Forbidden') { - configured = true; - } - - res.send({ - status: 'ok', - data: { - configured: configured, - }, - }); -}); - -app.post('/accounts', async (req, res) => { - let accessKey = secretsService.get(SecretName.simplefin_accessKey); - - if (accessKey == null || accessKey === 'Forbidden') { - let token = secretsService.get(SecretName.simplefin_token); - if (token == null || token === 'Forbidden') { + res.send({ + status: 'ok', + data: { + configured: configured, + }, + }); + }), +); + +app.post( + '/accounts', + handleError(async (req, res) => { + let accessKey = secretsService.get(SecretName.simplefin_accessKey); + + try { + if (accessKey == null || accessKey === 'Forbidden') { + const token = secretsService.get(SecretName.simplefin_token); + if (token == null || token === 'Forbidden') { + throw new Error('No token'); + } else { + accessKey = await getAccessKey(token); + secretsService.set(SecretName.simplefin_accessKey, accessKey); + if (accessKey == null || accessKey === 'Forbidden') { + throw new Error('No access key'); + } + } + } + } catch (error) { + invalidToken(res); return; - } else { - accessKey = await getAccessKey(token); - secretsService.set(SecretName.simplefin_accessKey, accessKey); } - } - - const now = new Date(); - let startDate = new Date(now.getFullYear(), now.getMonth(), 1); - let endDate = new Date(now.getFullYear(), now.getMonth() + 1, 1); - let accounts = await getAccounts(accessKey, startDate, endDate); + const now = new Date(); + const startDate = new Date(now.getFullYear(), now.getMonth(), 1); + const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 1); - res.send({ - status: 'ok', - data: { - accounts: accounts.accounts, - }, - }); -}); - -app.post('/transactions', async (req, res) => { - const { accountId, startDate } = req.body; - - let accessKey = secretsService.get(SecretName.simplefin_accessKey); - - if (accessKey == null || accessKey === 'Forbidden') { - return; - } + const accounts = await getAccounts(accessKey, startDate, endDate); - try { - let results = await getTransactions(accessKey, new Date(startDate)); + res.send({ + status: 'ok', + data: { + accounts: accounts.accounts, + }, + }); + }), +); - let account = results.accounts.find((a) => a.id === accountId); +app.post( + '/transactions', + handleError(async (req, res) => { + const { accountId, startDate } = req.body; - let response = {}; + const accessKey = secretsService.get(SecretName.simplefin_accessKey); - let balance = parseInt(account.balance.replace('.', '')); - let date = new Date(account['balance-date'] * 1000) - .toISOString() - .split('T')[0]; + if (accessKey == null || accessKey === 'Forbidden') { + invalidToken(res); + return; + } - response.balances = [ - { - balanceAmount: { amount: account.balance, currency: account.currency }, - balanceType: 'expected', - referenceDate: date, - }, - { - balanceAmount: { amount: account.balance, currency: account.currency }, - balanceType: 'interimAvailable', - referenceDate: date, - }, - ]; - //response.iban = don't have compared to GoCardless - //response.institutionId = don't have compared to GoCardless - response.startingBalance = balance; // could be named differently in this use case. - - let allTransactions = []; - let bookedTransactions = []; - let pendingTransactions = []; - - for (let trans of account.transactions) { - let newTrans = {}; - - let dateToUse = 0; - - if (trans.posted == 0) { - newTrans.booked = false; - dateToUse = trans.transacted_at; - } else { - newTrans.booked = true; - dateToUse = trans.posted; + try { + const results = await getTransactions(accessKey, new Date(startDate)); + + const account = results.accounts.find((a) => a.id === accountId); + + const needsAttention = results.errors.find( + (e) => e === `Connection to ${account.org.name} may need attention`, + ); + if (needsAttention) { + res.send({ + status: 'ok', + data: { + error_type: 'ACCOUNT_NEEDS_ATTENTION', + error_code: 'ACCOUNT_NEEDS_ATTENTION', + status: 'rejected', + reason: + 'The account needs your attention at SimpleFIN.', + }, + }); } - newTrans.bookingDate = new Date(dateToUse * 1000) - .toISOString() - .split('T')[0]; + const response = {}; - newTrans.date = new Date(dateToUse * 1000).toISOString().split('T')[0]; - newTrans.debtorName = trans.payee; - //newTrans.debtorAccount = don't have compared to GoCardless - newTrans.remittanceInformationUnstructured = trans.description; - newTrans.transactionAmount = { amount: trans.amount, currency: 'USD' }; - newTrans.transactionId = trans.id; - newTrans.valueDate = new Date(dateToUse * 1000) + const balance = parseInt(account.balance.replace('.', '')); + const date = new Date(account['balance-date'] * 1000) .toISOString() .split('T')[0]; - if (newTrans.booked) { - bookedTransactions.push(newTrans); - } else { - pendingTransactions.push(newTrans); + response.balances = [ + { + balanceAmount: { + amount: account.balance, + currency: account.currency, + }, + balanceType: 'expected', + referenceDate: date, + }, + { + balanceAmount: { + amount: account.balance, + currency: account.currency, + }, + balanceType: 'interimAvailable', + referenceDate: date, + }, + ]; + response.startingBalance = balance; // could be named differently in this use case. + + const allTransactions = []; + const bookedTransactions = []; + const pendingTransactions = []; + + for (const trans of account.transactions) { + const newTrans = {}; + + let dateToUse = 0; + + if (trans.posted == 0) { + newTrans.booked = false; + dateToUse = trans.transacted_at; + } else { + newTrans.booked = true; + dateToUse = trans.posted; + } + + newTrans.bookingDate = new Date(dateToUse * 1000) + .toISOString() + .split('T')[0]; + + newTrans.date = new Date(dateToUse * 1000).toISOString().split('T')[0]; + newTrans.debtorName = trans.payee; + //newTrans.debtorAccount = don't have compared to GoCardless + newTrans.remittanceInformationUnstructured = trans.description; + newTrans.transactionAmount = { amount: trans.amount, currency: 'USD' }; + newTrans.transactionId = trans.id; + newTrans.valueDate = new Date(dateToUse * 1000) + .toISOString() + .split('T')[0]; + + if (newTrans.booked) { + bookedTransactions.push(newTrans); + } else { + pendingTransactions.push(newTrans); + } + allTransactions.push(newTrans); } - allTransactions.push(newTrans); - } - response.transactions = { - all: allTransactions, - booked: bookedTransactions, - pending: pendingTransactions, - }; + response.transactions = { + all: allTransactions, + booked: bookedTransactions, + pending: pendingTransactions, + }; - res.send({ - status: 'ok', - data: response, - }); - } catch (error) { - const sendErrorResponse = (data) => - res.send({ status: 'ok', data: { ...data, details: error.details } }); - console.log( - 'Something went wrong', - inspect(error, { depth: null }), - sendErrorResponse, - ); - } -}); + res.send({ + status: 'ok', + data: response, + }); + } catch (error) { + const sendErrorResponse = (data) => + res.send({ status: 'ok', data: { ...data, details: error.details } }); + console.log( + 'Something went wrong', + inspect(error, { depth: null }), + sendErrorResponse, + ); + } + }), +); + +function invalidToken(res) { + res.send({ + status: 'ok', + data: { + error_type: 'INVALID_ACCESS_TOKEN', + error_code: 'INVALID_ACCESS_TOKEN', + status: 'rejected', + reason: + 'Invalid SimpleFIN access token. Reset the token and re-link any broken accounts.', + }, + }); +} function parseAccessKey(accessKey) { let scheme = null; diff --git a/upcoming-release-notes/375.md b/upcoming-release-notes/375.md new file mode 100644 index 000000000..b2efb9318 --- /dev/null +++ b/upcoming-release-notes/375.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [psybers] +--- + +Properly handle errors for SimpleFIN.