mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-28 18:29:22 +00:00
Compare commits
5 Commits
da456b4c80
...
0315ba4bc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0315ba4bc6 | ||
|
|
476a28d1f4 | ||
|
|
95a5b27e04 | ||
|
|
2c7ecfb912 | ||
|
|
12052ff96f |
@@ -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 uniqueBanks = new Set(accounts?.map(acc => acc.bank_name) || []).size;
|
||||
const uniqueBanks = new Set(accounts?.map(acc => acc.institution_id) || []).size;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -125,54 +129,57 @@ export default function AccountsOverview() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{accounts.map((account) => {
|
||||
const accountBalance = balances?.find(b => b.account_id === account.id);
|
||||
const balance = account.balance || accountBalance?.balance_amount || 0;
|
||||
const isPositive = balance >= 0;
|
||||
{accounts.map((account) => {
|
||||
// Get balance from account's balances array or fallback to balances query
|
||||
const accountBalance = account.balances?.[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 (
|
||||
<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 space-x-4">
|
||||
<div className="p-3 bg-gray-100 rounded-full">
|
||||
<Building2 className="h-6 w-6 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-medium text-gray-900">
|
||||
{account.name}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{account.bank_name} • {account.account_type}
|
||||
</p>
|
||||
{account.iban && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
IBAN: {account.iban}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<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 space-x-4">
|
||||
<div className="p-3 bg-gray-100 rounded-full">
|
||||
<Building2 className="h-6 w-6 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-medium text-gray-900">
|
||||
{account.name || 'Unnamed Account'}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{account.institution_id} • {account.status}
|
||||
</p>
|
||||
{account.iban && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
IBAN: {account.iban}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isPositive ? (
|
||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<p className={`text-lg font-semibold ${
|
||||
isPositive ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{formatCurrency(balance, account.currency)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
Updated {formatDate(account.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="text-right">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isPositive ? (
|
||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<p className={`text-lg font-semibold ${
|
||||
isPositive ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{formatCurrency(balance, currency)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
Updated {formatDate(account.last_accessed || account.created)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
X,
|
||||
Home,
|
||||
List,
|
||||
BarChart3
|
||||
BarChart3,
|
||||
Wifi,
|
||||
WifiOff
|
||||
} from 'lucide-react';
|
||||
import { apiClient } from '../lib/api';
|
||||
import AccountsOverview from './AccountsOverview';
|
||||
@@ -28,13 +30,27 @@ export default function Dashboard() {
|
||||
queryFn: apiClient.getAccounts,
|
||||
});
|
||||
|
||||
const { data: healthStatus, isLoading: healthLoading } = useQuery({
|
||||
queryKey: ['health'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000'}/api/v1/health`);
|
||||
return response.json();
|
||||
},
|
||||
refetchInterval: 30000, // Check every 30 seconds
|
||||
retry: 3,
|
||||
});
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Overview', icon: Home, id: 'overview' as TabType },
|
||||
{ name: 'Transactions', icon: List, id: 'transactions' 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 (
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
@@ -125,8 +141,22 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Activity className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-gray-600">Connected</span>
|
||||
{healthLoading ? (
|
||||
<>
|
||||
<Activity className="h-4 w-4 text-yellow-500 animate-pulse" />
|
||||
<span className="text-sm text-gray-600">Checking...</span>
|
||||
</>
|
||||
) : healthStatus?.success ? (
|
||||
<>
|
||||
<Wifi className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-gray-600">Connected</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="h-4 w-4 text-red-500" />
|
||||
<span className="text-sm text-red-500">Disconnected</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<option value="">All accounts</option>
|
||||
{accounts?.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name} ({account.bank_name})
|
||||
</option>
|
||||
))}
|
||||
{accounts?.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name || 'Unnamed Account'} ({account.institution_id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -259,10 +259,10 @@ export default function TransactionsList() {
|
||||
{transaction.description}
|
||||
</h4>
|
||||
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
{account && (
|
||||
<p>{account.name} • {account.bank_name}</p>
|
||||
)}
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
{account && (
|
||||
<p>{account.name || 'Unnamed Account'} • {account.institution_id}</p>
|
||||
)}
|
||||
|
||||
{(transaction.creditor_name || transaction.debtor_name) && (
|
||||
<p>
|
||||
|
||||
@@ -5,10 +5,22 @@ export function cn(...inputs: ClassValue[]) {
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number, currency: string = 'EUR'): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
}).format(amount);
|
||||
// Validate currency code - must be 3 letters and a valid ISO 4217 code
|
||||
const validCurrency = currency && /^[A-Z]{3}$/.test(currency) ? currency : 'EUR';
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
export interface AccountBalance {
|
||||
amount: number;
|
||||
currency: string;
|
||||
balance_type: string;
|
||||
last_change_date?: string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: string;
|
||||
name: string;
|
||||
bank_name: string;
|
||||
account_type: string;
|
||||
currency: string;
|
||||
balance?: number;
|
||||
institution_id: string;
|
||||
status: string;
|
||||
iban?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name?: string;
|
||||
currency?: string;
|
||||
created: string;
|
||||
last_accessed?: string;
|
||||
balances: AccountBalance[];
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
|
||||
@@ -16,6 +16,31 @@ def persist_balances(ctx: click.Context, balance: dict):
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create the accounts table if it doesn't exist
|
||||
cursor.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
institution_id TEXT,
|
||||
status TEXT,
|
||||
iban TEXT,
|
||||
name TEXT,
|
||||
currency TEXT,
|
||||
created DATETIME,
|
||||
last_accessed DATETIME,
|
||||
last_updated DATETIME
|
||||
)"""
|
||||
)
|
||||
|
||||
# Create indexes for accounts table
|
||||
cursor.execute(
|
||||
"""CREATE INDEX IF NOT EXISTS idx_accounts_institution_id
|
||||
ON accounts(institution_id)"""
|
||||
)
|
||||
cursor.execute(
|
||||
"""CREATE INDEX IF NOT EXISTS idx_accounts_status
|
||||
ON accounts(status)"""
|
||||
)
|
||||
|
||||
# Create the balances table if it doesn't exist
|
||||
cursor.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS balances (
|
||||
@@ -185,6 +210,7 @@ def get_transactions(
|
||||
min_amount=None,
|
||||
max_amount=None,
|
||||
search=None,
|
||||
hide_missing_ids=True,
|
||||
):
|
||||
"""Get transactions from SQLite database with optional filtering"""
|
||||
from pathlib import Path
|
||||
@@ -224,6 +250,11 @@ def get_transactions(
|
||||
query += " AND description LIKE ?"
|
||||
params.append(f"%{search}%")
|
||||
|
||||
if hide_missing_ids:
|
||||
query += (
|
||||
" AND internalTransactionId IS NOT NULL AND internalTransactionId != ''"
|
||||
)
|
||||
|
||||
# Add ordering and pagination
|
||||
query += " ORDER BY transactionDate DESC"
|
||||
|
||||
@@ -372,6 +403,11 @@ def get_transaction_count(account_id=None, **filters):
|
||||
query += " AND description LIKE ?"
|
||||
params.append(f"%{filters['search']}%")
|
||||
|
||||
if filters.get("hide_missing_ids", True):
|
||||
query += (
|
||||
" AND internalTransactionId IS NOT NULL AND internalTransactionId != ''"
|
||||
)
|
||||
|
||||
try:
|
||||
cursor.execute(query, params)
|
||||
count = cursor.fetchone()[0]
|
||||
@@ -381,3 +417,133 @@ def get_transaction_count(account_id=None, **filters):
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
raise e
|
||||
|
||||
|
||||
def persist_account(account_data: dict):
|
||||
"""Persist account details to SQLite database"""
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path.home() / ".config" / "leggen" / "leggen.db"
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create the accounts table if it doesn't exist
|
||||
cursor.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
institution_id TEXT,
|
||||
status TEXT,
|
||||
iban TEXT,
|
||||
name TEXT,
|
||||
currency TEXT,
|
||||
created DATETIME,
|
||||
last_accessed DATETIME,
|
||||
last_updated DATETIME
|
||||
)"""
|
||||
)
|
||||
|
||||
# Create indexes for accounts table
|
||||
cursor.execute(
|
||||
"""CREATE INDEX IF NOT EXISTS idx_accounts_institution_id
|
||||
ON accounts(institution_id)"""
|
||||
)
|
||||
cursor.execute(
|
||||
"""CREATE INDEX IF NOT EXISTS idx_accounts_status
|
||||
ON accounts(status)"""
|
||||
)
|
||||
|
||||
try:
|
||||
# Insert or replace account data
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO accounts (
|
||||
id,
|
||||
institution_id,
|
||||
status,
|
||||
iban,
|
||||
name,
|
||||
currency,
|
||||
created,
|
||||
last_accessed,
|
||||
last_updated
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
account_data["id"],
|
||||
account_data["institution_id"],
|
||||
account_data["status"],
|
||||
account_data.get("iban"),
|
||||
account_data.get("name"),
|
||||
account_data.get("currency"),
|
||||
account_data["created"],
|
||||
account_data.get("last_accessed"),
|
||||
account_data.get("last_updated", account_data["created"]),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
success(f"[{account_data['id']}] Account details persisted to database")
|
||||
return account_data
|
||||
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
raise e
|
||||
|
||||
|
||||
def get_accounts(account_ids=None):
|
||||
"""Get account details from SQLite database"""
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path.home() / ".config" / "leggen" / "leggen.db"
|
||||
if not db_path.exists():
|
||||
return []
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = "SELECT * FROM accounts"
|
||||
params = []
|
||||
|
||||
if account_ids:
|
||||
placeholders = ",".join("?" * len(account_ids))
|
||||
query += f" WHERE id IN ({placeholders})"
|
||||
params.extend(account_ids)
|
||||
|
||||
query += " ORDER BY created DESC"
|
||||
|
||||
try:
|
||||
cursor.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
accounts = [dict(row) for row in rows]
|
||||
conn.close()
|
||||
return accounts
|
||||
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
raise e
|
||||
|
||||
|
||||
def get_account(account_id: str):
|
||||
"""Get specific account details from SQLite database"""
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path.home() / ".config" / "leggen" / "leggen.db"
|
||||
if not db_path.exists():
|
||||
return None
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT * FROM accounts WHERE id = ?", (account_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
raise e
|
||||
|
||||
@@ -9,67 +9,65 @@ from leggend.api.models.accounts import (
|
||||
Transaction,
|
||||
TransactionSummary,
|
||||
)
|
||||
from leggend.services.gocardless_service import GoCardlessService
|
||||
from leggend.services.database_service import DatabaseService
|
||||
|
||||
router = APIRouter()
|
||||
gocardless_service = GoCardlessService()
|
||||
database_service = DatabaseService()
|
||||
|
||||
|
||||
@router.get("/accounts", response_model=APIResponse)
|
||||
async def get_all_accounts() -> APIResponse:
|
||||
"""Get all connected accounts"""
|
||||
"""Get all connected accounts from database"""
|
||||
try:
|
||||
requisitions_data = await gocardless_service.get_requisitions()
|
||||
|
||||
all_accounts = set()
|
||||
for req in requisitions_data.get("results", []):
|
||||
all_accounts.update(req.get("accounts", []))
|
||||
|
||||
accounts = []
|
||||
for account_id in all_accounts:
|
||||
|
||||
# Get all account details from database
|
||||
db_accounts = await database_service.get_accounts_from_db()
|
||||
|
||||
# Process accounts found in database
|
||||
for db_account in db_accounts:
|
||||
try:
|
||||
account_details = await gocardless_service.get_account_details(
|
||||
account_id
|
||||
)
|
||||
balances_data = await gocardless_service.get_account_balances(
|
||||
account_id
|
||||
# Get latest balances from database for this account
|
||||
balances_data = await database_service.get_balances_from_db(
|
||||
db_account["id"]
|
||||
)
|
||||
|
||||
# Process balances
|
||||
balances = []
|
||||
for balance in balances_data.get("balances", []):
|
||||
balance_amount = balance["balanceAmount"]
|
||||
for balance in balances_data:
|
||||
balances.append(
|
||||
AccountBalance(
|
||||
amount=float(balance_amount["amount"]),
|
||||
currency=balance_amount["currency"],
|
||||
balance_type=balance["balanceType"],
|
||||
last_change_date=balance.get("lastChangeDateTime"),
|
||||
amount=balance["amount"],
|
||||
currency=balance["currency"],
|
||||
balance_type=balance["type"],
|
||||
last_change_date=balance.get("timestamp"),
|
||||
)
|
||||
)
|
||||
|
||||
accounts.append(
|
||||
AccountDetails(
|
||||
id=account_details["id"],
|
||||
institution_id=account_details["institution_id"],
|
||||
status=account_details["status"],
|
||||
iban=account_details.get("iban"),
|
||||
name=account_details.get("name"),
|
||||
currency=account_details.get("currency"),
|
||||
created=account_details["created"],
|
||||
last_accessed=account_details.get("last_accessed"),
|
||||
id=db_account["id"],
|
||||
institution_id=db_account["institution_id"],
|
||||
status=db_account["status"],
|
||||
iban=db_account.get("iban"),
|
||||
name=db_account.get("name"),
|
||||
currency=db_account.get("currency"),
|
||||
created=db_account["created"],
|
||||
last_accessed=db_account.get("last_accessed"),
|
||||
balances=balances,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get details for account {account_id}: {e}")
|
||||
logger.error(
|
||||
f"Failed to process database account {db_account['id']}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
return APIResponse(
|
||||
success=True, data=accounts, message=f"Retrieved {len(accounts)} accounts"
|
||||
success=True,
|
||||
data=accounts,
|
||||
message=f"Retrieved {len(accounts)} accounts from database",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -81,46 +79,55 @@ async def get_all_accounts() -> APIResponse:
|
||||
|
||||
@router.get("/accounts/{account_id}", response_model=APIResponse)
|
||||
async def get_account_details(account_id: str) -> APIResponse:
|
||||
"""Get details for a specific account"""
|
||||
"""Get details for a specific account from database"""
|
||||
try:
|
||||
account_details = await gocardless_service.get_account_details(account_id)
|
||||
balances_data = await gocardless_service.get_account_balances(account_id)
|
||||
# Get account details from database
|
||||
db_account = await database_service.get_account_details_from_db(account_id)
|
||||
|
||||
if not db_account:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Account {account_id} not found in database"
|
||||
)
|
||||
|
||||
# Get latest balances from database for this account
|
||||
balances_data = await database_service.get_balances_from_db(account_id)
|
||||
|
||||
# Process balances
|
||||
balances = []
|
||||
for balance in balances_data.get("balances", []):
|
||||
balance_amount = balance["balanceAmount"]
|
||||
for balance in balances_data:
|
||||
balances.append(
|
||||
AccountBalance(
|
||||
amount=float(balance_amount["amount"]),
|
||||
currency=balance_amount["currency"],
|
||||
balance_type=balance["balanceType"],
|
||||
last_change_date=balance.get("lastChangeDateTime"),
|
||||
amount=balance["amount"],
|
||||
currency=balance["currency"],
|
||||
balance_type=balance["type"],
|
||||
last_change_date=balance.get("timestamp"),
|
||||
)
|
||||
)
|
||||
|
||||
account = AccountDetails(
|
||||
id=account_details["id"],
|
||||
institution_id=account_details["institution_id"],
|
||||
status=account_details["status"],
|
||||
iban=account_details.get("iban"),
|
||||
name=account_details.get("name"),
|
||||
currency=account_details.get("currency"),
|
||||
created=account_details["created"],
|
||||
last_accessed=account_details.get("last_accessed"),
|
||||
id=db_account["id"],
|
||||
institution_id=db_account["institution_id"],
|
||||
status=db_account["status"],
|
||||
iban=db_account.get("iban"),
|
||||
name=db_account.get("name"),
|
||||
currency=db_account.get("currency"),
|
||||
created=db_account["created"],
|
||||
last_accessed=db_account.get("last_accessed"),
|
||||
balances=balances,
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=account,
|
||||
message=f"Account details retrieved for {account_id}",
|
||||
message=f"Account details retrieved from database for {account_id}",
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get account details for {account_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Account not found: {str(e)}"
|
||||
status_code=500, detail=f"Failed to get account details: {str(e)}"
|
||||
) from e
|
||||
|
||||
|
||||
@@ -157,6 +164,56 @@ async def get_account_balances(account_id: str) -> APIResponse:
|
||||
) 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)
|
||||
async def get_account_transactions(
|
||||
account_id: str,
|
||||
|
||||
@@ -5,11 +5,9 @@ from loguru import logger
|
||||
|
||||
from leggend.api.models.common import APIResponse
|
||||
from leggend.api.models.accounts import Transaction, TransactionSummary
|
||||
from leggend.services.gocardless_service import GoCardlessService
|
||||
from leggend.services.database_service import DatabaseService
|
||||
|
||||
router = APIRouter()
|
||||
gocardless_service = GoCardlessService()
|
||||
database_service = DatabaseService()
|
||||
|
||||
|
||||
@@ -20,6 +18,9 @@ async def get_all_transactions(
|
||||
summary_only: bool = Query(
|
||||
default=True, description="Return transaction summaries only"
|
||||
),
|
||||
hide_missing_ids: bool = Query(
|
||||
default=True, description="Hide transactions without internalTransactionId"
|
||||
),
|
||||
date_from: Optional[str] = Query(
|
||||
default=None, description="Filter from date (YYYY-MM-DD)"
|
||||
),
|
||||
@@ -49,6 +50,18 @@ async def get_all_transactions(
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
search=search,
|
||||
hide_missing_ids=hide_missing_ids,
|
||||
)
|
||||
|
||||
# Get total count for pagination info (respecting the same filters)
|
||||
total_transactions = await database_service.get_transaction_count_from_db(
|
||||
account_id=account_id,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
search=search,
|
||||
hide_missing_ids=hide_missing_ids,
|
||||
)
|
||||
|
||||
# Get total count for pagination info
|
||||
@@ -113,6 +126,9 @@ async def get_all_transactions(
|
||||
async def get_transaction_stats(
|
||||
days: int = Query(default=30, description="Number of days to include in stats"),
|
||||
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
|
||||
hide_missing_ids: bool = Query(
|
||||
default=True, description="Hide transactions without internalTransactionId"
|
||||
),
|
||||
) -> APIResponse:
|
||||
"""Get transaction statistics for the last N days from database"""
|
||||
try:
|
||||
@@ -130,6 +146,7 @@ async def get_transaction_stats(
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
limit=None, # Get all matching transactions for stats
|
||||
hide_missing_ids=hide_missing_ids,
|
||||
)
|
||||
|
||||
# Calculate stats
|
||||
|
||||
@@ -77,9 +77,32 @@ def create_app() -> FastAPI:
|
||||
version = "unknown"
|
||||
return {"message": "Leggend API is running", "version": version}
|
||||
|
||||
@app.get("/health")
|
||||
@app.get("/api/v1/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "config_loaded": config._config is not None}
|
||||
"""Health check endpoint for API connectivity"""
|
||||
try:
|
||||
from leggend.api.models.common import APIResponse
|
||||
|
||||
config_loaded = config._config is not None
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={
|
||||
"status": "healthy",
|
||||
"config_loaded": config_loaded,
|
||||
"message": "API is running and responsive",
|
||||
},
|
||||
message="Health check successful",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
from leggend.api.models.common import APIResponse
|
||||
|
||||
return APIResponse(
|
||||
success=False,
|
||||
data={"status": "unhealthy", "error": str(e)},
|
||||
message="Health check failed",
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ class DatabaseService:
|
||||
min_amount: Optional[float] = None,
|
||||
max_amount: Optional[float] = None,
|
||||
search: Optional[str] = None,
|
||||
hide_missing_ids: bool = True,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get transactions from SQLite database"""
|
||||
if not self.sqlite_enabled:
|
||||
@@ -124,13 +125,14 @@ class DatabaseService:
|
||||
try:
|
||||
transactions = sqlite_db.get_transactions(
|
||||
account_id=account_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
limit=limit or 100,
|
||||
offset=offset or 0,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
search=search,
|
||||
hide_missing_ids=hide_missing_ids,
|
||||
)
|
||||
logger.debug(f"Retrieved {len(transactions)} transactions from database")
|
||||
return transactions
|
||||
@@ -146,6 +148,7 @@ class DatabaseService:
|
||||
min_amount: Optional[float] = None,
|
||||
max_amount: Optional[float] = None,
|
||||
search: Optional[str] = None,
|
||||
hide_missing_ids: bool = True,
|
||||
) -> int:
|
||||
"""Get total count of transactions from SQLite database"""
|
||||
if not self.sqlite_enabled:
|
||||
@@ -162,7 +165,9 @@ class DatabaseService:
|
||||
# Remove None values
|
||||
filters = {k: v for k, v in filters.items() if v is not None}
|
||||
|
||||
count = sqlite_db.get_transaction_count(account_id=account_id, **filters)
|
||||
count = sqlite_db.get_transaction_count(
|
||||
account_id=account_id, hide_missing_ids=hide_missing_ids, **filters
|
||||
)
|
||||
logger.debug(f"Total transaction count: {count}")
|
||||
return count
|
||||
except Exception as e:
|
||||
@@ -203,6 +208,49 @@ class DatabaseService:
|
||||
logger.error(f"Failed to get account summary from database: {e}")
|
||||
return None
|
||||
|
||||
async def persist_account_details(self, account_data: Dict[str, Any]) -> None:
|
||||
"""Persist account details to database"""
|
||||
if not self.sqlite_enabled:
|
||||
logger.warning("SQLite database disabled, skipping account persistence")
|
||||
return
|
||||
|
||||
await self._persist_account_details_sqlite(account_data)
|
||||
|
||||
async def get_accounts_from_db(
|
||||
self, account_ids: Optional[List[str]] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get account details from database"""
|
||||
if not self.sqlite_enabled:
|
||||
logger.warning("SQLite database disabled, cannot read accounts")
|
||||
return []
|
||||
|
||||
try:
|
||||
accounts = sqlite_db.get_accounts(account_ids=account_ids)
|
||||
logger.debug(f"Retrieved {len(accounts)} accounts from database")
|
||||
return accounts
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get accounts from database: {e}")
|
||||
return []
|
||||
|
||||
async def get_account_details_from_db(
|
||||
self, account_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get specific account details from database"""
|
||||
if not self.sqlite_enabled:
|
||||
logger.warning("SQLite database disabled, cannot read account")
|
||||
return None
|
||||
|
||||
try:
|
||||
account = sqlite_db.get_account(account_id)
|
||||
if account:
|
||||
logger.debug(
|
||||
f"Retrieved account details from database for {account_id}"
|
||||
)
|
||||
return account
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get account details from database: {e}")
|
||||
return None
|
||||
|
||||
async def _persist_balance_sqlite(
|
||||
self, account_id: str, balance_data: Dict[str, Any]
|
||||
) -> None:
|
||||
@@ -381,3 +429,23 @@ class DatabaseService:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to persist transactions to SQLite: {e}")
|
||||
raise
|
||||
|
||||
async def _persist_account_details_sqlite(
|
||||
self, account_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Persist account details to SQLite"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path.home() / ".config" / "leggen" / "leggen.db"
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Use the sqlite_db module function
|
||||
sqlite_db.persist_account(account_data)
|
||||
|
||||
logger.info(
|
||||
f"Persisted account details to SQLite for account {account_data['id']}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to persist account details to SQLite: {e}")
|
||||
raise
|
||||
|
||||
@@ -55,6 +55,10 @@ class SyncService:
|
||||
account_id
|
||||
)
|
||||
|
||||
# Persist account details to database
|
||||
if account_details:
|
||||
await self.database.persist_account_details(account_details)
|
||||
|
||||
# Get and save balances
|
||||
balances = await self.gocardless.get_account_balances(account_id)
|
||||
if balances:
|
||||
|
||||
7
opencode.json
Normal file
7
opencode.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permission": {
|
||||
"edit": "ask",
|
||||
"bash": "ask"
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,18 @@ def temp_config_dir():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(temp_config_dir):
|
||||
def temp_db_path():
|
||||
"""Create a temporary database file for testing."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp_file:
|
||||
db_path = Path(tmp_file.name)
|
||||
yield db_path
|
||||
# Clean up the temporary database file after test
|
||||
if db_path.exists():
|
||||
db_path.unlink()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(temp_config_dir, temp_db_path):
|
||||
"""Mock configuration for testing."""
|
||||
config_data = {
|
||||
"gocardless": {
|
||||
@@ -72,6 +83,28 @@ def api_client(fastapi_app):
|
||||
return TestClient(fastapi_app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_path(temp_db_path):
|
||||
"""Mock the database path to use temporary database for testing."""
|
||||
from pathlib import Path
|
||||
|
||||
# Create the expected directory structure
|
||||
temp_home = temp_db_path.parent
|
||||
config_dir = temp_home / ".config" / "leggen"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create the expected database path
|
||||
expected_db_path = config_dir / "leggen.db"
|
||||
|
||||
# Mock Path.home to return our temp directory
|
||||
def mock_home():
|
||||
return temp_home
|
||||
|
||||
# Patch Path.home in the main pathlib module
|
||||
with patch.object(Path, "home", staticmethod(mock_home)):
|
||||
yield expected_db_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_bank_data():
|
||||
"""Sample bank/institution data for testing."""
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Tests for accounts API endpoints."""
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
import httpx
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -10,44 +8,51 @@ from unittest.mock import patch
|
||||
class TestAccountsAPI:
|
||||
"""Test account-related API endpoints."""
|
||||
|
||||
@respx.mock
|
||||
def test_get_all_accounts_success(
|
||||
self, api_client, mock_config, mock_auth_token, sample_account_data
|
||||
self,
|
||||
api_client,
|
||||
mock_config,
|
||||
mock_auth_token,
|
||||
sample_account_data,
|
||||
mock_db_path,
|
||||
):
|
||||
"""Test successful retrieval of all accounts."""
|
||||
requisitions_data = {
|
||||
"results": [{"id": "req-123", "accounts": ["test-account-123"]}]
|
||||
}
|
||||
"""Test successful retrieval of all accounts from database."""
|
||||
mock_accounts = [
|
||||
{
|
||||
"id": "test-account-123",
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"status": "READY",
|
||||
"iban": "LT313250081177977789",
|
||||
"created": "2024-02-13T23:56:00Z",
|
||||
"last_accessed": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
]
|
||||
|
||||
balances_data = {
|
||||
"balances": [
|
||||
{
|
||||
"balanceAmount": {"amount": "100.50", "currency": "EUR"},
|
||||
"balanceType": "interimAvailable",
|
||||
"lastChangeDateTime": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_balances = [
|
||||
{
|
||||
"id": 1,
|
||||
"account_id": "test-account-123",
|
||||
"bank": "REVOLUT_REVOLT21",
|
||||
"status": "active",
|
||||
"iban": "LT313250081177977789",
|
||||
"amount": 100.50,
|
||||
"currency": "EUR",
|
||||
"type": "interimAvailable",
|
||||
"timestamp": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
]
|
||||
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
# Mock GoCardless API calls
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/requisitions/").mock(
|
||||
return_value=httpx.Response(200, json=requisitions_data)
|
||||
)
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/"
|
||||
).mock(return_value=httpx.Response(200, json=sample_account_data))
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/balances/"
|
||||
).mock(return_value=httpx.Response(200, json=balances_data))
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
with (
|
||||
patch("leggend.config.config", mock_config),
|
||||
patch(
|
||||
"leggend.api.routes.accounts.database_service.get_accounts_from_db",
|
||||
return_value=mock_accounts,
|
||||
),
|
||||
patch(
|
||||
"leggend.api.routes.accounts.database_service.get_balances_from_db",
|
||||
return_value=mock_balances,
|
||||
),
|
||||
):
|
||||
response = api_client.get("/api/v1/accounts")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -60,36 +65,49 @@ class TestAccountsAPI:
|
||||
assert len(account["balances"]) == 1
|
||||
assert account["balances"][0]["amount"] == 100.50
|
||||
|
||||
@respx.mock
|
||||
def test_get_account_details_success(
|
||||
self, api_client, mock_config, mock_auth_token, sample_account_data
|
||||
self,
|
||||
api_client,
|
||||
mock_config,
|
||||
mock_auth_token,
|
||||
sample_account_data,
|
||||
mock_db_path,
|
||||
):
|
||||
"""Test successful retrieval of specific account details."""
|
||||
balances_data = {
|
||||
"balances": [
|
||||
{
|
||||
"balanceAmount": {"amount": "250.75", "currency": "EUR"},
|
||||
"balanceType": "interimAvailable",
|
||||
}
|
||||
]
|
||||
"""Test successful retrieval of specific account details from database."""
|
||||
mock_account = {
|
||||
"id": "test-account-123",
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"status": "READY",
|
||||
"iban": "LT313250081177977789",
|
||||
"created": "2024-02-13T23:56:00Z",
|
||||
"last_accessed": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
mock_balances = [
|
||||
{
|
||||
"id": 1,
|
||||
"account_id": "test-account-123",
|
||||
"bank": "REVOLUT_REVOLT21",
|
||||
"status": "active",
|
||||
"iban": "LT313250081177977789",
|
||||
"amount": 250.75,
|
||||
"currency": "EUR",
|
||||
"type": "interimAvailable",
|
||||
"timestamp": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
]
|
||||
|
||||
# Mock GoCardless API calls
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/"
|
||||
).mock(return_value=httpx.Response(200, json=sample_account_data))
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/balances/"
|
||||
).mock(return_value=httpx.Response(200, json=balances_data))
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
with (
|
||||
patch("leggend.config.config", mock_config),
|
||||
patch(
|
||||
"leggend.api.routes.accounts.database_service.get_account_details_from_db",
|
||||
return_value=mock_account,
|
||||
),
|
||||
patch(
|
||||
"leggend.api.routes.accounts.database_service.get_balances_from_db",
|
||||
return_value=mock_balances,
|
||||
),
|
||||
):
|
||||
response = api_client.get("/api/v1/accounts/test-account-123")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -101,7 +119,7 @@ class TestAccountsAPI:
|
||||
assert len(account["balances"]) == 1
|
||||
|
||||
def test_get_account_balances_success(
|
||||
self, api_client, mock_config, mock_auth_token
|
||||
self, api_client, mock_config, mock_auth_token, mock_db_path
|
||||
):
|
||||
"""Test successful retrieval of account balances from database."""
|
||||
mock_balances = [
|
||||
@@ -153,6 +171,7 @@ class TestAccountsAPI:
|
||||
mock_auth_token,
|
||||
sample_account_data,
|
||||
sample_transaction_data,
|
||||
mock_db_path,
|
||||
):
|
||||
"""Test successful retrieval of account transactions from database."""
|
||||
mock_transactions = [
|
||||
@@ -203,6 +222,7 @@ class TestAccountsAPI:
|
||||
mock_auth_token,
|
||||
sample_account_data,
|
||||
sample_transaction_data,
|
||||
mock_db_path,
|
||||
):
|
||||
"""Test retrieval of full transaction details from database."""
|
||||
mock_transactions = [
|
||||
@@ -246,24 +266,17 @@ class TestAccountsAPI:
|
||||
assert transaction["iban"] == "LT313250081177977789"
|
||||
assert "raw_transaction" in transaction
|
||||
|
||||
def test_get_account_not_found(self, api_client, mock_config, mock_auth_token):
|
||||
def test_get_account_not_found(
|
||||
self, api_client, mock_config, mock_auth_token, mock_db_path
|
||||
):
|
||||
"""Test handling of non-existent account."""
|
||||
# Mock 404 response from GoCardless
|
||||
with respx.mock:
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
with (
|
||||
patch("leggend.config.config", mock_config),
|
||||
patch(
|
||||
"leggend.api.routes.accounts.database_service.get_account_details_from_db",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
response = api_client.get("/api/v1/accounts/nonexistent")
|
||||
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/nonexistent/"
|
||||
).mock(
|
||||
return_value=httpx.Response(404, json={"detail": "Account not found"})
|
||||
)
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/accounts/nonexistent")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -166,6 +166,7 @@ class TestTransactionsAPI:
|
||||
min_amount=-50.0,
|
||||
max_amount=0.0,
|
||||
search="Coffee",
|
||||
hide_missing_ids=True,
|
||||
)
|
||||
|
||||
def test_get_transactions_empty_result(
|
||||
|
||||
@@ -99,6 +99,7 @@ class TestDatabaseService:
|
||||
min_amount=None,
|
||||
max_amount=None,
|
||||
search=None,
|
||||
hide_missing_ids=True,
|
||||
)
|
||||
|
||||
async def test_get_transactions_from_db_with_filters(
|
||||
@@ -129,6 +130,7 @@ class TestDatabaseService:
|
||||
min_amount=-50.0,
|
||||
max_amount=0.0,
|
||||
search="Coffee",
|
||||
hide_missing_ids=True,
|
||||
)
|
||||
|
||||
async def test_get_transactions_from_db_sqlite_disabled(self, database_service):
|
||||
@@ -158,7 +160,9 @@ class TestDatabaseService:
|
||||
)
|
||||
|
||||
assert result == 42
|
||||
mock_get_count.assert_called_once_with(account_id="test-account-123")
|
||||
mock_get_count.assert_called_once_with(
|
||||
account_id="test-account-123", hide_missing_ids=True
|
||||
)
|
||||
|
||||
async def test_get_transaction_count_from_db_with_filters(self, database_service):
|
||||
"""Test getting transaction count with filters."""
|
||||
@@ -178,6 +182,7 @@ class TestDatabaseService:
|
||||
date_from="2025-09-01",
|
||||
min_amount=-100.0,
|
||||
search="Coffee",
|
||||
hide_missing_ids=True,
|
||||
)
|
||||
|
||||
async def test_get_transaction_count_from_db_sqlite_disabled(
|
||||
|
||||
Reference in New Issue
Block a user