Skip to content

Commit

Permalink
fix: Handle Nordigen account status. (#4)
Browse files Browse the repository at this point in the history
fix: Handle Nordigen account status.

Checks current account status and halts if in ERROR or SUSPENDED.
If status changed since last execution, then reports status change to discord.
Adds more console logging since errors thrown in worker lack context.
  • Loading branch information
pdcalado authored Mar 6, 2023
1 parent 22ff95d commit c2f993e
Showing 1 changed file with 98 additions and 5 deletions.
103 changes: 98 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const DAYS_TO_FETCH = 2;
const NOTIFY_EXPIRATION_DAYS = 7;
// key in KV to store date when we last notified agreement expiration
const NOTIFY_EXPIRATION_KEY = "agreement-expiration-notified";
// key in KV to store last account status
const LAST_ACCOUNT_STATUS_KEY = "last-account-status";
// key in KV to store match transaction patterns
const TRANSACTION_MATCHERS_KEY = "transaction-matchers";
// Nordigen API host
Expand All @@ -46,6 +48,16 @@ async function markNotifiedToday(kv: KVNamespace): Promise<void> {
await kv.put(NOTIFY_EXPIRATION_KEY, today);
}

// gets latest account status from KV
async function getLatestAccountStatus(kv: KVNamespace): Promise<string | null> {
return await kv.get(LAST_ACCOUNT_STATUS_KEY);
}

// dets latest account status in KV
async function setLatestAccountStatus(kv: KVNamespace, status: string) {
return await kv.put(LAST_ACCOUNT_STATUS_KEY, status);
}

type BankTransaction = {
transactionId: string;
bookingDate: string;
Expand Down Expand Up @@ -88,6 +100,27 @@ async function fetchNordigenToken(
return resp.json().then((data: any) => data.access);
}

async function fetchNordigenAccountStatus(
token: string,
accountId: string,
): Promise<string> {
const url = NORDIGEN_HOST + `/api/v2/accounts/${accountId}`;

const resp = await fetch(url, {
method: "GET",
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + token
},
});

if (resp.status !== 200) {
throw Error(`get nordigen account status yielded (${resp.status}): (${resp.statusText})`);
}

return resp.json<{ status: string }>().then((data) => data.status);
}

async function fetchNordigenTransactions(
token: string,
accountId: string,
Expand All @@ -110,6 +143,10 @@ async function fetchNordigenTransactions(
},
});

if (resp.status !== 200) {
throw Error(`fetch nordigen transactions yielded (${resp.status}): (${resp.statusText})`);
}

return resp.json().then((data: any) => data);
}

Expand Down Expand Up @@ -229,6 +266,28 @@ async function notifyDiscord(
});
}

async function reportAccountStatus(
discordUrl: string,
accountId: string,
status: string,
) {
let msg = `account '${accountId}' has unknown status: ${status}`;

switch (status.toUpperCase()) {
case "SUSPENDED":
msg = `❌ account '${accountId}' has been ${status}!`;
break;
case "ERROR":
msg = `❌ account '${accountId}' is in ${status}, this may be temporary.`;
break;
case "READY":
msg = `✅ account '${accountId}' is now ready.`;
break;
}

return await notifyDiscord(discordUrl, msg);
}

async function fetchTransactionMatchers(kv: KVNamespace): Promise<TransactionMatcher[]> {
const matchersRaw = await kv.get(TRANSACTION_MATCHERS_KEY);
if (matchersRaw === null) {
Expand All @@ -242,19 +301,45 @@ async function execute(env: Env) {
try {
await doExecute(env);
} catch (e: any) {
console.error("execution failed:", e);
const stringed = JSON.stringify(e);
console.error("stringed", stringed);
console.error("type of", typeof e);
console.error("properties", e.message, e.lineNumber, e.fileName. e.stack);
for(var property in e){
for (var property in e) {
console.log("error props", e[property]);
}
}
}

async function doExecute(env: Env) {
console.log("fetching nordigen token");

// fetch token first
const token = await fetchNordigenToken(env.NORDIGEN_SECRET_ID, env.NORDIGEN_SECRET_KEY);

console.log("fetching nordigen account status");

// check account status before proceeding
const status = await fetchNordigenAccountStatus(token, env.NORDIGEN_ACCOUNT_ID)

console.log("comparing nordigen account status with previous");

// get latest account status
const previousStatus = await getLatestAccountStatus(env.KV);

console.log(`nordigen account status: ${status} (was ${previousStatus})`);

if (previousStatus === null || previousStatus.toUpperCase() !== status.toUpperCase()) {
await reportAccountStatus(env.DISCORD_URL, env.NORDIGEN_ACCOUNT_ID, status);
await setLatestAccountStatus(env.KV, status);
}

if (status === "SUSPENDED" || status === "ERROR") {
throw Error(`unable to proceed with status ${env.NORDIGEN_ACCOUNT_ID}`);
}

console.log("fetching nordigen agreement expiration");

// check agreement expiration and notify if needed
const expirationDays = await fetchNordigenAgreementExpiration(token, env.NORDIGEN_AGREEMENT_ID);
if (expirationDays <= NOTIFY_EXPIRATION_DAYS) {
Expand All @@ -266,17 +351,21 @@ async function doExecute(env: Env) {
}
}

console.log("fetching transactions (nordigen agreement still valid)");

// prepare to get transactions
const dateFrom = new Date(Date.now() - DAYS_TO_FETCH * ONE_DAY_MS).toISOString().split("T")[0]; // X days ago
const dateTo = new Date(Date.now()).toISOString().split("T")[0]; // today

const results = await fetchNordigenTransactions(token, env.NORDIGEN_ACCOUNT_ID, dateFrom, dateTo);

console.log("booked", results.transactions.booked);
console.log("storing booked transactions:", results.transactions.booked);

// store transactions in DB
await storeBankTransactions(env.DB, results.transactions.booked);

console.log("reading transaction matchers from KV");

// read transaction matchers from KV
const transactionMatchers = await fetchTransactionMatchers(env.KV);

Expand All @@ -295,12 +384,13 @@ async function doExecute(env: Env) {
})

if (matched.size === 0) {
console.log("no matching transactions");
return;
}

const matchedList = Array.from(matched.values());

console.log("matched", matchedList);
console.log("found matching transactions:", matchedList);

// list transactions which haven't yet been notified
const checkNotifiedResults = await checkNotifiedTransactions(env.DB, matchedList);
Expand All @@ -311,10 +401,11 @@ async function doExecute(env: Env) {
});

if (toNotify.length === 0) {
console.log("no transactions to notify");
return;
}

console.log("to notify", toNotify);
console.log("found transactions to notify:", toNotify);

// notify transactions
matched.forEach(async (tx, name) => {
Expand All @@ -324,6 +415,8 @@ async function doExecute(env: Env) {
);
});

console.log("storing notified transactions");

await storeNotifiedTransactions(env.DB, toNotify);
}

Expand Down

0 comments on commit c2f993e

Please sign in to comment.