Implement account and transaction upsert functionality; add updateAccounts API endpoint

This commit is contained in:
2025-07-07 18:35:00 -04:00
parent 824a23765e
commit 3bc6b9ab7c
5 changed files with 181 additions and 61 deletions

View File

@ -166,5 +166,97 @@ export async function setTransactionNote(transactionId, note) {
return result
}
export async function updateAccounts(data) {
try {
console.log('Updating accounts with data:', data);
for (const account of data.accounts) {
// Upsert Org
console.log(`Upserting org for account: ${account.id}`, account.org);
await db`
insert into org (id, domain, name, sfin_url, url)
values (${account.org.id}, ${account.org.domain ?? null}, ${account.org.name ?? null}, ${account.org.sfin_url ?? null}, ${account.org.url ?? null})
on conflict (id) do update set
domain = excluded.domain,
name = excluded.name,
sfin_url = excluded.sfin_url,
url = excluded.url
`;
console.log(`Upserting account: ${account.id} (${account.name})`);
// Upsert Account
await db`
insert into account (id, org_id, name, currency, balance, available_balance, balance_date)
values (
${account.id},
${account.org.id},
${account.name ?? null},
${account.currency ?? null},
${account.balance ?? null},
${account.available_balance ?? null},
${account.balance_date ?? null}
)
on conflict (id) do update set
org_id = excluded.org_id,
name = excluded.name,
currency = excluded.currency,
balance = excluded.balance,
available_balance = excluded.available_balance,
balance_date = excluded.balance_date
`;
// Upsert Transactions
if (account.transactions && account.transactions.length > 0) {
for (const txn of account.transactions) {
let extraId = null;
console.log(`Upserting transaction: ${txn.id} for account: ${account.id}`);
if (txn.extra) {
// Upsert TransactionExtra (insert only, update not needed for category)
const extraResult = await db`
insert into transaction_extra (category)
values (${txn.extra.category ?? null})
on conflict (category) do nothing
returning id
`;
if (extraResult.length > 0) {
extraId = extraResult[0].id;
} else {
// If already exists, fetch id
const existing = await db`
select id from transaction_extra where category = ${txn.extra.category ?? null}
`;
if (existing.length > 0) {
extraId = existing[0].id;
}
}
}
console.log(`Preparing to upsert transaction: ${txn.id} with data:`, txn);
await db`
insert into transaction (id, account_id, posted, amount, description, pending, extra_id)
values (
${txn.id},
${account.id},
${txn.posted},
${txn.amount ?? null},
${txn.description ?? null},
${txn.pending},
${extraId}
)
on conflict (id) do update set
account_id = excluded.account_id,
posted = excluded.posted,
amount = excluded.amount,
description = excluded.description,
pending = excluded.pending,
extra_id = excluded.extra_id
`;
}
}
}
return true;
} catch (error) {
console.error('updateAccounts error:', error);
return false;
}
}
export default db

View File

@ -1,5 +1,14 @@
export async function updateAccounts()
{
fetch
export async function fetchAccounts(url, startDate) {
const { username, password, origin, pathname } = new URL(url);
const start = Math.floor(startDate.getTime() / 1000);
const apiUrl = `${origin}${pathname}/accounts?start-date=${start}`;
const headers = {};
if (username && password) {
headers['Authorization'] = 'Basic ' + btoa(`${username}:${password}`);
}
const response = await fetch(apiUrl, { headers });
return await response.json();
}

View File

@ -1,38 +1,42 @@
<script>
import '../app.css';
let { children, data } = $props();
let budgets = $state(data.budgets);
let newBudget = $state({
name: '',
amount: 0,
notes: ''
});
async function saveBudget() {
let res = await fetch('/budget', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newBudget)
let budgets = $state(data.budgets);
let newBudget = $state({
name: '',
amount: 0,
notes: ''
});
async function update() {
let res = await fetch('/api/simplefin/update', {
method: 'POST'
});
if (res.ok) {
AddBudgetModal.close();
budgets.push({
id: res.text(), // Temporary ID, replace with actual ID from the server
...newBudget
});
newBudget = { name: '', amount: 0, notes: '' }; // Reset the form
// Optionally, you can refresh the budgets list or show a success message
} else {
console.error('Failed to save budget');
}
}
async function saveBudget() {
let res = await fetch('/budget', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newBudget)
});
if (res.ok) {
AddBudgetModal.close();
budgets.push({
id: res.text(), // Temporary ID, replace with actual ID from the server
...newBudget
});
newBudget = { name: '', amount: 0, notes: '' }; // Reset the form
// Optionally, you can refresh the budgets list or show a success message
} else {
console.error('Failed to save budget');
}
}
</script>
<div class="drawer lg:drawer-open">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col items-center justify-center">
<div class="drawer-content flex flex-col m-5">
<!-- Page content here -->
{@render children()}
<label for="my-drawer-2" class="btn btn-primary drawer-button lg:hidden"> Open drawer </label>
@ -40,14 +44,10 @@
<div class="drawer-side">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
{#each data.accounts as account}
<li>
<a href={`/account/${account.id}`}>
{account.name} ({account.balance})
</a>
</li>
{/each}
<li><div class="divider"></div></li>
<li>
<button onclick={update}>Update</button>
</li>
<li><div class="divider">Budgets</div></li>
{#each budgets as budget}
<li>
<a href={`/budget/${budget.id}`}>
@ -56,7 +56,7 @@
</li>
{/each}
<li>
<button class="btn btn-circle" aria-label="Add" onclick={()=>AddBudgetModal.showModal()}>
<button class="btn btn-circle" aria-label="Add" onclick={() => AddBudgetModal.showModal()}>
<svg
fill="#F0F0F0F0"
height="800px"
@ -76,30 +76,38 @@
</svg>
</button>
</li>
<li><div class="divider">Accounts</div></li>
{#each data.accounts as account}
<li>
<a href={`/account/${account.id}`}>
{account.name} ({account.balance})
</a>
</li>
{/each}
</ul>
</div>
</div>
<dialog id="AddBudgetModal" class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<div>
<input
bind:value={newBudget.name}
type="text"
placeholder="Budget Name"
class="input input-bordered w-full max-w-xs"/>
<input
bind:value={newBudget.amount}
type="number"
placeholder="Budget Amount"
class="input input-bordered w-full max-w-xs mt-2"/>
<textarea bind:value={newBudget.notes} class="textarea w-100"></textarea>
</div>
<button onclick={() => saveBudget()} class="btn btn-primary mt-4">Save</button>
</div>
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<div>
<input
bind:value={newBudget.name}
type="text"
placeholder="Budget Name"
class="input input-bordered w-full max-w-xs"
/>
<input
bind:value={newBudget.amount}
type="number"
placeholder="Budget Amount"
class="input input-bordered w-full max-w-xs mt-2"
/>
<textarea bind:value={newBudget.notes} class="textarea w-100"></textarea>
</div>
<button onclick={() => saveBudget()} class="btn btn-primary mt-4">Save</button>
</div>
</dialog>

View File

@ -63,7 +63,7 @@
<h4>${currentTransaction?.amount}</h4>
<p> {currentTransaction?.date?.toDateString()}</p>
<div>
<textarea bind:value={notes} class="textarea w-100"/>
<textarea bind:value={notes} class="textarea w-100"></textarea>
</div>
<button onclick={() => saveNotes()} class="btn btn-primary mt-4">Save</button>
</div>

View File

@ -0,0 +1,11 @@
import { fetchAccounts } from '$lib/simplefin';
import { updateAccounts } from '$lib/db' ;
const url = "https://19443E0E8171E175EC5DA0C69B35DD50197F234B9A74C00D27FD606121257ECF:DAA3702E2100CFFD3B544251E6D755E86B1EDDFBFCC7F6FA9CE77AB3677E60DE@beta-bridge.simplefin.org/simplefin";
export async function POST() {
const res = await fetchAccounts(url, new Date("2025-07-01"))
return new Response(await updateAccounts(res));
}