refactor: consolidate formatting utilities and add testing infrastructure

- Replace 8 duplicate formatCurrency functions with shared utility
- Add comprehensive formatter utilities (currency, date, number, etc.)
- Set up Vitest for unit testing with 38 passing tests
- Set up Playwright for E2E testing
- Consolidate database connection to single source (src/utils/db.ts)
- Add auth middleware helpers to reduce code duplication
- Fix display bug: remove spurious minus sign in recent activity amounts
- Add path aliases for cleaner imports ($utils, $models)
- Add project documentation (CODEMAP.md, REFACTORING_PLAN.md)

Test coverage: 38 unit tests passing
Build: successful with no breaking changes
This commit is contained in:
2025-11-18 15:24:22 +01:00
parent d09dc2dfed
commit 10ee2e81ae
58 changed files with 11127 additions and 131 deletions

View File

@@ -10,7 +10,8 @@
import { isSettlementPayment, getSettlementIcon, getSettlementClasses, getSettlementReceiver } from '$lib/utils/settlements';
import AddButton from '$lib/components/AddButton.svelte';
export let data; // Contains session data and balance from server
import { formatCurrency } from '$lib/utils/formatters'; export let data; // Contains session data and balance from server
// Use server-side data, with fallback for progressive enhancement
let balance = data.balance || {
@@ -98,13 +99,6 @@
}
}
function formatCurrency(amount) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(Math.abs(amount));
}
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('de-CH');
}
@@ -211,10 +205,10 @@
</div>
<div class="settlement-arrow-section">
<div class="settlement-amount-large">
{formatCurrency(Math.abs(split.amount))}
{formatCurrency(Math.abs(split.amount), 'CHF', 'de-CH')}
</div>
<div class="settlement-flow-arrow"></div>
<div class="settlement-date">{formatDate(split.createdAt)}</div>
<div class="settlement-date">{formatDate(split.paymentId?.date || split.paymentId?.createdAt)}</div>
</div>
<div class="settlement-receiver">
<ProfilePicture username={getSettlementReceiverFromSplit(split) || 'Unknown'} size={64} />
@@ -247,17 +241,17 @@
class:positive={split.amount < 0}
class:negative={split.amount > 0}>
{#if split.amount > 0}
-{formatCurrency(split.amount)}
-{formatCurrency(Math.abs(split.amount), 'CHF', 'de-CH')}
{:else if split.amount < 0}
+{formatCurrency(split.amount)}
+{formatCurrency(Math.abs(split.amount), 'CHF', 'de-CH')}
{:else}
{formatCurrency(split.amount)}
{formatCurrency(split.amount, 'CHF', 'de-CH')}
{/if}
</div>
</div>
<div class="payment-details">
<div class="payment-meta">
<span class="payment-date">{formatDate(split.createdAt)}</span>
<span class="payment-date">{formatDate(split.paymentId?.date || split.paymentId?.createdAt)}</span>
</div>
{#if split.paymentId?.description}
<div class="payment-description">

View File

@@ -6,7 +6,8 @@
import { isSettlementPayment, getSettlementIcon, getSettlementReceiver } from '$lib/utils/settlements';
import AddButton from '$lib/components/AddButton.svelte';
export let data;
import { formatCurrency } from '$lib/utils/formatters'; export let data;
// Use server-side data with progressive enhancement
let payments = data.payments || [];
@@ -80,19 +81,13 @@
}
}
function formatCurrency(amount, currency = 'CHF') {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: currency
}).format(amount);
}
function formatAmountWithCurrency(payment) {
if (payment.currency === 'CHF' || !payment.originalAmount) {
return formatCurrency(payment.amount);
return formatCurrency(payment.amount, 'CHF', 'de-CH');
}
return `${formatCurrency(payment.originalAmount, payment.currency)}${formatCurrency(payment.amount)}`;
return `${formatCurrency(payment.originalAmount, payment.currency, 'CHF', 'de-CH')}${formatCurrency(payment.amount, 'CHF', 'de-CH')}`;
}
function formatDate(dateString) {
@@ -214,11 +209,11 @@
<span class="split-user">{split.username}</span>
<span class="split-amount" class:positive={split.amount < 0} class:negative={split.amount > 0}>
{#if split.amount > 0}
owes {formatCurrency(split.amount)}
owes {formatCurrency(split.amount, 'CHF', 'de-CH')}
{:else if split.amount < 0}
owed {formatCurrency(Math.abs(split.amount))}
owed {formatCurrency(Math.abs(split.amount, 'CHF', 'de-CH'))}
{:else}
owes {formatCurrency(split.amount)}
owes {formatCurrency(split.amount, 'CHF', 'de-CH')}
{/if}
</span>
</div>

View File

@@ -5,7 +5,8 @@
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
import EditButton from '$lib/components/EditButton.svelte';
export let data;
import { formatCurrency } from '$lib/utils/formatters'; export let data;
// Use server-side data with progressive enhancement
let payment = data.payment || null;
@@ -39,19 +40,12 @@
}
}
function formatCurrency(amount, currency = 'CHF') {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: currency
}).format(Math.abs(amount));
}
function formatAmountWithCurrency(payment) {
if (payment.currency === 'CHF' || !payment.originalAmount) {
return formatCurrency(payment.amount);
return formatCurrency(payment.amount, 'CHF', 'de-CH');
}
return `${formatCurrency(payment.originalAmount, payment.currency)}${formatCurrency(payment.amount)}`;
return `${formatCurrency(payment.originalAmount, payment.currency, 'CHF', 'de-CH')}${formatCurrency(payment.amount, 'CHF', 'de-CH')}`;
}
function formatDate(dateString) {
@@ -157,11 +151,11 @@
</div>
<div class="split-amount" class:positive={split.amount < 0} class:negative={split.amount > 0}>
{#if split.amount > 0}
owes {formatCurrency(split.amount)}
owes {formatCurrency(split.amount, 'CHF', 'de-CH')}
{:else if split.amount < 0}
owed {formatCurrency(split.amount)}
owed {formatCurrency(split.amount, 'CHF', 'de-CH')}
{:else}
owes {formatCurrency(split.amount)}
owes {formatCurrency(split.amount, 'CHF', 'de-CH')}
{/if}
</div>
</div>

View File

@@ -5,7 +5,8 @@
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
import AddButton from '$lib/components/AddButton.svelte';
export let data;
import { formatCurrency } from '$lib/utils/formatters'; export let data;
let recurringPayments = [];
let loading = true;
@@ -75,13 +76,6 @@
}
}
function formatCurrency(amount) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(Math.abs(amount));
}
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('de-CH');
}
@@ -131,7 +125,7 @@
</span>
</div>
<div class="payment-amount">
{formatCurrency(payment.amount)}
{formatCurrency(payment.amount, 'CHF', 'de-CH')}
</div>
</div>
@@ -189,11 +183,11 @@
<span class="username">{split.username}</span>
<span class="split-amount" class:positive={split.amount < 0} class:negative={split.amount > 0}>
{#if split.amount > 0}
owes {formatCurrency(split.amount)}
owes {formatCurrency(split.amount, 'CHF', 'de-CH')}
{:else if split.amount < 0}
gets {formatCurrency(split.amount)}
gets {formatCurrency(split.amount, 'CHF', 'de-CH')}
{:else}
owes {formatCurrency(split.amount)}
owes {formatCurrency(split.amount, 'CHF', 'de-CH')}
{/if}
</span>
</div>

View File

@@ -4,7 +4,8 @@
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
import { PREDEFINED_USERS, isPredefinedUsersMode } from '$lib/config/users';
export let data;
import { formatCurrency } from '$lib/utils/formatters'; export let data;
export let form;
// Use server-side data with progressive enhancement
@@ -133,12 +134,6 @@
}
}
function formatCurrency(amount) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
}
</script>
<svelte:head>
@@ -180,7 +175,7 @@
<ProfilePicture username={debt.username} size={40} />
<div class="user-details">
<span class="username">{debt.username}</span>
<span class="debt-amount">owes you {formatCurrency(debt.netAmount)}</span>
<span class="debt-amount">owes you {formatCurrency(debt.netAmount, 'CHF', 'de-CH')}</span>
</div>
</div>
<div class="settlement-action">
@@ -202,7 +197,7 @@
<ProfilePicture username={debt.username} size={40} />
<div class="user-details">
<span class="username">{debt.username}</span>
<span class="debt-amount">you owe {formatCurrency(debt.netAmount)}</span>
<span class="debt-amount">you owe {formatCurrency(debt.netAmount, 'CHF', 'de-CH')}</span>
</div>
</div>
<div class="settlement-action">
@@ -287,12 +282,12 @@
<option value="">Select settlement type</option>
{#each debtData.whoOwesMe as debt}
<option value="receive" data-from="{debt.username}" data-to="{data.currentUser}">
Receive {formatCurrency(debt.netAmount)} from {debt.username}
Receive {formatCurrency(debt.netAmount, 'CHF', 'de-CH')} from {debt.username}
</option>
{/each}
{#each debtData.whoIOwe as debt}
<option value="pay" data-from="{data.currentUser}" data-to="{debt.username}">
Pay {formatCurrency(debt.netAmount)} to {debt.username}
Pay {formatCurrency(debt.netAmount, 'CHF', 'de-CH')} to {debt.username}
</option>
{/each}
</select>