Skip to content
This repository has been archived by the owner on Feb 10, 2025. It is now read-only.

Commit

Permalink
Initial support for SimpleFin.
Browse files Browse the repository at this point in the history
  • Loading branch information
zachwhelchel committed Jan 7, 2024
1 parent 933fc27 commit c608daa
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 0 deletions.
202 changes: 202 additions & 0 deletions src/app-simplefin/app-simplefin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import express from 'express';
import path from 'path';
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 === undefined || token === "Forbidden") {

} else {
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 === undefined || accessKey === "Forbidden") {
let token = secretsService.get(SecretName.simplefin_token);
if (token === null || token === undefined || 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,
},
});
});

app.post('/transactions', async (req, res) => {

const { accountId, startDate } = req.body;

let accessKey = secretsService.get(SecretName.simplefin_accessKey);

if (accessKey === null || accessKey === undefined || 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 = "thisismadeup"
response.institutionId = "thisismadeup"
response.startingBalance = balance // should be named differently in this use case.

let allTransactions = [];

for (let i = 0; i < account.transactions.length; i++) {
let trans = account.transactions[i];
let newTrans = {};

//newTrans.bankTransactionCode = don't have, not sure if issue.
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, not sure if issue.
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 }));
}
});

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 @@ -45,6 +46,7 @@ app.use('/sync', syncApp.handlers);
app.use('/account', accountApp.handlers);
app.use('/nordigen', goCardlessApp.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 = {
nordigen_secretId: 'nordigen_secretId',
nordigen_secretKey: 'nordigen_secretKey',
simplefin_token: 'simplefin_token',
simplefin_accessKey: 'simplefin_accessKey',
};

class SecretsDb {
Expand Down

0 comments on commit c608daa

Please sign in to comment.