Implement account and transaction upsert functionality; add updateAccounts API endpoint
This commit is contained in:
@ -166,5 +166,97 @@ export async function setTransactionNote(transactionId, note) {
|
|||||||
return result
|
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
|
export default db
|
||||||
@ -1,5 +1,14 @@
|
|||||||
|
|
||||||
export async function updateAccounts()
|
export async function fetchAccounts(url, startDate) {
|
||||||
{
|
const { username, password, origin, pathname } = new URL(url);
|
||||||
fetch
|
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();
|
||||||
}
|
}
|
||||||
@ -1,38 +1,42 @@
|
|||||||
<script>
|
<script>
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
let { children, data } = $props();
|
let { children, data } = $props();
|
||||||
let budgets = $state(data.budgets);
|
let budgets = $state(data.budgets);
|
||||||
let newBudget = $state({
|
let newBudget = $state({
|
||||||
name: '',
|
name: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
notes: ''
|
notes: ''
|
||||||
});
|
});
|
||||||
async function saveBudget() {
|
async function update() {
|
||||||
let res = await fetch('/budget', {
|
let res = await fetch('/api/simplefin/update', {
|
||||||
method: 'POST',
|
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="drawer lg:drawer-open">
|
<div class="drawer lg:drawer-open">
|
||||||
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
<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 -->
|
<!-- Page content here -->
|
||||||
{@render children()}
|
{@render children()}
|
||||||
<label for="my-drawer-2" class="btn btn-primary drawer-button lg:hidden"> Open drawer </label>
|
<label for="my-drawer-2" class="btn btn-primary drawer-button lg:hidden"> Open drawer </label>
|
||||||
@ -40,14 +44,10 @@
|
|||||||
<div class="drawer-side">
|
<div class="drawer-side">
|
||||||
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
|
<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">
|
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
|
||||||
{#each data.accounts as account}
|
<li>
|
||||||
<li>
|
<button onclick={update}>Update</button>
|
||||||
<a href={`/account/${account.id}`}>
|
</li>
|
||||||
{account.name} ({account.balance})
|
<li><div class="divider">Budgets</div></li>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
<li><div class="divider"></div></li>
|
|
||||||
{#each budgets as budget}
|
{#each budgets as budget}
|
||||||
<li>
|
<li>
|
||||||
<a href={`/budget/${budget.id}`}>
|
<a href={`/budget/${budget.id}`}>
|
||||||
@ -56,7 +56,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-circle" aria-label="Add" onclick={()=>AddBudgetModal.showModal()}>
|
<button class="btn btn-circle" aria-label="Add" onclick={() => AddBudgetModal.showModal()}>
|
||||||
<svg
|
<svg
|
||||||
fill="#F0F0F0F0"
|
fill="#F0F0F0F0"
|
||||||
height="800px"
|
height="800px"
|
||||||
@ -76,30 +76,38 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<dialog id="AddBudgetModal" class="modal">
|
<dialog id="AddBudgetModal" class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div>
|
||||||
<div>
|
<input
|
||||||
<input
|
bind:value={newBudget.name}
|
||||||
bind:value={newBudget.name}
|
type="text"
|
||||||
type="text"
|
placeholder="Budget Name"
|
||||||
placeholder="Budget Name"
|
class="input input-bordered w-full max-w-xs"
|
||||||
class="input input-bordered w-full max-w-xs"/>
|
/>
|
||||||
<input
|
<input
|
||||||
bind:value={newBudget.amount}
|
bind:value={newBudget.amount}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Budget Amount"
|
placeholder="Budget Amount"
|
||||||
class="input input-bordered w-full max-w-xs mt-2"/>
|
class="input input-bordered w-full max-w-xs mt-2"
|
||||||
<textarea bind:value={newBudget.notes} class="textarea w-100"></textarea>
|
/>
|
||||||
</div>
|
<textarea bind:value={newBudget.notes} class="textarea w-100"></textarea>
|
||||||
<button onclick={() => saveBudget()} class="btn btn-primary mt-4">Save</button>
|
</div>
|
||||||
</div>
|
<button onclick={() => saveBudget()} class="btn btn-primary mt-4">Save</button>
|
||||||
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<h4>${currentTransaction?.amount}</h4>
|
<h4>${currentTransaction?.amount}</h4>
|
||||||
<p> {currentTransaction?.date?.toDateString()}</p>
|
<p> {currentTransaction?.date?.toDateString()}</p>
|
||||||
<div>
|
<div>
|
||||||
<textarea bind:value={notes} class="textarea w-100"/>
|
<textarea bind:value={notes} class="textarea w-100"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button onclick={() => saveNotes()} class="btn btn-primary mt-4">Save</button>
|
<button onclick={() => saveNotes()} class="btn btn-primary mt-4">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
11
src/routes/api/simplefin/update/+server.js
Normal file
11
src/routes/api/simplefin/update/+server.js
Normal 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));
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user