Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SimpleFin #296

Merged
merged 9 commits into from
Jan 20, 2024
227 changes: 227 additions & 0 deletions src/app-simplefin/app-simplefin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import express from 'express';
import { inspect } from 'util';
import https from 'https';
import { SecretName, secretsService } from '../services/secrets-service.js';

const app = express();
export { app as handlers };
app.use(express.json());

app.post('/status', async (req, res) => {
let configured = false;

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') {
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);

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

try {
let results = await getTransactions(accessKey, new Date(startDate));

let account = results.accounts.find((a) => a.id === accountId);

let response = {};

let balance = parseInt(account.balance.replace('.', ''));
let date = new Date(account['balance-date'] * 1000)
.toISOString()
.split('T')[0];

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 = [];

for (let trans of account.transactions) {
let newTrans = {};

//newTrans.bankTransactionCode = don't have compared to GoCardless
newTrans.booked = true;
newTrans.bookingDate = new Date(trans.posted * 1000)
.toISOString()
.split('T')[0];
newTrans.date = new Date(trans.posted * 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(trans.posted * 1000)
.toISOString()
.split('T')[0];

allTransactions.push(newTrans);
}

response.transactions = {
all: allTransactions,
booked: allTransactions,
pending: [],
};

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 parseAccessKey(accessKey) {
let scheme = null;
let rest = null;
let auth = null;
let username = null;
let password = null;
let baseUrl = null;
[scheme, rest] = accessKey.split('//');
[auth, rest] = rest.split('@');
[username, password] = auth.split(':');
baseUrl = `${scheme}//${rest}`;
return {
baseUrl: baseUrl,
username: username,
password: password,
};
}

async function getAccessKey(base64Token) {
const token = Buffer.from(base64Token, 'base64').toString();
const options = {
method: 'POST',
port: 443,
headers: { 'Content-Length': 0 },
};
return new Promise((resolve, reject) => {
const req = https.request(new URL(token), options, (res) => {
res.on('data', (d) => {
resolve(d.toString());
});
});
req.on('error', (e) => {
reject(e);
});
req.end();
});
}

async function getTransactions(accessKey, startDate, endDate) {
const now = new Date();
startDate = startDate || new Date(now.getFullYear(), now.getMonth(), 1);
endDate = endDate || new Date(now.getFullYear(), now.getMonth() + 1, 1);
console.log(
`${startDate.toISOString().split('T')[0]} - ${
endDate.toISOString().split('T')[0]
}`,
);
return await getAccounts(accessKey, startDate, endDate);
}

function normalizeDate(date) {
return (date.valueOf() - date.getTimezoneOffset() * 60 * 1000) / 1000;
}

async function getAccounts(accessKey, startDate, endDate) {
const sfin = parseAccessKey(accessKey);
const options = {
headers: {
Authorization: `Basic ${Buffer.from(
`${sfin.username}:${sfin.password}`,
).toString('base64')}`,
},
};
const params = [];
let queryString = '';
if (startDate) {
params.push(`start-date=${normalizeDate(startDate)}`);
}
if (endDate) {
params.push(`end-date=${normalizeDate(endDate)}`);
}
if (params.length > 0) {
queryString += '?' + params.join('&');
}
return new Promise((resolve, reject) => {
const req = https.request(
new URL(`${sfin.baseUrl}/accounts${queryString}`),
options,
(res) => {
let data = '';
res.on('data', (d) => {
data += d;
});
res.on('end', () => {
resolve(JSON.parse(data));
});
},
);
req.on('error', (e) => {
reject(e);
});
req.end();
});
}
2 changes: 2 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import rateLimit from 'express-rate-limit';
import * as accountApp from './app-account.js';
import * as syncApp from './app-sync.js';
import * as goCardlessApp from './app-gocardless/app-gocardless.js';
import * as simpleFinApp from './app-simplefin/app-simplefin.js';
import * as secretApp from './app-secrets.js';

const app = express();
Expand Down Expand Up @@ -44,6 +45,7 @@ app.use(
app.use('/sync', syncApp.handlers);
app.use('/account', accountApp.handlers);
app.use('/gocardless', goCardlessApp.handlers);
app.use('/simplefin', simpleFinApp.handlers);
app.use('/secret', secretApp.handlers);

app.get('/mode', (req, res) => {
Expand Down
2 changes: 2 additions & 0 deletions src/services/secrets-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import getAccountDb from '../account-db.js';
export const SecretName = {
gocardless_secretId: 'gocardless_secretId',
gocardless_secretKey: 'gocardless_secretKey',
simplefin_token: 'simplefin_token',
simplefin_accessKey: 'simplefin_accessKey',
};

class SecretsDb {
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/296.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [zachwhelchel,duplaja,lancepick,latetedemelon]
---

Add option to link an account to SimpleFIN for syncing transactions.
Loading