From 34b1524d3a32252a6ba6e899fc6463b85f554c9a Mon Sep 17 00:00:00 2001 From: grabowski Date: Fri, 17 Apr 2026 16:24:00 +0700 Subject: [PATCH] Add FX rate per account, convert foreign balances to base currency in budget Accounts now have fxRateToBase (default 1.0). The budget total query multiplies each transaction by the account's rate, so a USD account with rate 34.5 contributes correctly to the THB-denominated budget. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/server/db/schema.ts | 2 ++ .../companies/[companyId]/+layout.server.ts | 4 ++-- .../[companyId]/accounts/+page.server.ts | 4 ++++ .../companies/[companyId]/accounts/+page.svelte | 17 ++++++++++++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 86e54ae..47a4198 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -893,6 +893,8 @@ export const companyAccounts = pgTable( creditLimit: numeric('credit_limit', { precision: 15, scale: 2 }), statementCloseDay: integer('statement_close_day'), paymentDueDay: integer('payment_due_day'), + // FX conversion to company base currency (e.g. 34.5 for USD→THB) + fxRateToBase: numeric('fx_rate_to_base', { precision: 18, scale: 8 }).notNull().default('1'), // Banking integration link externalAccountId: uuid('external_account_id').references(() => externalAccounts.id, { onDelete: 'set null' diff --git a/src/routes/(app)/companies/[companyId]/+layout.server.ts b/src/routes/(app)/companies/[companyId]/+layout.server.ts index a3da916..2c90fde 100644 --- a/src/routes/(app)/companies/[companyId]/+layout.server.ts +++ b/src/routes/(app)/companies/[companyId]/+layout.server.ts @@ -27,10 +27,10 @@ export const load: LayoutServerLoad = async ({ locals, params }) => { error(403, 'Not a member of this company'); } - // Total budget = sum of all non-deleted account balances + // Total budget = sum of all non-deleted account balances, converted to base currency const [balanceRow] = await db .select({ - total: sql`coalesce(sum(${companyAccountTransactions.amount}), '0')::text` + total: sql`coalesce(sum(${companyAccountTransactions.amount} * ${companyAccounts.fxRateToBase}), '0')::text` }) .from(companyAccountTransactions) .innerJoin(companyAccounts, eq(companyAccountTransactions.accountId, companyAccounts.id)) diff --git a/src/routes/(app)/companies/[companyId]/accounts/+page.server.ts b/src/routes/(app)/companies/[companyId]/accounts/+page.server.ts index 2be3760..d0a162b 100644 --- a/src/routes/(app)/companies/[companyId]/accounts/+page.server.ts +++ b/src/routes/(app)/companies/[companyId]/accounts/+page.server.ts @@ -112,6 +112,7 @@ type AccountFields = { creditLimit: string | null; statementCloseDay: number | null; paymentDueDay: number | null; + fxRateToBase: string; externalAccountId: string | null; }; @@ -171,6 +172,7 @@ function extractAccountFields(fd: FormData): creditLimit: parseDecimalOrNull(fd.get('creditLimit')), statementCloseDay, paymentDueDay, + fxRateToBase: parseDecimalOrNull(fd.get('fxRateToBase')) ?? '1', externalAccountId: trimOrNull(fd.get('externalAccountId')) } }; @@ -304,6 +306,7 @@ export const actions: Actions = { creditLimit: f.creditLimit, statementCloseDay: f.statementCloseDay, paymentDueDay: f.paymentDueDay, + fxRateToBase: f.fxRateToBase, externalAccountId: f.externalAccountId }) .returning({ id: companyAccounts.id }); @@ -383,6 +386,7 @@ export const actions: Actions = { creditLimit: f.creditLimit, statementCloseDay: f.statementCloseDay, paymentDueDay: f.paymentDueDay, + fxRateToBase: f.fxRateToBase, externalAccountId: f.externalAccountId, updatedAt: new Date() }) diff --git a/src/routes/(app)/companies/[companyId]/accounts/+page.svelte b/src/routes/(app)/companies/[companyId]/accounts/+page.svelte index 51b413c..7b5c5ce 100644 --- a/src/routes/(app)/companies/[companyId]/accounts/+page.svelte +++ b/src/routes/(app)/companies/[companyId]/accounts/+page.svelte @@ -143,6 +143,7 @@ creditLimit?: string | null; statementCloseDay?: number | null; paymentDueDay?: number | null; + fxRateToBase?: string | null; externalAccountId?: string | null; } = {} )} @@ -176,7 +177,20 @@ class={inputCls} /> -
+
+ + +

1.0 if same as company currency

+
{#if type === 'bank'}
@@ -891,6 +905,7 @@ creditLimit: acct.creditLimit, statementCloseDay: acct.statementCloseDay, paymentDueDay: acct.paymentDueDay, + fxRateToBase: acct.fxRateToBase, externalAccountId: acct.externalAccountId })}