Skip to content

Commit

Permalink
autosync Fidelity balances and autosync all on tab change
Browse files Browse the repository at this point in the history
  • Loading branch information
anh-chu committed Sep 14, 2024
1 parent 93512ee commit d4db07e
Show file tree
Hide file tree
Showing 14 changed files with 274 additions and 37 deletions.
13 changes: 12 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

gitleaks detect --source . -v
if ! [ -x "$(command -v docker)" ]; then
if ! [ -x "$(command -v podman)" ]; then
echo 'docker or podman could not be found.' >&2
exit 1
fi
podman run --rm -v $PWD:/app ghcr.io/zricethezav/gitleaks:latest detect --source /app -v
fi

if [ -x "$(command -v docker)" ]; then
docker run --rm -v $PWD:/app ghcr.io/zricethezav/gitleaks:latest detect --source /app -v
fi

npx pretty-quick --staged
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"prettier.tabWidth": 4,
"prettier.useTabs": true,
"prettier.singleAttributePerLine": true,
"prettier.printWidth": 120
}
"prettier.printWidth": 120,
"editor.tabSize": 2
}
4 changes: 4 additions & 0 deletions public/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ <h3>Main config</h3>
<label> TCB base URL </label>
<input type="text" id="tcbUrl" class="pure-input-rounded" required />
</div>
<div class="pure-control-group">
<label> Fidelity base URL </label>
<input type="text" id="fidelityUrl" class="pure-input-rounded" required />
</div>
<div class="pure-control-group">
<label> Actual Base URL </label>
<input type="text" id="actualUrl" class="pure-input-rounded" required />
Expand Down
2 changes: 2 additions & 0 deletions public/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<button id="run">Download ZIPs</button>
<button id="auto-run">Sync with API</button>

<button id="fidelity">Fidelity</button>

<div id="lastSyncWrap">
<b>Last transaction:</b>
<i id="lastSync"></i>
Expand Down
24 changes: 21 additions & 3 deletions src/actual/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getMappings } from "../util/mapping";

let actual: string;
let actualPassword: string;
let actualBudgetId: string;
Expand Down Expand Up @@ -51,6 +53,23 @@ export async function getAccounts(token: string) {
return await r.json();
}

export async function getAccountBalance(
token: string,
accountId?: string
): Promise<{ value: number }> {
const body = JSON.stringify({ _: [accountId] });
const r = await fetch(`${actual}/api/getAccountBalance?paramsInBody=true`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body,
});
return await r.json();
}

export async function getLastTransaction(
token: string,
accountId?: string
Expand All @@ -60,11 +79,10 @@ export async function getLastTransaction(
id: string;
}[];
}> {
const { checkpoint: checkpoint } = await getMappings();
const raw = {
q: "transactions",
filter: accountId
? { account: accountId, cleared: true }
: { cleared: true },
filter: { account: accountId ? accountId : checkpoint, cleared: true },
select: "date",
orderBy: {
date: "desc",
Expand Down
105 changes: 83 additions & 22 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,98 @@
import { execute } from "./execute";
import { apiSync } from "./autorun";
import { downloadTcbNoSync } from "./sync/downloadTcbNoSync";
import { apiSync } from "./sync/sync";
import { getCurrentTab } from "./util/chrome";
import { fidelitySync } from "./sync/sync";

let tcb: string;
chrome.storage.sync.get(["tcbUrl", "actualUrl"], (items) => {
let fidelity: string;

chrome.storage.sync.get(["tcbUrl", "fidelityUrl"], (items) => {
tcb = items.tcbUrl;
fidelity = items.fidelityUrl;
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action != "execute") return true;

getCurrentTab().then((tab: any) => {
if (!tab.url?.startsWith(tcb)) {
sendResponse({ text: "Wrong tab!" });
return true;
}
execute(message.body.minDate, message.body.maxDate).then(() => {});
function runFidelitySync() {
return getCurrentTab().then((tab) => {
if (!tab.url?.startsWith(fidelity)) throw new Error("Wrong tab!");
fidelitySync().then(() => {});
});
}

return true;
});
function runTcbSync() {
return getCurrentTab().then((tab) => {
if (!tab.url?.startsWith(tcb)) throw new Error("Wrong tab!");
apiSync().then(() => {});
});
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action != "auto-run") return true;
const minDate: string = message.minDate || "";
switch (message.action) {
case "execute": {
getCurrentTab().then((tab: any) => {
if (!tab.url?.startsWith(tcb)) {
sendResponse({ text: "Wrong tab!" });
return true;
}
downloadTcbNoSync(message.body.minDate, message.body.maxDate).then(
() => {}
);
});
break;
}
case "auto-run": {
const minDate: string = message.minDate;
runTcbSync().then(() =>
chrome.storage.local.set({ lastTcbSync: new Date().toISOString() })
);
break;
}
case "fidelity": {
runFidelitySync().then(() =>
chrome.storage.local.set({ lastFidelitySync: new Date().toISOString() })
);
break;
}
}
});

getCurrentTab().then((tab) => {
if (!tab.url?.startsWith(tcb)) {
sendResponse({ text: "Wrong tab!" });
return true;
// sync Fidelity automatically on tab change
chrome.tabs.onActivated.addListener((activeInfo) => {
chrome.tabs.get(activeInfo.tabId).then((tab) => {
if (tab.url?.startsWith(fidelity)) {
chrome.storage.local.get(["lastFidelitySync"], (items) => {
const lastFidelitySync = items.lastFidelitySync;
if (
lastFidelitySync &&
new Date().getTime() - new Date(lastFidelitySync).getTime() <
4 * 60 * 60 * 1000
)
return;
runFidelitySync().then(() =>
chrome.storage.local.set({
lastFidelitySync: new Date().toISOString(),
})
);
});
}
apiSync(minDate).then(() => {});
});
});

return true;
//sync TCB automatically on tab change
chrome.tabs.onActivated.addListener((activeInfo) => {
chrome.tabs.get(activeInfo.tabId).then((tab) => {
if (tab.url?.startsWith(tcb)) {
chrome.storage.local.get(["lastTcbSync"], (items) => {
const lastTcbSync = items.lastTcbSync;
if (
lastTcbSync &&
new Date().getTime() - new Date(lastTcbSync).getTime() <
4 * 60 * 60 * 1000
)
return;
runTcbSync().then(() =>
chrome.storage.local.set({ lastTcbSync: new Date().toISOString() })
);
});
}
});
});
47 changes: 47 additions & 0 deletions src/fidelity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { getCurrentTab } from "../util/chrome";

let fidelity: string;
chrome.storage.sync.get(["fidelityUrl", "actualUrl"], (items) => {
fidelity = items.fidelityUrl;
});

async function getAccessToken() {
const activeTab = await getCurrentTab();
const tabId = activeTab?.id as number;

function _() {
try {
return sessionStorage?.access_token as string;
} catch (err) {
console.error("Error occured in getting sessionStorage", err);
}
}

const injectionResults = await chrome.scripting.executeScript({
target: { tabId: tabId },
func: _,
});
return injectionResults[0]?.result;
}

export async function getBalances(): Promise<Balances> {
const myHeaders = new Headers();
myHeaders.append("accept", "*/*");
myHeaders.append("accept-language", "en-US,en");
myHeaders.append("content-type", "application/json");

const graphql = JSON.stringify({
query:
"query GetContext {\n getContext {\n person {\n assets {\n acctNum\n preferenceDetail {\n name\n }\n gainLossBalanceDetail {\n totalMarketVal\n }\n }\n }\n }\n}\n",
variables: {},
});
const requestOptions = {
method: "POST",
headers: myHeaders,
body: graphql,
};

const url = `${fidelity}/ftgw/digital/portfolio/api/graphql?ref_at=portsum`;
const r = await fetch(url, requestOptions);
return await r.json();
}
24 changes: 24 additions & 0 deletions src/fidelity/process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getBalances } from "./index";
import { getMappings } from "../util/mapping";
import { getExchangeRate } from "../util/currency";

export async function processFidelity() {
const balances = await getBalances();
const { bm: balanceMapping } = await getMappings();
let accounts = {} as { [key: string]: number };
const exchangeRate = await getExchangeRate("USD");

balances.data.getContext.person.assets.forEach((asset) => {
if (balanceMapping[asset.acctNum]) {
const assetValue = asset.gainLossBalanceDetail.totalMarketVal;
const accountId = balanceMapping[asset.acctNum];
// convert asset value to VND
const assetValueVnd = assetValue * exchangeRate;
accounts[accountId] = assetValueVnd;
}
});

console.log(accounts);

return accounts;
}
5 changes: 5 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const handleFileSelect = () => {
const text = reader.result;
const config = JSON.parse(text);
document?.getElementById("tcbUrl")?.value = config.tcb_url;
document?.getElementById("fidelityUrl")?.value = config.fidelity_url;
document?.getElementById("actualUrl")?.value = config.actual_url;
document?.getElementById("actualPassword")?.value = config.actual_password;
document?.getElementById("actualBudgetId")?.value = config.actual_budget_id;
Expand All @@ -24,6 +25,7 @@ const handleFileSelect = () => {

const saveOptions = () => {
const tcbUrl = document?.getElementById("tcbUrl")?.value;
const fidelityUrl = document?.getElementById("fidelityUrl")?.value;
const actualUrl = document?.getElementById("actualUrl")?.value;
const actualPassword = document?.getElementById("actualPassword")?.value;
const actualBudgetId = document?.getElementById("actualBudgetId")?.value;
Expand All @@ -34,6 +36,7 @@ const saveOptions = () => {
chrome.storage.sync.set(
{
tcbUrl,
fidelityUrl,
actualUrl,
actualPassword,
actualBudgetId,
Expand All @@ -60,6 +63,7 @@ const restoreOptions = () => {
chrome.storage.sync.get(
[
"tcbUrl",
"fidelityUrl",
"actualUrl",
"actualPassword",
"actualBudgetId",
Expand All @@ -68,6 +72,7 @@ const restoreOptions = () => {
],
(items) => {
document.getElementById("tcbUrl").value = items?.tcbUrl || "";
document.getElementById("fidelityUrl").value = items?.fidelityUrl || "";
document.getElementById("actualUrl").value = items?.actualUrl || "";
document.getElementById("actualPassword").value =
items?.actualPassword || "";
Expand Down
7 changes: 7 additions & 0 deletions src/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
document.getElementById("newSync")?.textContent = message.body;
return true;
});

document.getElementById("fidelity")?.addEventListener("click", () => {
chrome.runtime.sendMessage({
action: "fidelity",
});
return true;
});
8 changes: 4 additions & 4 deletions src/execute.ts → src/sync/downloadTcbNoSync.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Parser } from "@json2csv/plainjs";
import { splitAndProcessTransaction } from "./tcb/process";
import { getArrangements, getTransactions } from "./tcb";
import { flattenObject } from "./util/flatten";
import { splitAndProcessTransaction } from "../tcb/process";
import { getArrangements, getTransactions } from "../tcb";
import { flattenObject } from "../util/flatten";

export async function execute(
export async function downloadTcbNoSync(
minDate: string,
maxDate: string,
from: number = 0,
Expand Down
36 changes: 32 additions & 4 deletions src/autorun.ts → src/sync/sync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { initActual, getLastTransaction, importTransactions } from "./actual";
import { getArrangements, getTransactions } from "./tcb";
import { splitAndProcessTransaction } from "./tcb/process";
import { sendMessage } from "./util/chrome";
import {
initActual,
getLastTransaction,
importTransactions,
getAccountBalance,
} from "../actual";
import { getArrangements, getTransactions } from "../tcb";
import { splitAndProcessTransaction } from "../tcb/process";
import { sendMessage } from "../util/chrome";
import { processFidelity } from "../fidelity/process";

export async function apiSync(startDate: string = "") {
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
Expand Down Expand Up @@ -42,3 +48,25 @@ export async function apiSync(startDate: string = "") {
body: latestTransaction.data[0].date,
}).then(() => {});
}

export async function fidelitySync() {
const token = await initActual();

const fidelityBalances = await processFidelity();

for (const [accountId, newBalance] of Object.entries(fidelityBalances)) {
const currentBalance = (await getAccountBalance(token, accountId)).value;
const transactionValue = Math.ceil(+newBalance * 100 - currentBalance);
console.log(accountId, newBalance, currentBalance, transactionValue);
if (transactionValue !== 0) {
const transaction: ActualTransaction = {
account: accountId,
amount: transactionValue,
date: new Date().toISOString().split("T")[0],
cleared: true,
notes: "Reconciliation balance adjustment",
};
await importTransactions(token, accountId, [transaction]);
}
}
}
Loading

0 comments on commit d4db07e

Please sign in to comment.