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

Commit e7f6348

Browse files
authored
SimpleFin (#296)
* Initial support for SimpleFin. * Added release notes and lint cleanup. * Changed some notes for better context. * Fixed logic. * Changes per requests on PR. * More cleanup of null checks.
1 parent af6de6b commit e7f6348

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed

src/app-simplefin/app-simplefin.js

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import express from 'express';
2+
import { inspect } from 'util';
3+
import https from 'https';
4+
import { SecretName, secretsService } from '../services/secrets-service.js';
5+
6+
const app = express();
7+
export { app as handlers };
8+
app.use(express.json());
9+
10+
app.post('/status', async (req, res) => {
11+
let configured = false;
12+
13+
let token = secretsService.get(SecretName.simplefin_token);
14+
if (token != null && token !== 'Forbidden') {
15+
configured = true;
16+
}
17+
18+
res.send({
19+
status: 'ok',
20+
data: {
21+
configured: configured,
22+
},
23+
});
24+
});
25+
26+
app.post('/accounts', async (req, res) => {
27+
let accessKey = secretsService.get(SecretName.simplefin_accessKey);
28+
29+
if (accessKey == null || accessKey === 'Forbidden') {
30+
let token = secretsService.get(SecretName.simplefin_token);
31+
if (token == null || token === 'Forbidden') {
32+
return;
33+
} else {
34+
accessKey = await getAccessKey(token);
35+
secretsService.set(SecretName.simplefin_accessKey, accessKey);
36+
}
37+
}
38+
39+
const now = new Date();
40+
let startDate = new Date(now.getFullYear(), now.getMonth(), 1);
41+
let endDate = new Date(now.getFullYear(), now.getMonth() + 1, 1);
42+
43+
let accounts = await getAccounts(accessKey, startDate, endDate);
44+
45+
res.send({
46+
status: 'ok',
47+
data: {
48+
accounts: accounts.accounts,
49+
},
50+
});
51+
});
52+
53+
app.post('/transactions', async (req, res) => {
54+
const { accountId, startDate } = req.body;
55+
56+
let accessKey = secretsService.get(SecretName.simplefin_accessKey);
57+
58+
if (accessKey == null || accessKey === 'Forbidden') {
59+
return;
60+
}
61+
62+
try {
63+
let results = await getTransactions(accessKey, new Date(startDate));
64+
65+
let account = results.accounts.find((a) => a.id === accountId);
66+
67+
let response = {};
68+
69+
let balance = parseInt(account.balance.replace('.', ''));
70+
let date = new Date(account['balance-date'] * 1000)
71+
.toISOString()
72+
.split('T')[0];
73+
74+
response.balances = [
75+
{
76+
balanceAmount: { amount: account.balance, currency: account.currency },
77+
balanceType: 'expected',
78+
referenceDate: date,
79+
},
80+
{
81+
balanceAmount: { amount: account.balance, currency: account.currency },
82+
balanceType: 'interimAvailable',
83+
referenceDate: date,
84+
},
85+
];
86+
//response.iban = don't have compared to GoCardless
87+
//response.institutionId = don't have compared to GoCardless
88+
response.startingBalance = balance; // could be named differently in this use case.
89+
90+
let allTransactions = [];
91+
92+
for (let trans of account.transactions) {
93+
let newTrans = {};
94+
95+
//newTrans.bankTransactionCode = don't have compared to GoCardless
96+
newTrans.booked = true;
97+
newTrans.bookingDate = new Date(trans.posted * 1000)
98+
.toISOString()
99+
.split('T')[0];
100+
newTrans.date = new Date(trans.posted * 1000).toISOString().split('T')[0];
101+
newTrans.debtorName = trans.payee;
102+
//newTrans.debtorAccount = don't have compared to GoCardless
103+
newTrans.remittanceInformationUnstructured = trans.description;
104+
newTrans.transactionAmount = { amount: trans.amount, currency: 'USD' };
105+
newTrans.transactionId = trans.id;
106+
newTrans.valueDate = new Date(trans.posted * 1000)
107+
.toISOString()
108+
.split('T')[0];
109+
110+
allTransactions.push(newTrans);
111+
}
112+
113+
response.transactions = {
114+
all: allTransactions,
115+
booked: allTransactions,
116+
pending: [],
117+
};
118+
119+
res.send({
120+
status: 'ok',
121+
data: response,
122+
});
123+
} catch (error) {
124+
const sendErrorResponse = (data) =>
125+
res.send({ status: 'ok', data: { ...data, details: error.details } });
126+
console.log(
127+
'Something went wrong',
128+
inspect(error, { depth: null }),
129+
sendErrorResponse,
130+
);
131+
}
132+
});
133+
134+
function parseAccessKey(accessKey) {
135+
let scheme = null;
136+
let rest = null;
137+
let auth = null;
138+
let username = null;
139+
let password = null;
140+
let baseUrl = null;
141+
[scheme, rest] = accessKey.split('//');
142+
[auth, rest] = rest.split('@');
143+
[username, password] = auth.split(':');
144+
baseUrl = `${scheme}//${rest}`;
145+
return {
146+
baseUrl: baseUrl,
147+
username: username,
148+
password: password,
149+
};
150+
}
151+
152+
async function getAccessKey(base64Token) {
153+
const token = Buffer.from(base64Token, 'base64').toString();
154+
const options = {
155+
method: 'POST',
156+
port: 443,
157+
headers: { 'Content-Length': 0 },
158+
};
159+
return new Promise((resolve, reject) => {
160+
const req = https.request(new URL(token), options, (res) => {
161+
res.on('data', (d) => {
162+
resolve(d.toString());
163+
});
164+
});
165+
req.on('error', (e) => {
166+
reject(e);
167+
});
168+
req.end();
169+
});
170+
}
171+
172+
async function getTransactions(accessKey, startDate, endDate) {
173+
const now = new Date();
174+
startDate = startDate || new Date(now.getFullYear(), now.getMonth(), 1);
175+
endDate = endDate || new Date(now.getFullYear(), now.getMonth() + 1, 1);
176+
console.log(
177+
`${startDate.toISOString().split('T')[0]} - ${
178+
endDate.toISOString().split('T')[0]
179+
}`,
180+
);
181+
return await getAccounts(accessKey, startDate, endDate);
182+
}
183+
184+
function normalizeDate(date) {
185+
return (date.valueOf() - date.getTimezoneOffset() * 60 * 1000) / 1000;
186+
}
187+
188+
async function getAccounts(accessKey, startDate, endDate) {
189+
const sfin = parseAccessKey(accessKey);
190+
const options = {
191+
headers: {
192+
Authorization: `Basic ${Buffer.from(
193+
`${sfin.username}:${sfin.password}`,
194+
).toString('base64')}`,
195+
},
196+
};
197+
const params = [];
198+
let queryString = '';
199+
if (startDate) {
200+
params.push(`start-date=${normalizeDate(startDate)}`);
201+
}
202+
if (endDate) {
203+
params.push(`end-date=${normalizeDate(endDate)}`);
204+
}
205+
if (params.length > 0) {
206+
queryString += '?' + params.join('&');
207+
}
208+
return new Promise((resolve, reject) => {
209+
const req = https.request(
210+
new URL(`${sfin.baseUrl}/accounts${queryString}`),
211+
options,
212+
(res) => {
213+
let data = '';
214+
res.on('data', (d) => {
215+
data += d;
216+
});
217+
res.on('end', () => {
218+
resolve(JSON.parse(data));
219+
});
220+
},
221+
);
222+
req.on('error', (e) => {
223+
reject(e);
224+
});
225+
req.end();
226+
});
227+
}

src/app.js

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import rateLimit from 'express-rate-limit';
99
import * as accountApp from './app-account.js';
1010
import * as syncApp from './app-sync.js';
1111
import * as goCardlessApp from './app-gocardless/app-gocardless.js';
12+
import * as simpleFinApp from './app-simplefin/app-simplefin.js';
1213
import * as secretApp from './app-secrets.js';
1314

1415
const app = express();
@@ -44,6 +45,7 @@ app.use(
4445
app.use('/sync', syncApp.handlers);
4546
app.use('/account', accountApp.handlers);
4647
app.use('/gocardless', goCardlessApp.handlers);
48+
app.use('/simplefin', simpleFinApp.handlers);
4749
app.use('/secret', secretApp.handlers);
4850

4951
app.get('/mode', (req, res) => {

src/services/secrets-service.js

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import getAccountDb from '../account-db.js';
99
export const SecretName = {
1010
gocardless_secretId: 'gocardless_secretId',
1111
gocardless_secretKey: 'gocardless_secretKey',
12+
simplefin_token: 'simplefin_token',
13+
simplefin_accessKey: 'simplefin_accessKey',
1214
};
1315

1416
class SecretsDb {

upcoming-release-notes/296.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: Enhancements
3+
authors: [zachwhelchel,duplaja,lancepick,latetedemelon]
4+
---
5+
6+
Add option to link an account to SimpleFIN for syncing transactions.

0 commit comments

Comments
 (0)