Skip to content

Instantly share code, notes, and snippets.

@dtorres
Created April 10, 2024 16:56
Show Gist options
  • Save dtorres/c92a0e59e613ad7fc142f6f9c15137a1 to your computer and use it in GitHub Desktop.
Save dtorres/c92a0e59e613ad7fc142f6f9c15137a1 to your computer and use it in GitHub Desktop.
JS Script to import transactions via GoCardless API
import { createHash } from 'crypto';
import fs from 'fs/promises';
import axios from 'axios';
import { exit } from 'process';
// Get these from http://bankaccountdata.gocardless.com
let gclSecretId = "GoCardlessSecretID"
let gclSecretKey = "GoCardlessAccountKey"
// You can get this by creating a connection to your accounte
// See their docs on how to do it
// Warning: Requires CURL and command line (or similar)
let gclAccountId = "GoCardlessAccountID"
// Salt to hash external ID
let hashSalt = "GENERATE_YOUR_OWN"
let account_id = 0 // Account ID in LunchMoney
let lunchMoneyKey = "YOUR_LUNCH_MONEY_API_KEY";
function createDraft(tr) {
var notes = []
var payee = null;
var date = tr.bookingDate;
if (tr.creditorName != null) {
payee = tr.creditorName.trim()
let location = tr.remittanceInformationUnstructured.replace(tr.creditorName, "").trim()
var fxNote = null;
if (tr.currencyExchange != null && tr.currencyExchange.length == 1) {
let fxData = tr.currencyExchange[0];
if (fxData.sourceCurrency != fxData.targetCurrency) {
fxNote = "FX Rate: " + fxData.exchangeRate;
}
// Booking date is when the transaction was booked
// Quotation Date is when the transaction (and FX exchange) happened.
// This is filled also for same currency so we use it.
if (fxData.quotationDate != null) {
let dateParts = fxData.quotationDate.split("T")
if (dateParts.length == 2) {
date = dateParts[0];
}
}
}
notes = [tr.proprietaryBankTransactionCode, tr.additionalInformation.trim(), location, fxNote]
} else if (tr.debtorAccount != null) {
payee = tr.remittanceInformationUnstructured
}
return {
date: date,
amount: tr.transactionAmount.amount,
currency: tr.transactionAmount.currency.toLowerCase(),
payee: payee,
asset_id: account_id,
notes: notes.filter( x => x ).join(" – "),
external_id: createHash('sha256').update(tr.transactionId).update(tr.bookingDate).update(hashSalt).digest('hex')
}
}
async function main() {
let ccClient = axios.create({
baseURL: "https://bankaccountdata.gocardless.com/api/v2/"
});
const lunchMoney = axios.create({
baseURL: "https://dev.lunchmoney.app",
headers: {
"Authorization": `Bearer ${lunchMoneyKey}`
}
});
let lastTransactions = (await lunchMoney.get("/v1/transactions", {
params: {
start_date: "1970-01-01",
end_date: (new Date()).toISOString().split("T")[0],
limit: 5,
asset_id: account_id
}
})).data.transactions
var knownTransactions = new Set(lastTransactions.map( x => x.external_id));
let lastTransaction = lastTransactions[lastTransactions.length-1];
var endpoint = `/accounts/${gclAccountId}/transactions/`
var refDate = null;
if (lastTransaction != null && lastTransaction.date != null) {
endpoint += "?date_from="+lastTransaction.date;
refDate = Date.parse(lastTransaction.date);
} else {
refDate = Date.parse("1970-01-01");
}
try {
let ccClientAuthProm = ccClient.post("/token/new/", {
secret_id: gclSecretId,
secret_key: gclSecretKey
});
let authRes = (await ccClientAuthProm).data
ccClient.defaults.headers.common["Authorization"] = `Bearer ${authRes.access}`
let raw_t = await ccClient.get(endpoint);
let t = raw_t.data;
var draft = []
for (const transaction of t.transactions.booked) {
if (Date.parse(transaction.bookingDate) < refDate) {
continue;
}
let lm_tran = createDraft(transaction)
if (!knownTransactions.has(lm_tran.external_id)) {
draft.push(lm_tran)
knownTransactions.add(lm_tran.external_id)
}
}
} catch(e) {
console.log(e);
exit(1);
}
if (draft.length > 0) {
// We reverse because GCL sends transactions in descending order
// And lunch-money inserts in FIFO, so transactions with the same date will
// have an inverted display order
let createTransPromise = lunchMoney.post("/v1/transactions", {
transactions: draft.reverse(),
apply_rules:true,
debit_as_negative: true
})
let balance = (await ccClient.get(`/accounts/${gclAccountId}/balances/`)).data.balances
.find((e) => e.balanceType == "closingBooked");
let balanceAmount = ""
if (balance.balanceAmount.amount[0] == "-") {
balanceAmount = balance.balanceAmount.amount.substring(1);
} else {
balanceAmount = "-"+balance.balanceAmount.amount;
}
let updateBalance = await lunchMoney.put(`/v1/assets/${account_id}`, {
balance: balanceAmount,
balance_as_of: balance.referenceDate
});
console.log(await createTransPromise.data);
console.log(updateBalance.data);
}
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment