fix: resolve 404 balances endpoint and currency formatting errors

- Add missing /api/v1/balances endpoint to backend
- Update frontend Account type to match backend AccountDetails model
- Add currency validation with EUR fallback in formatCurrency function
- Update AccountsOverview, TransactionsList, and Dashboard components
- Fix balance calculations to use balances array structure
- All pre-commit checks pass
This commit is contained in:
Elisiário Couto
2025-09-08 23:45:31 +01:00
committed by Elisiário Couto
parent 947342e196
commit 417b77539f
6 changed files with 149 additions and 69 deletions

View File

@@ -61,9 +61,13 @@ export default function AccountsOverview() {
); );
} }
const totalBalance = accounts?.reduce((sum, account) => sum + (account.balance || 0), 0) || 0; const totalBalance = accounts?.reduce((sum, account) => {
// Get the first available balance from the balances array
const primaryBalance = account.balances?.[0]?.amount || 0;
return sum + primaryBalance;
}, 0) || 0;
const totalAccounts = accounts?.length || 0; const totalAccounts = accounts?.length || 0;
const uniqueBanks = new Set(accounts?.map(acc => acc.bank_name) || []).size; const uniqueBanks = new Set(accounts?.map(acc => acc.institution_id) || []).size;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -125,54 +129,57 @@ export default function AccountsOverview() {
</div> </div>
) : ( ) : (
<div className="divide-y divide-gray-200"> <div className="divide-y divide-gray-200">
{accounts.map((account) => { {accounts.map((account) => {
const accountBalance = balances?.find(b => b.account_id === account.id); // Get balance from account's balances array or fallback to balances query
const balance = account.balance || accountBalance?.balance_amount || 0; const accountBalance = account.balances?.[0];
const isPositive = balance >= 0; const fallbackBalance = balances?.find(b => b.account_id === account.id);
const balance = accountBalance?.amount || fallbackBalance?.balance_amount || 0;
const currency = accountBalance?.currency || fallbackBalance?.currency || account.currency || 'EUR';
const isPositive = balance >= 0;
return ( return (
<div key={account.id} className="p-6 hover:bg-gray-50 transition-colors"> <div key={account.id} className="p-6 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="p-3 bg-gray-100 rounded-full"> <div className="p-3 bg-gray-100 rounded-full">
<Building2 className="h-6 w-6 text-gray-600" /> <Building2 className="h-6 w-6 text-gray-600" />
</div> </div>
<div> <div>
<h4 className="text-lg font-medium text-gray-900"> <h4 className="text-lg font-medium text-gray-900">
{account.name} {account.name || 'Unnamed Account'}
</h4> </h4>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{account.bank_name} {account.account_type} {account.institution_id} {account.status}
</p> </p>
{account.iban && ( {account.iban && (
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
IBAN: {account.iban} IBAN: {account.iban}
</p> </p>
)} )}
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{isPositive ? ( {isPositive ? (
<TrendingUp className="h-4 w-4 text-green-500" /> <TrendingUp className="h-4 w-4 text-green-500" />
) : ( ) : (
<TrendingDown className="h-4 w-4 text-red-500" /> <TrendingDown className="h-4 w-4 text-red-500" />
)} )}
<p className={`text-lg font-semibold ${ <p className={`text-lg font-semibold ${
isPositive ? 'text-green-600' : 'text-red-600' isPositive ? 'text-green-600' : 'text-red-600'
}`}> }`}>
{formatCurrency(balance, account.currency)} {formatCurrency(balance, currency)}
</p> </p>
</div> </div>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Updated {formatDate(account.updated_at)} Updated {formatDate(account.last_accessed || account.created)}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
); );
})} })}
</div> </div>
)} )}
</div> </div>

View File

@@ -34,7 +34,11 @@ export default function Dashboard() {
{ name: 'Analytics', icon: BarChart3, id: 'analytics' as TabType }, { name: 'Analytics', icon: BarChart3, id: 'analytics' as TabType },
]; ];
const totalBalance = accounts?.reduce((sum, account) => sum + (account.balance || 0), 0) || 0; const totalBalance = accounts?.reduce((sum, account) => {
// Get the first available balance from the balances array
const primaryBalance = account.balances?.[0]?.amount || 0;
return sum + primaryBalance;
}, 0) || 0;
return ( return (
<div className="flex h-screen bg-gray-100"> <div className="flex h-screen bg-gray-100">

View File

@@ -163,11 +163,11 @@ export default function TransactionsList() {
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
> >
<option value="">All accounts</option> <option value="">All accounts</option>
{accounts?.map((account) => ( {accounts?.map((account) => (
<option key={account.id} value={account.id}> <option key={account.id} value={account.id}>
{account.name} ({account.bank_name}) {account.name || 'Unnamed Account'} ({account.institution_id})
</option> </option>
))} ))}
</select> </select>
</div> </div>
@@ -259,10 +259,10 @@ export default function TransactionsList() {
{transaction.description} {transaction.description}
</h4> </h4>
<div className="text-xs text-gray-500 space-y-1"> <div className="text-xs text-gray-500 space-y-1">
{account && ( {account && (
<p>{account.name} {account.bank_name}</p> <p>{account.name || 'Unnamed Account'} {account.institution_id}</p>
)} )}
{(transaction.creditor_name || transaction.debtor_name) && ( {(transaction.creditor_name || transaction.debtor_name) && (
<p> <p>

View File

@@ -5,10 +5,22 @@ export function cn(...inputs: ClassValue[]) {
} }
export function formatCurrency(amount: number, currency: string = 'EUR'): string { export function formatCurrency(amount: number, currency: string = 'EUR'): string {
return new Intl.NumberFormat('en-US', { // Validate currency code - must be 3 letters and a valid ISO 4217 code
style: 'currency', const validCurrency = currency && /^[A-Z]{3}$/.test(currency) ? currency : 'EUR';
currency: currency,
}).format(amount); try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: validCurrency,
}).format(amount);
} catch (error) {
// Fallback if currency is still invalid
console.warn(`Invalid currency code: ${currency}, falling back to EUR`);
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR',
}).format(amount);
}
} }
export function formatDate(date: string): string { export function formatDate(date: string): string {

View File

@@ -1,13 +1,20 @@
export interface AccountBalance {
amount: number;
currency: string;
balance_type: string;
last_change_date?: string;
}
export interface Account { export interface Account {
id: string; id: string;
name: string; institution_id: string;
bank_name: string; status: string;
account_type: string;
currency: string;
balance?: number;
iban?: string; iban?: string;
created_at: string; name?: string;
updated_at: string; currency?: string;
created: string;
last_accessed?: string;
balances: AccountBalance[];
} }
export interface Transaction { export interface Transaction {

View File

@@ -164,6 +164,56 @@ async def get_account_balances(account_id: str) -> APIResponse:
) from e ) from e
@router.get("/balances", response_model=APIResponse)
async def get_all_balances() -> APIResponse:
"""Get all balances from all accounts in database"""
try:
# Get all accounts first to iterate through them
db_accounts = await database_service.get_accounts_from_db()
all_balances = []
for db_account in db_accounts:
try:
# Get balances for this account
db_balances = await database_service.get_balances_from_db(
account_id=db_account["id"]
)
# Process balances and add account info
for balance in db_balances:
balance_data = {
"id": f"{db_account['id']}_{balance['type']}", # Create unique ID
"account_id": db_account["id"],
"balance_amount": balance["amount"],
"balance_type": balance["type"],
"currency": balance["currency"],
"reference_date": balance.get(
"timestamp", db_account.get("last_accessed")
),
"created_at": db_account.get("created"),
"updated_at": db_account.get("last_accessed"),
}
all_balances.append(balance_data)
except Exception as e:
logger.error(
f"Failed to get balances for account {db_account['id']}: {e}"
)
continue
return APIResponse(
success=True,
data=all_balances,
message=f"Retrieved {len(all_balances)} balances from {len(db_accounts)} accounts",
)
except Exception as e:
logger.error(f"Failed to get all balances: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get balances: {str(e)}"
) from e
@router.get("/accounts/{account_id}/transactions", response_model=APIResponse) @router.get("/accounts/{account_id}/transactions", response_model=APIResponse)
async def get_account_transactions( async def get_account_transactions(
account_id: str, account_id: str,