Skip to content

Instantly share code, notes, and snippets.

@darsh12
Last active May 13, 2024 20:45
Show Gist options
  • Save darsh12/4e93abfc6a6307c6ae0933e325f2084f to your computer and use it in GitHub Desktop.
Save darsh12/4e93abfc6a6307c6ae0933e325f2084f to your computer and use it in GitHub Desktop.
Firefly-SimpleFIN-N8N
{
"name": "Firefly SimpleFIN Git",
"nodes": [
{
"parameters": {
"message": "={{ $json.errors.join() }}",
"additionalFields": {
"priority": 10
},
"options": {
"contentType": "text/plain"
}
},
"id": "e8791c55-7fa8-4c0a-a50f-463cfdf7ce9b",
"name": "Gotify",
"type": "n8n-nodes-base.gotify",
"typeVersion": 1,
"position": [
960,
100
],
"credentials": {
"gotifyApi": {
"id": "4WPYQ0VlpLcqQuKY",
"name": "Gotify account"
}
}
},
{
"parameters": {
"jsCode": "return $input.all();"
},
"id": "e4fb564e-f429-44a4-9c20-004ea66f6d8c",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
360
]
},
{
"parameters": {
"jsCode": "const orgIds = [\n { id:\"ACT-001122-1122-334n-0009-123456780dfg\", bank:\"11\"}, // Bank Account\n \n];\n\nlet transactions = [];\n\norgIds.forEach(org => {\n let account = $input.all()[0].json.accounts.find(account => account.id === org.id);\n\n if(!account) { // if no matching account found, skip the current iteration\n return;\n }\n\n let orgTransactions = account.transactions; \n orgTransactions.forEach((item)=>{\n item.bank = org.bank;\n const amount = Number(item.amount);\n\n // If the 'organization' is not an investment account, then we follow this logic.\n if (!org.investment){\n if (amount < 0){\n item.amount = Math.abs(amount).toString();\n item.type = \"withdrawal\";\n item.source_id = org.bank;\n } else {\n item.type=\"deposit\";\n item.destination_id = org.bank;\n }\n }else{\n // If the 'organization' is an investment account, then a negative amount means there is a deposit and we indicate it as such\n if (amount < 0){\n item.amount = Math.abs(amount).toString();\n item.type = \"deposit\";\n item.source_id = org.bank;\n }\n }\n });\n\n transactions = transactions.concat(orgTransactions);\n});\n\nreturn transactions;"
},
"id": "f443321a-b974-4d70-b57d-73fa22dcdcbc",
"name": "Transactions",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
760,
480
]
},
{
"parameters": {
"language": "python",
"pythonCode": "import json\nfrom collections import defaultdict\nfrom datetime import datetime, timedelta \n\n# We have added keywords, because there maybe some transactions that have different 'banks', made on the same day and\n# match the amount. Hence, we only want transactions that contain these keywords\nkeywords = ['transfer', 'zelle']\n# Amount in days to consider for an item transfer\ntimeDeltaDays=3\n\nclass Transaction:\n def __init__(self, id, type, amount, bank, source_id, destination_id, posted, description, payee, transacted_at):\n self.id = id\n self.type = type\n self.amount = float(amount)\n self.bank = bank\n self.source_id = source_id\n self.destination_id = destination_id\n self.posted = datetime.fromtimestamp(posted)\n self.description = description.strip()\n self.payee = payee\n self.transacted_at = datetime.fromtimestamp(transacted_at)\n\n def to_dict(self):\n return {\n 'id': self.id,\n 'type': self.type,\n 'amount': self.amount,\n 'bank': self.bank,\n 'source_id': self.source_id,\n 'destination_id': self.destination_id,\n 'posted': self.posted.timestamp(),\n 'description': self.description,\n 'payee': self.payee,\n 'transacted_at': self.transacted_at.timestamp(),\n }\n\nclass Transfer:\n def __init__(self, id, amount, source_id, destination_id, posted, description, payee, transacted_at):\n self.id = id\n self.amount = amount\n self.source_id = source_id\n self.destination_id = destination_id\n self.posted = posted\n self.description = description\n self.payee = payee\n self.transacted_at = transacted_at\n\n def to_dict(self):\n return {\n 'id': self.id,\n 'type': 'transfer',\n 'amount': self.amount,\n 'source_id': self.source_id,\n 'destination_id': self.destination_id,\n 'posted': self.posted.timestamp(),\n 'description': self.description,\n 'payee': self.payee,\n 'transacted_at': self.transacted_at.timestamp(),\n }\n\n \n\ntransactions=[]\nfor item in _input.all():\n t=item.json\n src_id = t['source_id'] if 'source_id' in t else \"\"\n dest_id = t['destination_id'] if 'destination_id' in t else \"\"\n description = \" \".join(t['description'].strip().split())\n payee = \" \".join(t['payee'].strip().split())\n transaction = Transaction(t['id'], t['type'], t['amount'], t['bank'], src_id, dest_id, t['posted'], description, payee, t['transacted_at'])\n transactions.append(transaction)\n\n\n# Group transactions by type and amount\ngrouped_transactions = defaultdict(lambda: defaultdict(list))\nfor t in transactions:\n grouped_transactions[t.type][t.amount].append(t)\n\n# Find transfers\nfor amount, deposit_transactions in grouped_transactions['deposit'].items():\n withdrawal_transactions = grouped_transactions['withdrawal'].get(amount, [])\n\n for deposit in deposit_transactions:\n for withdrawal in withdrawal_transactions:\n # If banks are different, posted times are within 1 day and descriptions contain required keywords\n if deposit.bank != withdrawal.bank and abs(deposit.posted - withdrawal.posted) <= timedelta(days=timeDeltaDays) and (\n any(\n keyword.lower() in deposit.description.lower() for keyword in keywords) or (any(\n keyword.lower() in withdrawal.description.lower() for keyword in keywords))):\n transfer = Transfer(withdrawal.id + deposit.id, deposit.amount, withdrawal.bank, deposit.bank,\n max(deposit.posted, withdrawal.posted),\n withdrawal.description + \" - \" + deposit.description, deposit.payee,\n deposit.transacted_at)\n transactions.remove(deposit)\n transactions.remove(withdrawal)\n withdrawal_transactions.remove(withdrawal)\n transactions.append(transfer)\n break\n\noutput=[]\nfor i in transactions:\n output.append(i.to_dict())\n\nreturn output"
},
"id": "1a5421c8-ce3a-48f2-8baf-3b6a1e5637c6",
"name": "Transfer Transactions",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
980,
340
]
},
{
"parameters": {
"url": "=https://beta-bridge.simplefin.org/simplefin/accounts",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "start-date",
"value": "={{ Math.floor(new Date().setDate(new Date().getDate() -5 )/1000);}}"
}
]
},
"options": {}
},
"id": "9f1759e6-68fe-45ee-9959-5e9b084ac2a7",
"name": "SimpleFin",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
740,
260
],
"credentials": {
"httpBasicAuth": {
"id": "8HQHP0oGrWIVg7HM",
"name": "SimpleFIN Basic Auth"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://firefly.paid.lol/api/v1/transactions",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"error_if_duplicate_hash\": true,\n \"apply_rules\": true,\n \"fire_webhooks\": true,\n \"group_title\": \"\",\n \"transactions\": [\n {\n \"type\": \"{{ $json[\"type\"] }}\",\n \"date\": \"{{ $json[\"transacted_at\"].toDateTime(\"s\").toISO() }}\",\n \"amount\": \"{{ $json[\"amount\"] }}\",\n \"description\": \"{{ $json[\"description\"] }}\",\n \"source_id\": \"{{ $json[\"source_id\"] }}\",\n \"destination_id\": \"{{ $json[\"destination_id\"] }}\",\n \"process_date\": \"{{ $json[\"posted\"].toDateTime(\"s\").toISO() }}\",\n \"destination_name\": \"{{ $json[\"payee\"] }}\",\n \"external_id\":\"{{ $json[\"id\"] }}\"\n } \n ]\n}",
"options": {}
},
"id": "981cdc9e-e324-4fdf-b664-912079926c79",
"name": "Firefly Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1180,
340
],
"credentials": {
"httpHeaderAuth": {
"id": "OixlU0thMigFLiC3",
"name": "Firefly Header Auth"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 14
}
]
}
},
"id": "010b5ff8-bcf7-4133-8f36-06a373e2ec6c",
"name": "Daily at 2pm",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
540,
260
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.errors }}",
"rightValue": "",
"operator": {
"type": "array",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"id": "f1d64143-35f5-4fe7-96c5-9c4d534eeb27",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
780,
100
]
}
],
"pinData": {},
"connections": {
"Transactions": {
"main": [
[
{
"node": "Transfer Transactions",
"type": "main",
"index": 0
}
]
]
},
"SimpleFin": {
"main": [
[
{
"node": "Transactions",
"type": "main",
"index": 0
},
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Transfer Transactions": {
"main": [
[
{
"node": "Firefly Request",
"type": "main",
"index": 0
}
]
]
},
"Firefly Request": {
"main": [
[],
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Daily at 2pm": {
"main": [
[
{
"node": "SimpleFin",
"type": "main",
"index": 0
}
]
]
},
"Switch": {
"main": [
[
{
"node": "Gotify",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner"
},
"versionId": "063bb1d8-6cc0-473c-a2f7-c251ca95f68b",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "5e1619f539bf4fca477bbc4fab24e9f25d671f9e9b0589672c29f0bdbe199d84"
},
"id": "jO9rvClvsHA3ZFia",
"tags": []
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment