Created
April 10, 2024 16:56
-
-
Save dtorres/c92a0e59e613ad7fc142f6f9c15137a1 to your computer and use it in GitHub Desktop.
JS Script to import transactions via GoCardless API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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