mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-11 17:22:18 +00:00
refactor: Remove API response wrapper pattern.
Replace wrapped responses {success, data, message} with direct data returns
following REST best practices. Simplifies 41 endpoints across 7 route files
and updates all 109 tests. Also fixes test config setup to not require
user home directory config file.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
1176
frontend/package-lock.json
generated
1176
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import type {
|
||||
Transaction,
|
||||
AnalyticsTransaction,
|
||||
Balance,
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
NotificationSettings,
|
||||
NotificationTest,
|
||||
NotificationService,
|
||||
@@ -36,14 +36,14 @@ const api = axios.create({
|
||||
export const apiClient = {
|
||||
// Get all accounts
|
||||
getAccounts: async (): Promise<Account[]> => {
|
||||
const response = await api.get<ApiResponse<Account[]>>("/accounts");
|
||||
return response.data.data;
|
||||
const response = await api.get<Account[]>("/accounts");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get account by ID
|
||||
getAccount: async (id: string): Promise<Account> => {
|
||||
const response = await api.get<ApiResponse<Account>>(`/accounts/${id}`);
|
||||
return response.data.data;
|
||||
const response = await api.get<Account>(`/accounts/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update account details
|
||||
@@ -51,16 +51,17 @@ export const apiClient = {
|
||||
id: string,
|
||||
updates: AccountUpdate,
|
||||
): Promise<{ id: string; display_name?: string }> => {
|
||||
const response = await api.put<
|
||||
ApiResponse<{ id: string; display_name?: string }>
|
||||
>(`/accounts/${id}`, updates);
|
||||
return response.data.data;
|
||||
const response = await api.put<{ id: string; display_name?: string }>(
|
||||
`/accounts/${id}`,
|
||||
updates,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get all balances
|
||||
getBalances: async (): Promise<Balance[]> => {
|
||||
const response = await api.get<ApiResponse<Balance[]>>("/balances");
|
||||
return response.data.data;
|
||||
const response = await api.get<Balance[]>("/balances");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get historical balances for balance progression chart
|
||||
@@ -72,18 +73,18 @@ export const apiClient = {
|
||||
if (days) queryParams.append("days", days.toString());
|
||||
if (accountId) queryParams.append("account_id", accountId);
|
||||
|
||||
const response = await api.get<ApiResponse<Balance[]>>(
|
||||
const response = await api.get<Balance[]>(
|
||||
`/balances/history?${queryParams.toString()}`,
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get balances for specific account
|
||||
getAccountBalances: async (accountId: string): Promise<Balance[]> => {
|
||||
const response = await api.get<ApiResponse<Balance[]>>(
|
||||
const response = await api.get<Balance[]>(
|
||||
`/accounts/${accountId}/balances`,
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get transactions with optional filters
|
||||
@@ -97,7 +98,7 @@ export const apiClient = {
|
||||
summaryOnly?: boolean;
|
||||
minAmount?: number;
|
||||
maxAmount?: number;
|
||||
}): Promise<ApiResponse<Transaction[]>> => {
|
||||
}): Promise<PaginatedResponse<Transaction>> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params?.accountId) queryParams.append("account_id", params.accountId);
|
||||
@@ -117,7 +118,7 @@ export const apiClient = {
|
||||
queryParams.append("max_amount", params.maxAmount.toString());
|
||||
}
|
||||
|
||||
const response = await api.get<ApiResponse<Transaction[]>>(
|
||||
const response = await api.get<PaginatedResponse<Transaction>>(
|
||||
`/transactions?${queryParams.toString()}`,
|
||||
);
|
||||
return response.data;
|
||||
@@ -125,29 +126,27 @@ export const apiClient = {
|
||||
|
||||
// Get transaction by ID
|
||||
getTransaction: async (id: string): Promise<Transaction> => {
|
||||
const response = await api.get<ApiResponse<Transaction>>(
|
||||
`/transactions/${id}`,
|
||||
);
|
||||
return response.data.data;
|
||||
const response = await api.get<Transaction>(`/transactions/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get notification settings
|
||||
getNotificationSettings: async (): Promise<NotificationSettings> => {
|
||||
const response = await api.get<ApiResponse<NotificationSettings>>(
|
||||
const response = await api.get<NotificationSettings>(
|
||||
"/notifications/settings",
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update notification settings
|
||||
updateNotificationSettings: async (
|
||||
settings: NotificationSettings,
|
||||
): Promise<NotificationSettings> => {
|
||||
const response = await api.put<ApiResponse<NotificationSettings>>(
|
||||
const response = await api.put<NotificationSettings>(
|
||||
"/notifications/settings",
|
||||
settings,
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Test notification
|
||||
@@ -157,11 +156,11 @@ export const apiClient = {
|
||||
|
||||
// Get notification services
|
||||
getNotificationServices: async (): Promise<NotificationService[]> => {
|
||||
const response = await api.get<ApiResponse<NotificationServicesResponse>>(
|
||||
const response = await api.get<NotificationServicesResponse>(
|
||||
"/notifications/services",
|
||||
);
|
||||
// Convert object to array format
|
||||
const servicesData = response.data.data;
|
||||
const servicesData = response.data;
|
||||
return Object.values(servicesData);
|
||||
},
|
||||
|
||||
@@ -172,8 +171,8 @@ export const apiClient = {
|
||||
|
||||
// Health check
|
||||
getHealth: async (): Promise<HealthData> => {
|
||||
const response = await api.get<ApiResponse<HealthData>>("/health");
|
||||
return response.data.data;
|
||||
const response = await api.get<HealthData>("/health");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Analytics endpoints
|
||||
@@ -181,10 +180,10 @@ export const apiClient = {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (days) queryParams.append("days", days.toString());
|
||||
|
||||
const response = await api.get<ApiResponse<TransactionStats>>(
|
||||
const response = await api.get<TransactionStats>(
|
||||
`/transactions/stats?${queryParams.toString()}`,
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get all transactions for analytics (no pagination)
|
||||
@@ -194,10 +193,10 @@ export const apiClient = {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (days) queryParams.append("days", days.toString());
|
||||
|
||||
const response = await api.get<ApiResponse<AnalyticsTransaction[]>>(
|
||||
const response = await api.get<AnalyticsTransaction[]>(
|
||||
`/transactions/analytics?${queryParams.toString()}`,
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get monthly transaction statistics (pre-calculated)
|
||||
@@ -215,16 +214,14 @@ export const apiClient = {
|
||||
if (days) queryParams.append("days", days.toString());
|
||||
|
||||
const response = await api.get<
|
||||
ApiResponse<
|
||||
Array<{
|
||||
month: string;
|
||||
income: number;
|
||||
expenses: number;
|
||||
net: number;
|
||||
}>
|
||||
>
|
||||
Array<{
|
||||
month: string;
|
||||
income: number;
|
||||
expenses: number;
|
||||
net: number;
|
||||
}>
|
||||
>(`/transactions/monthly-stats?${queryParams.toString()}`);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get sync operations history
|
||||
@@ -232,24 +229,23 @@ export const apiClient = {
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
): Promise<SyncOperationsResponse> => {
|
||||
const response = await api.get<ApiResponse<SyncOperationsResponse>>(
|
||||
const response = await api.get<SyncOperationsResponse>(
|
||||
`/sync/operations?limit=${limit}&offset=${offset}`,
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Bank management endpoints
|
||||
getBankInstitutions: async (country: string): Promise<BankInstitution[]> => {
|
||||
const response = await api.get<ApiResponse<BankInstitution[]>>(
|
||||
const response = await api.get<BankInstitution[]>(
|
||||
`/banks/institutions?country=${country}`,
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getBankConnectionsStatus: async (): Promise<BankConnectionStatus[]> => {
|
||||
const response =
|
||||
await api.get<ApiResponse<BankConnectionStatus[]>>("/banks/status");
|
||||
return response.data.data;
|
||||
const response = await api.get<BankConnectionStatus[]>("/banks/status");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createBankConnection: async (
|
||||
@@ -260,14 +256,11 @@ export const apiClient = {
|
||||
const finalRedirectUrl =
|
||||
redirectUrl || `${window.location.origin}/bank-connected`;
|
||||
|
||||
const response = await api.post<ApiResponse<BankRequisition>>(
|
||||
"/banks/connect",
|
||||
{
|
||||
institution_id: institutionId,
|
||||
redirect_url: finalRedirectUrl,
|
||||
},
|
||||
);
|
||||
return response.data.data;
|
||||
const response = await api.post<BankRequisition>("/banks/connect", {
|
||||
institution_id: institutionId,
|
||||
redirect_url: finalRedirectUrl,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteBankConnection: async (requisitionId: string): Promise<void> => {
|
||||
@@ -275,39 +268,44 @@ export const apiClient = {
|
||||
},
|
||||
|
||||
getSupportedCountries: async (): Promise<Country[]> => {
|
||||
const response = await api.get<ApiResponse<Country[]>>("/banks/countries");
|
||||
return response.data.data;
|
||||
const response = await api.get<Country[]>("/banks/countries");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Backup endpoints
|
||||
getBackupSettings: async (): Promise<BackupSettings> => {
|
||||
const response =
|
||||
await api.get<ApiResponse<BackupSettings>>("/backup/settings");
|
||||
return response.data.data;
|
||||
const response = await api.get<BackupSettings>("/backup/settings");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateBackupSettings: async (
|
||||
settings: BackupSettings,
|
||||
): Promise<BackupSettings> => {
|
||||
const response = await api.put<ApiResponse<BackupSettings>>(
|
||||
const response = await api.put<BackupSettings>(
|
||||
"/backup/settings",
|
||||
settings,
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
|
||||
testBackupConnection: async (test: BackupTest): Promise<ApiResponse<{ connected?: boolean }>> => {
|
||||
const response = await api.post<ApiResponse<{ connected?: boolean }>>("/backup/test", test);
|
||||
testBackupConnection: async (test: BackupTest): Promise<{ connected?: boolean }> => {
|
||||
const response = await api.post<{ connected?: boolean }>(
|
||||
"/backup/test",
|
||||
test,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
listBackups: async (): Promise<BackupInfo[]> => {
|
||||
const response = await api.get<ApiResponse<BackupInfo[]>>("/backup/list");
|
||||
return response.data.data;
|
||||
const response = await api.get<BackupInfo[]>("/backup/list");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
performBackupOperation: async (operation: BackupOperation): Promise<ApiResponse<{ operation: string; completed: boolean }>> => {
|
||||
const response = await api.post<ApiResponse<{ operation: string; completed: boolean }>>("/backup/operation", operation);
|
||||
performBackupOperation: async (operation: BackupOperation): Promise<{ operation: string; completed: boolean }> => {
|
||||
const response = await api.post<{ operation: string; completed: boolean }>(
|
||||
"/backup/operation",
|
||||
operation,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -133,26 +133,14 @@ export interface Bank {
|
||||
logo_url?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
message?: string;
|
||||
success: boolean;
|
||||
pagination?: {
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
// Notification types
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Generic, List, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class APIResponse(BaseModel):
|
||||
"""Base API response model"""
|
||||
|
||||
success: bool = True
|
||||
message: Optional[str] = None
|
||||
data: Optional[Any] = None
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Error response model"""
|
||||
|
||||
success: bool = False
|
||||
message: str
|
||||
error_code: Optional[str] = None
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class PaginatedResponse(BaseModel):
|
||||
class PaginatedResponse(BaseModel, Generic[T]):
|
||||
"""Paginated response model"""
|
||||
|
||||
success: bool = True
|
||||
data: list
|
||||
pagination: Dict[str, Any]
|
||||
message: Optional[str] = None
|
||||
data: List[T]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
total_pages: int
|
||||
has_next: bool
|
||||
has_prev: bool
|
||||
|
||||
@@ -10,15 +10,14 @@ from leggen.api.models.accounts import (
|
||||
Transaction,
|
||||
TransactionSummary,
|
||||
)
|
||||
from leggen.api.models.common import APIResponse
|
||||
from leggen.services.database_service import DatabaseService
|
||||
|
||||
router = APIRouter()
|
||||
database_service = DatabaseService()
|
||||
|
||||
|
||||
@router.get("/accounts", response_model=APIResponse)
|
||||
async def get_all_accounts() -> APIResponse:
|
||||
@router.get("/accounts")
|
||||
async def get_all_accounts() -> List[AccountDetails]:
|
||||
"""Get all connected accounts from database"""
|
||||
try:
|
||||
accounts = []
|
||||
@@ -68,11 +67,7 @@ async def get_all_accounts() -> APIResponse:
|
||||
)
|
||||
continue
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=accounts,
|
||||
message=f"Retrieved {len(accounts)} accounts from database",
|
||||
)
|
||||
return accounts
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get accounts: {e}")
|
||||
@@ -81,8 +76,8 @@ async def get_all_accounts() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/accounts/{account_id}", response_model=APIResponse)
|
||||
async def get_account_details(account_id: str) -> APIResponse:
|
||||
@router.get("/accounts/{account_id}")
|
||||
async def get_account_details(account_id: str) -> AccountDetails:
|
||||
"""Get details for a specific account from database"""
|
||||
try:
|
||||
# Get account details from database
|
||||
@@ -122,11 +117,7 @@ async def get_account_details(account_id: str) -> APIResponse:
|
||||
balances=balances,
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=account,
|
||||
message=f"Account details retrieved from database for {account_id}",
|
||||
)
|
||||
return account
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -137,8 +128,8 @@ async def get_account_details(account_id: str) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/accounts/{account_id}/balances", response_model=APIResponse)
|
||||
async def get_account_balances(account_id: str) -> APIResponse:
|
||||
@router.get("/accounts/{account_id}/balances")
|
||||
async def get_account_balances(account_id: str) -> List[AccountBalance]:
|
||||
"""Get balances for a specific account from database"""
|
||||
try:
|
||||
# Get balances from database instead of GoCardless API
|
||||
@@ -155,11 +146,7 @@ async def get_account_balances(account_id: str) -> APIResponse:
|
||||
)
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=balances,
|
||||
message=f"Retrieved {len(balances)} balances for account {account_id}",
|
||||
)
|
||||
return balances
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -170,8 +157,8 @@ async def get_account_balances(account_id: str) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/balances", response_model=APIResponse)
|
||||
async def get_all_balances() -> APIResponse:
|
||||
@router.get("/balances")
|
||||
async def get_all_balances() -> List[dict]:
|
||||
"""Get all balances from all accounts in database"""
|
||||
try:
|
||||
# Get all accounts first to iterate through them
|
||||
@@ -207,11 +194,7 @@ async def get_all_balances() -> APIResponse:
|
||||
)
|
||||
continue
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=all_balances,
|
||||
message=f"Retrieved {len(all_balances)} balances from {len(db_accounts)} accounts",
|
||||
)
|
||||
return all_balances
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get all balances: {e}")
|
||||
@@ -220,7 +203,7 @@ async def get_all_balances() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/balances/history", response_model=APIResponse)
|
||||
@router.get("/balances/history")
|
||||
async def get_historical_balances(
|
||||
days: Optional[int] = Query(
|
||||
default=365, le=1095, ge=1, description="Number of days of history to retrieve"
|
||||
@@ -228,7 +211,7 @@ async def get_historical_balances(
|
||||
account_id: Optional[str] = Query(
|
||||
default=None, description="Filter by specific account ID"
|
||||
),
|
||||
) -> APIResponse:
|
||||
) -> List[dict]:
|
||||
"""Get historical balance progression calculated from transaction history"""
|
||||
try:
|
||||
# Get historical balances from database
|
||||
@@ -236,11 +219,7 @@ async def get_historical_balances(
|
||||
account_id=account_id, days=days or 365
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=historical_balances,
|
||||
message=f"Retrieved {len(historical_balances)} historical balance points over {days} days",
|
||||
)
|
||||
return historical_balances
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get historical balances: {e}")
|
||||
@@ -249,7 +228,7 @@ async def get_historical_balances(
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/accounts/{account_id}/transactions", response_model=APIResponse)
|
||||
@router.get("/accounts/{account_id}/transactions")
|
||||
async def get_account_transactions(
|
||||
account_id: str,
|
||||
limit: Optional[int] = Query(default=100, le=500),
|
||||
@@ -257,7 +236,7 @@ async def get_account_transactions(
|
||||
summary_only: bool = Query(
|
||||
default=False, description="Return transaction summaries only"
|
||||
),
|
||||
) -> APIResponse:
|
||||
) -> Union[List[TransactionSummary], List[Transaction]]:
|
||||
"""Get transactions for a specific account from database"""
|
||||
try:
|
||||
# Get transactions from database instead of GoCardless API
|
||||
@@ -308,12 +287,7 @@ async def get_account_transactions(
|
||||
for txn in db_transactions
|
||||
]
|
||||
|
||||
actual_offset = offset or 0
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=data,
|
||||
message=f"Retrieved {len(data)} transactions (showing {actual_offset + 1}-{actual_offset + len(data)} of {total_transactions})",
|
||||
)
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -324,10 +298,10 @@ async def get_account_transactions(
|
||||
) from e
|
||||
|
||||
|
||||
@router.put("/accounts/{account_id}", response_model=APIResponse)
|
||||
@router.put("/accounts/{account_id}")
|
||||
async def update_account_details(
|
||||
account_id: str, update_data: AccountUpdate
|
||||
) -> APIResponse:
|
||||
) -> dict:
|
||||
"""Update account details (currently only display_name)"""
|
||||
try:
|
||||
# Get current account details
|
||||
@@ -346,11 +320,7 @@ async def update_account_details(
|
||||
# Persist updated account details
|
||||
await database_service.persist_account_details(updated_account_data)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"id": account_id, "display_name": update_data.display_name},
|
||||
message=f"Account {account_id} display name updated successfully",
|
||||
)
|
||||
return {"id": account_id, "display_name": update_data.display_name}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -9,7 +9,6 @@ from leggen.api.models.backup import (
|
||||
BackupTest,
|
||||
S3Config,
|
||||
)
|
||||
from leggen.api.models.common import APIResponse
|
||||
from leggen.models.config import S3BackupConfig
|
||||
from leggen.services.backup_service import BackupService
|
||||
from leggen.utils.config import config
|
||||
@@ -18,8 +17,8 @@ from leggen.utils.paths import path_manager
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/backup/settings", response_model=APIResponse)
|
||||
async def get_backup_settings() -> APIResponse:
|
||||
@router.get("/backup/settings")
|
||||
async def get_backup_settings() -> BackupSettings:
|
||||
"""Get current backup settings."""
|
||||
try:
|
||||
backup_config = config.backup_config
|
||||
@@ -41,11 +40,7 @@ async def get_backup_settings() -> APIResponse:
|
||||
else None,
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=settings,
|
||||
message="Backup settings retrieved successfully",
|
||||
)
|
||||
return settings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get backup settings: {e}")
|
||||
@@ -54,8 +49,8 @@ async def get_backup_settings() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.put("/backup/settings", response_model=APIResponse)
|
||||
async def update_backup_settings(settings: BackupSettings) -> APIResponse:
|
||||
@router.put("/backup/settings")
|
||||
async def update_backup_settings(settings: BackupSettings) -> dict:
|
||||
"""Update backup settings."""
|
||||
try:
|
||||
# First test the connection if S3 config is provided
|
||||
@@ -99,11 +94,7 @@ async def update_backup_settings(settings: BackupSettings) -> APIResponse:
|
||||
if backup_config:
|
||||
config.update_section("backup", backup_config)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"updated": True},
|
||||
message="Backup settings updated successfully",
|
||||
)
|
||||
return {"updated": True}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -114,8 +105,8 @@ async def update_backup_settings(settings: BackupSettings) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/backup/test", response_model=APIResponse)
|
||||
async def test_backup_connection(test_request: BackupTest) -> APIResponse:
|
||||
@router.post("/backup/test")
|
||||
async def test_backup_connection(test_request: BackupTest) -> dict:
|
||||
"""Test backup connection."""
|
||||
try:
|
||||
if test_request.service != "s3":
|
||||
@@ -137,18 +128,13 @@ async def test_backup_connection(test_request: BackupTest) -> APIResponse:
|
||||
backup_service = BackupService()
|
||||
success = await backup_service.test_connection(s3_config)
|
||||
|
||||
if success:
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"connected": True},
|
||||
message="S3 connection test successful",
|
||||
)
|
||||
else:
|
||||
return APIResponse(
|
||||
success=False,
|
||||
message="S3 connection test failed",
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="S3 connection test failed"
|
||||
)
|
||||
|
||||
return {"connected": True}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -158,18 +144,14 @@ async def test_backup_connection(test_request: BackupTest) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/backup/list", response_model=APIResponse)
|
||||
async def list_backups() -> APIResponse:
|
||||
@router.get("/backup/list")
|
||||
async def list_backups() -> list:
|
||||
"""List available backups."""
|
||||
try:
|
||||
backup_config = config.backup_config.get("s3", {})
|
||||
|
||||
if not backup_config.get("bucket_name"):
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=[],
|
||||
message="No S3 backup configuration found",
|
||||
)
|
||||
return []
|
||||
|
||||
# Convert config to model
|
||||
s3_config = S3BackupConfig(**backup_config)
|
||||
@@ -177,11 +159,7 @@ async def list_backups() -> APIResponse:
|
||||
|
||||
backups = await backup_service.list_backups()
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=backups,
|
||||
message=f"Found {len(backups)} backups",
|
||||
)
|
||||
return backups
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list backups: {e}")
|
||||
@@ -190,8 +168,8 @@ async def list_backups() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/backup/operation", response_model=APIResponse)
|
||||
async def backup_operation(operation_request: BackupOperation) -> APIResponse:
|
||||
@router.post("/backup/operation")
|
||||
async def backup_operation(operation_request: BackupOperation) -> dict:
|
||||
"""Perform backup operation (backup or restore)."""
|
||||
try:
|
||||
backup_config = config.backup_config.get("s3", {})
|
||||
@@ -214,18 +192,13 @@ async def backup_operation(operation_request: BackupOperation) -> APIResponse:
|
||||
database_path = path_manager.get_database_path()
|
||||
success = await backup_service.backup_database(database_path)
|
||||
|
||||
if success:
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"operation": "backup", "completed": True},
|
||||
message="Database backup completed successfully",
|
||||
)
|
||||
else:
|
||||
return APIResponse(
|
||||
success=False,
|
||||
message="Database backup failed",
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Database backup failed"
|
||||
)
|
||||
|
||||
return {"operation": "backup", "completed": True}
|
||||
|
||||
elif operation_request.operation == "restore":
|
||||
if not operation_request.backup_key:
|
||||
raise HTTPException(
|
||||
@@ -239,17 +212,12 @@ async def backup_operation(operation_request: BackupOperation) -> APIResponse:
|
||||
operation_request.backup_key, database_path
|
||||
)
|
||||
|
||||
if success:
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"operation": "restore", "completed": True},
|
||||
message="Database restore completed successfully",
|
||||
)
|
||||
else:
|
||||
return APIResponse(
|
||||
success=False,
|
||||
message="Database restore failed",
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Database restore failed"
|
||||
)
|
||||
|
||||
return {"operation": "restore", "completed": True}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid operation. Use 'backup' or 'restore'"
|
||||
|
||||
@@ -8,7 +8,6 @@ from leggen.api.models.banks import (
|
||||
BankInstitution,
|
||||
BankRequisition,
|
||||
)
|
||||
from leggen.api.models.common import APIResponse
|
||||
from leggen.services.gocardless_service import GoCardlessService
|
||||
from leggen.utils.gocardless import REQUISITION_STATUS
|
||||
|
||||
@@ -16,10 +15,10 @@ router = APIRouter()
|
||||
gocardless_service = GoCardlessService()
|
||||
|
||||
|
||||
@router.get("/banks/institutions", response_model=APIResponse)
|
||||
@router.get("/banks/institutions")
|
||||
async def get_bank_institutions(
|
||||
country: str = Query(default="PT", description="Country code (e.g., PT, ES, FR)"),
|
||||
) -> APIResponse:
|
||||
) -> list[BankInstitution]:
|
||||
"""Get available bank institutions for a country"""
|
||||
try:
|
||||
institutions_response = await gocardless_service.get_institutions(country)
|
||||
@@ -41,11 +40,7 @@ async def get_bank_institutions(
|
||||
for inst in institutions_data
|
||||
]
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=institutions,
|
||||
message=f"Found {len(institutions)} institutions for {country}",
|
||||
)
|
||||
return institutions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get institutions for {country}: {e}")
|
||||
@@ -54,8 +49,8 @@ async def get_bank_institutions(
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/banks/connect", response_model=APIResponse)
|
||||
async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
|
||||
@router.post("/banks/connect")
|
||||
async def connect_to_bank(request: BankConnectionRequest) -> BankRequisition:
|
||||
"""Create a connection to a bank (requisition)"""
|
||||
try:
|
||||
redirect_url = request.redirect_url or "http://localhost:8000/"
|
||||
@@ -72,11 +67,7 @@ async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
|
||||
accounts=requisition_data.get("accounts", []),
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=requisition,
|
||||
message="Bank connection created. Please visit the link to authorize.",
|
||||
)
|
||||
return requisition
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to bank {request.institution_id}: {e}")
|
||||
@@ -85,8 +76,8 @@ async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/banks/status", response_model=APIResponse)
|
||||
async def get_bank_connections_status() -> APIResponse:
|
||||
@router.get("/banks/status")
|
||||
async def get_bank_connections_status() -> list[BankConnectionStatus]:
|
||||
"""Get status of all bank connections"""
|
||||
try:
|
||||
requisitions_data = await gocardless_service.get_requisitions()
|
||||
@@ -110,11 +101,7 @@ async def get_bank_connections_status() -> APIResponse:
|
||||
)
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=connections,
|
||||
message=f"Found {len(connections)} bank connections",
|
||||
)
|
||||
return connections
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get bank connection status: {e}")
|
||||
@@ -123,8 +110,8 @@ async def get_bank_connections_status() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.delete("/banks/connections/{requisition_id}", response_model=APIResponse)
|
||||
async def delete_bank_connection(requisition_id: str) -> APIResponse:
|
||||
@router.delete("/banks/connections/{requisition_id}")
|
||||
async def delete_bank_connection(requisition_id: str) -> dict:
|
||||
"""Delete a bank connection"""
|
||||
try:
|
||||
# Delete the requisition from GoCardless
|
||||
@@ -134,10 +121,7 @@ async def delete_bank_connection(requisition_id: str) -> APIResponse:
|
||||
# We should check if the operation was actually successful
|
||||
logger.info(f"GoCardless delete response for {requisition_id}: {result}")
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
message=f"Bank connection {requisition_id} deleted successfully",
|
||||
)
|
||||
return {"deleted": requisition_id}
|
||||
|
||||
except httpx.HTTPStatusError as http_err:
|
||||
logger.error(
|
||||
@@ -164,8 +148,8 @@ async def delete_bank_connection(requisition_id: str) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/banks/countries", response_model=APIResponse)
|
||||
async def get_supported_countries() -> APIResponse:
|
||||
@router.get("/banks/countries")
|
||||
async def get_supported_countries() -> list[dict]:
|
||||
"""Get list of supported countries"""
|
||||
countries = [
|
||||
{"code": "AT", "name": "Austria"},
|
||||
@@ -201,8 +185,4 @@ async def get_supported_countries() -> APIResponse:
|
||||
{"code": "GB", "name": "United Kingdom"},
|
||||
]
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=countries,
|
||||
message="Supported countries retrieved successfully",
|
||||
)
|
||||
return countries
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any, Dict
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from leggen.api.models.common import APIResponse
|
||||
from leggen.api.models.notifications import (
|
||||
DiscordConfig,
|
||||
NotificationFilters,
|
||||
@@ -18,8 +17,8 @@ router = APIRouter()
|
||||
notification_service = NotificationService()
|
||||
|
||||
|
||||
@router.get("/notifications/settings", response_model=APIResponse)
|
||||
async def get_notification_settings() -> APIResponse:
|
||||
@router.get("/notifications/settings")
|
||||
async def get_notification_settings() -> NotificationSettings:
|
||||
"""Get current notification settings"""
|
||||
try:
|
||||
notifications_config = config.notifications_config
|
||||
@@ -49,11 +48,7 @@ async def get_notification_settings() -> APIResponse:
|
||||
),
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=settings,
|
||||
message="Notification settings retrieved successfully",
|
||||
)
|
||||
return settings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get notification settings: {e}")
|
||||
@@ -62,8 +57,8 @@ async def get_notification_settings() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.put("/notifications/settings", response_model=APIResponse)
|
||||
async def update_notification_settings(settings: NotificationSettings) -> APIResponse:
|
||||
@router.put("/notifications/settings")
|
||||
async def update_notification_settings(settings: NotificationSettings) -> dict:
|
||||
"""Update notification settings"""
|
||||
try:
|
||||
# Update notifications config
|
||||
@@ -95,11 +90,7 @@ async def update_notification_settings(settings: NotificationSettings) -> APIRes
|
||||
if filters_config:
|
||||
config.update_section("filters", filters_config)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"updated": True},
|
||||
message="Notification settings updated successfully",
|
||||
)
|
||||
return {"updated": True}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update notification settings: {e}")
|
||||
@@ -108,26 +99,24 @@ async def update_notification_settings(settings: NotificationSettings) -> APIRes
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/notifications/test", response_model=APIResponse)
|
||||
async def test_notification(test_request: NotificationTest) -> APIResponse:
|
||||
@router.post("/notifications/test")
|
||||
async def test_notification(test_request: NotificationTest) -> dict:
|
||||
"""Send a test notification"""
|
||||
try:
|
||||
success = await notification_service.send_test_notification(
|
||||
test_request.service, test_request.message
|
||||
)
|
||||
|
||||
if success:
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"sent": True},
|
||||
message=f"Test notification sent to {test_request.service} successfully",
|
||||
)
|
||||
else:
|
||||
return APIResponse(
|
||||
success=False,
|
||||
message=f"Failed to send test notification to {test_request.service}",
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Failed to send test notification to {test_request.service}",
|
||||
)
|
||||
|
||||
return {"sent": True}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send test notification: {e}")
|
||||
raise HTTPException(
|
||||
@@ -135,8 +124,8 @@ async def test_notification(test_request: NotificationTest) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/notifications/services", response_model=APIResponse)
|
||||
async def get_notification_services() -> APIResponse:
|
||||
@router.get("/notifications/services")
|
||||
async def get_notification_services() -> dict:
|
||||
"""Get available notification services and their status"""
|
||||
try:
|
||||
notifications_config = config.notifications_config
|
||||
@@ -164,11 +153,7 @@ async def get_notification_services() -> APIResponse:
|
||||
},
|
||||
}
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=services,
|
||||
message="Notification services status retrieved successfully",
|
||||
)
|
||||
return services
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get notification services: {e}")
|
||||
@@ -177,8 +162,8 @@ async def get_notification_services() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.delete("/notifications/settings/{service}", response_model=APIResponse)
|
||||
async def delete_notification_service(service: str) -> APIResponse:
|
||||
@router.delete("/notifications/settings/{service}")
|
||||
async def delete_notification_service(service: str) -> dict:
|
||||
"""Delete/disable a notification service"""
|
||||
try:
|
||||
if service not in ["discord", "telegram"]:
|
||||
@@ -191,12 +176,10 @@ async def delete_notification_service(service: str) -> APIResponse:
|
||||
del notifications_config[service]
|
||||
config.update_section("notifications", notifications_config)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"deleted": service},
|
||||
message=f"{service.capitalize()} notification service deleted successfully",
|
||||
)
|
||||
return {"deleted": service}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete notification service {service}: {e}")
|
||||
raise HTTPException(
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Optional
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from leggen.api.models.common import APIResponse
|
||||
from leggen.api.models.sync import SchedulerConfig, SyncRequest
|
||||
from leggen.background.scheduler import scheduler
|
||||
from leggen.services.sync_service import SyncService
|
||||
@@ -13,8 +12,8 @@ router = APIRouter()
|
||||
sync_service = SyncService()
|
||||
|
||||
|
||||
@router.get("/sync/status", response_model=APIResponse)
|
||||
async def get_sync_status() -> APIResponse:
|
||||
@router.get("/sync/status")
|
||||
async def get_sync_status() -> dict:
|
||||
"""Get current sync status"""
|
||||
try:
|
||||
status = await sync_service.get_sync_status()
|
||||
@@ -24,9 +23,7 @@ async def get_sync_status() -> APIResponse:
|
||||
if next_sync_time:
|
||||
status.next_sync = next_sync_time
|
||||
|
||||
return APIResponse(
|
||||
success=True, data=status, message="Sync status retrieved successfully"
|
||||
)
|
||||
return status
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get sync status: {e}")
|
||||
@@ -35,18 +32,18 @@ async def get_sync_status() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/sync", response_model=APIResponse)
|
||||
@router.post("/sync")
|
||||
async def trigger_sync(
|
||||
background_tasks: BackgroundTasks, sync_request: Optional[SyncRequest] = None
|
||||
) -> APIResponse:
|
||||
) -> dict:
|
||||
"""Trigger a manual sync operation"""
|
||||
try:
|
||||
# Check if sync is already running
|
||||
status = await sync_service.get_sync_status()
|
||||
if status.is_running and not (sync_request and sync_request.force):
|
||||
return APIResponse(
|
||||
success=False,
|
||||
message="Sync is already running. Use 'force: true' to override.",
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Sync is already running. Use 'force: true' to override.",
|
||||
)
|
||||
|
||||
# Determine what to sync
|
||||
@@ -58,9 +55,6 @@ async def trigger_sync(
|
||||
sync_request.force if sync_request else False,
|
||||
"api", # trigger_type
|
||||
)
|
||||
message = (
|
||||
f"Started sync for {len(sync_request.account_ids)} specific accounts"
|
||||
)
|
||||
else:
|
||||
# Sync all accounts in background
|
||||
background_tasks.add_task(
|
||||
@@ -68,17 +62,14 @@ async def trigger_sync(
|
||||
sync_request.force if sync_request else False,
|
||||
"api", # trigger_type
|
||||
)
|
||||
message = "Started sync for all accounts"
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={
|
||||
"sync_started": True,
|
||||
"force": sync_request.force if sync_request else False,
|
||||
},
|
||||
message=message,
|
||||
)
|
||||
return {
|
||||
"sync_started": True,
|
||||
"force": sync_request.force if sync_request else False,
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to trigger sync: {e}")
|
||||
raise HTTPException(
|
||||
@@ -86,8 +77,8 @@ async def trigger_sync(
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/sync/now", response_model=APIResponse)
|
||||
async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
|
||||
@router.post("/sync/now")
|
||||
async def sync_now(sync_request: Optional[SyncRequest] = None) -> dict:
|
||||
"""Run sync synchronously and return results (slower, for testing)"""
|
||||
try:
|
||||
if sync_request and sync_request.account_ids:
|
||||
@@ -99,13 +90,7 @@ async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
|
||||
sync_request.force if sync_request else False, "api"
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=result.success,
|
||||
data=result,
|
||||
message="Sync completed"
|
||||
if result.success
|
||||
else f"Sync failed with {len(result.errors)} errors",
|
||||
)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run sync: {e}")
|
||||
@@ -114,8 +99,8 @@ async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/sync/scheduler", response_model=APIResponse)
|
||||
async def get_scheduler_config() -> APIResponse:
|
||||
@router.get("/sync/scheduler")
|
||||
async def get_scheduler_config() -> dict:
|
||||
"""Get current scheduler configuration"""
|
||||
try:
|
||||
scheduler_config = config.scheduler_config
|
||||
@@ -131,11 +116,7 @@ async def get_scheduler_config() -> APIResponse:
|
||||
else False,
|
||||
}
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=response_data,
|
||||
message="Scheduler configuration retrieved successfully",
|
||||
)
|
||||
return response_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get scheduler config: {e}")
|
||||
@@ -144,8 +125,8 @@ async def get_scheduler_config() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.put("/sync/scheduler", response_model=APIResponse)
|
||||
async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIResponse:
|
||||
@router.put("/sync/scheduler")
|
||||
async def update_scheduler_config(scheduler_config: SchedulerConfig) -> dict:
|
||||
"""Update scheduler configuration"""
|
||||
try:
|
||||
# Validate cron expression if provided
|
||||
@@ -168,12 +149,10 @@ async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIRespo
|
||||
# Reschedule the job
|
||||
scheduler.reschedule_sync(schedule_data)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=schedule_data,
|
||||
message="Scheduler configuration updated successfully",
|
||||
)
|
||||
return schedule_data
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update scheduler config: {e}")
|
||||
raise HTTPException(
|
||||
@@ -181,15 +160,15 @@ async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIRespo
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/sync/scheduler/start", response_model=APIResponse)
|
||||
async def start_scheduler() -> APIResponse:
|
||||
@router.post("/sync/scheduler/start")
|
||||
async def start_scheduler() -> dict:
|
||||
"""Start the background scheduler"""
|
||||
try:
|
||||
if not scheduler.scheduler.running:
|
||||
scheduler.start()
|
||||
return APIResponse(success=True, message="Scheduler started successfully")
|
||||
return {"started": True}
|
||||
else:
|
||||
return APIResponse(success=True, message="Scheduler is already running")
|
||||
return {"started": False, "message": "Scheduler is already running"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start scheduler: {e}")
|
||||
@@ -198,15 +177,15 @@ async def start_scheduler() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/sync/scheduler/stop", response_model=APIResponse)
|
||||
async def stop_scheduler() -> APIResponse:
|
||||
@router.post("/sync/scheduler/stop")
|
||||
async def stop_scheduler() -> dict:
|
||||
"""Stop the background scheduler"""
|
||||
try:
|
||||
if scheduler.scheduler.running:
|
||||
scheduler.shutdown()
|
||||
return APIResponse(success=True, message="Scheduler stopped successfully")
|
||||
return {"stopped": True}
|
||||
else:
|
||||
return APIResponse(success=True, message="Scheduler is already stopped")
|
||||
return {"stopped": False, "message": "Scheduler is already stopped"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stop scheduler: {e}")
|
||||
@@ -215,19 +194,15 @@ async def stop_scheduler() -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/sync/operations", response_model=APIResponse)
|
||||
async def get_sync_operations(limit: int = 50, offset: int = 0) -> APIResponse:
|
||||
@router.get("/sync/operations")
|
||||
async def get_sync_operations(limit: int = 50, offset: int = 0) -> dict:
|
||||
"""Get sync operations history"""
|
||||
try:
|
||||
operations = await sync_service.database.get_sync_operations(
|
||||
limit=limit, offset=offset
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"operations": operations, "count": len(operations)},
|
||||
message="Sync operations retrieved successfully",
|
||||
)
|
||||
return {"operations": operations, "count": len(operations)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get sync operations: {e}")
|
||||
|
||||
@@ -5,14 +5,14 @@ from fastapi import APIRouter, HTTPException, Query
|
||||
from loguru import logger
|
||||
|
||||
from leggen.api.models.accounts import Transaction, TransactionSummary
|
||||
from leggen.api.models.common import APIResponse, PaginatedResponse
|
||||
from leggen.api.models.common import PaginatedResponse
|
||||
from leggen.services.database_service import DatabaseService
|
||||
|
||||
router = APIRouter()
|
||||
database_service = DatabaseService()
|
||||
|
||||
|
||||
@router.get("/transactions", response_model=PaginatedResponse)
|
||||
@router.get("/transactions")
|
||||
async def get_all_transactions(
|
||||
page: int = Query(default=1, ge=1, description="Page number (1-based)"),
|
||||
per_page: int = Query(default=50, le=500, description="Items per page"),
|
||||
@@ -35,7 +35,7 @@ async def get_all_transactions(
|
||||
default=None, description="Search in transaction descriptions"
|
||||
),
|
||||
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
|
||||
) -> PaginatedResponse:
|
||||
) -> PaginatedResponse[Union[TransactionSummary, Transaction]]:
|
||||
"""Get all transactions from database with filtering options"""
|
||||
try:
|
||||
# Calculate offset from page and per_page
|
||||
@@ -103,16 +103,13 @@ async def get_all_transactions(
|
||||
total_pages = (total_transactions + per_page - 1) // per_page
|
||||
|
||||
return PaginatedResponse(
|
||||
success=True,
|
||||
data=data,
|
||||
pagination={
|
||||
"total": total_transactions,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"total_pages": total_pages,
|
||||
"has_next": page < total_pages,
|
||||
"has_prev": page > 1,
|
||||
},
|
||||
total=total_transactions,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
total_pages=total_pages,
|
||||
has_next=page < total_pages,
|
||||
has_prev=page > 1,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -122,11 +119,11 @@ async def get_all_transactions(
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/transactions/stats", response_model=APIResponse)
|
||||
@router.get("/transactions/stats")
|
||||
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"),
|
||||
) -> APIResponse:
|
||||
) -> dict:
|
||||
"""Get transaction statistics for the last N days from database"""
|
||||
try:
|
||||
# Date range for stats
|
||||
@@ -192,11 +189,7 @@ async def get_transaction_stats(
|
||||
"accounts_included": unique_accounts,
|
||||
}
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=stats,
|
||||
message=f"Transaction statistics for last {days} days",
|
||||
)
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get transaction stats from database: {e}")
|
||||
@@ -205,11 +198,11 @@ async def get_transaction_stats(
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/transactions/analytics", response_model=APIResponse)
|
||||
@router.get("/transactions/analytics")
|
||||
async def get_transactions_for_analytics(
|
||||
days: int = Query(default=365, description="Number of days to include"),
|
||||
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
|
||||
) -> APIResponse:
|
||||
) -> List[dict]:
|
||||
"""Get all transactions for analytics (no pagination) for the last N days"""
|
||||
try:
|
||||
# Date range for analytics
|
||||
@@ -242,11 +235,7 @@ async def get_transactions_for_analytics(
|
||||
for txn in transactions
|
||||
]
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=transaction_summaries,
|
||||
message=f"Retrieved {len(transaction_summaries)} transactions for analytics",
|
||||
)
|
||||
return transaction_summaries
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get transactions for analytics: {e}")
|
||||
@@ -255,11 +244,11 @@ async def get_transactions_for_analytics(
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/transactions/monthly-stats", response_model=APIResponse)
|
||||
@router.get("/transactions/monthly-stats")
|
||||
async def get_monthly_transaction_stats(
|
||||
days: int = Query(default=365, description="Number of days to include"),
|
||||
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
|
||||
) -> APIResponse:
|
||||
) -> List[dict]:
|
||||
"""Get monthly transaction statistics aggregated by the database"""
|
||||
try:
|
||||
# Date range for monthly stats
|
||||
@@ -277,11 +266,7 @@ async def get_monthly_transaction_stats(
|
||||
date_to=date_to,
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=monthly_stats,
|
||||
message=f"Retrieved monthly stats for last {days} days",
|
||||
)
|
||||
return monthly_stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get monthly transaction stats: {e}")
|
||||
|
||||
@@ -89,8 +89,6 @@ def create_app() -> FastAPI:
|
||||
async def health():
|
||||
"""Health check endpoint for API connectivity"""
|
||||
try:
|
||||
from leggen.api.models.common import APIResponse
|
||||
|
||||
config_loaded = config._config is not None
|
||||
|
||||
# Get version dynamically
|
||||
@@ -99,25 +97,17 @@ def create_app() -> FastAPI:
|
||||
except metadata.PackageNotFoundError:
|
||||
version = "dev"
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={
|
||||
"status": "healthy",
|
||||
"config_loaded": config_loaded,
|
||||
"version": version,
|
||||
"message": "API is running and responsive",
|
||||
},
|
||||
message="Health check successful",
|
||||
)
|
||||
return {
|
||||
"status": "healthy",
|
||||
"config_loaded": config_loaded,
|
||||
"version": version,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
from leggen.api.models.common import APIResponse
|
||||
|
||||
return APIResponse(
|
||||
success=False,
|
||||
data={"status": "unhealthy", "error": str(e)},
|
||||
message="Health check failed",
|
||||
)
|
||||
return {
|
||||
"status": "unhealthy",
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Pytest configuration and shared fixtures."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
@@ -8,9 +10,45 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from leggen.commands.server import create_app
|
||||
from leggen.utils.config import Config
|
||||
|
||||
# Create test config before any imports that might load it
|
||||
_test_config_dir = tempfile.mkdtemp(prefix="leggen_test_")
|
||||
_test_config_path = Path(_test_config_dir) / "config.toml"
|
||||
|
||||
# Create minimal test config
|
||||
_config_data = {
|
||||
"gocardless": {
|
||||
"key": "test-key",
|
||||
"secret": "test-secret",
|
||||
"url": "https://bankaccountdata.gocardless.com/api/v2",
|
||||
},
|
||||
"database": {"sqlite": True},
|
||||
"scheduler": {"sync": {"enabled": True, "hour": 3, "minute": 0}},
|
||||
}
|
||||
|
||||
import tomli_w
|
||||
with open(_test_config_path, "wb") as f:
|
||||
tomli_w.dump(_config_data, f)
|
||||
|
||||
# Set environment variables to point to test config BEFORE importing the app
|
||||
os.environ["LEGGEN_CONFIG_FILE"] = str(_test_config_path)
|
||||
|
||||
from leggen.commands.server import create_app
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Pytest hook called before test collection."""
|
||||
# Ensure test config is set
|
||||
os.environ["LEGGEN_CONFIG_FILE"] = str(_test_config_path)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
"""Pytest hook called after all tests."""
|
||||
# Cleanup test config directory
|
||||
if Path(_test_config_dir).exists():
|
||||
shutil.rmtree(_test_config_dir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_config_dir():
|
||||
@@ -73,9 +111,12 @@ def mock_auth_token(temp_config_dir):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fastapi_app():
|
||||
def fastapi_app(mock_db_path):
|
||||
"""Create FastAPI test application."""
|
||||
return create_app()
|
||||
# Patch the database path for the app
|
||||
with patch("leggen.utils.paths.path_manager.get_database_path", return_value=mock_db_path):
|
||||
app = create_app()
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -66,8 +66,7 @@ class TestAnalyticsFix:
|
||||
)
|
||||
|
||||
# Verify that the response contains stats for all 600 transactions
|
||||
assert data["success"] is True
|
||||
stats = data["data"]
|
||||
stats = data
|
||||
assert stats["total_transactions"] == 600, (
|
||||
"Should process all 600 transactions, not just 100"
|
||||
)
|
||||
@@ -132,8 +131,7 @@ class TestAnalyticsFix:
|
||||
)
|
||||
|
||||
# Verify that all 600 transactions are returned
|
||||
assert data["success"] is True
|
||||
transactions_data = data["data"]
|
||||
transactions_data = data
|
||||
assert len(transactions_data) == 600, (
|
||||
"Analytics endpoint should return all 600 transactions"
|
||||
)
|
||||
|
||||
@@ -60,9 +60,8 @@ class TestAccountsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 1
|
||||
account = data["data"][0]
|
||||
assert len(data) == 1
|
||||
account = data[0]
|
||||
assert account["id"] == "test-account-123"
|
||||
assert account["institution_id"] == "REVOLUT_REVOLT21"
|
||||
assert len(account["balances"]) == 1
|
||||
@@ -117,11 +116,9 @@ class TestAccountsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
account = data["data"]
|
||||
assert account["id"] == "test-account-123"
|
||||
assert account["iban"] == "LT313250081177977789"
|
||||
assert len(account["balances"]) == 1
|
||||
assert data["id"] == "test-account-123"
|
||||
assert data["iban"] == "LT313250081177977789"
|
||||
assert len(data["balances"]) == 1
|
||||
|
||||
def test_get_account_balances_success(
|
||||
self, api_client, mock_config, mock_auth_token, mock_db_path
|
||||
@@ -163,11 +160,10 @@ class TestAccountsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 2
|
||||
assert data["data"][0]["amount"] == 1000.00
|
||||
assert data["data"][0]["currency"] == "EUR"
|
||||
assert data["data"][0]["balance_type"] == "interimAvailable"
|
||||
assert len(data) == 2
|
||||
assert data[0]["amount"] == 1000.00
|
||||
assert data[0]["currency"] == "EUR"
|
||||
assert data[0]["balance_type"] == "interimAvailable"
|
||||
|
||||
def test_get_account_transactions_success(
|
||||
self,
|
||||
@@ -212,10 +208,9 @@ class TestAccountsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 1
|
||||
assert len(data) == 1
|
||||
|
||||
transaction = data["data"][0]
|
||||
transaction = data[0]
|
||||
assert transaction["internal_transaction_id"] == "txn-123"
|
||||
assert transaction["amount"] == -10.50
|
||||
assert transaction["currency"] == "EUR"
|
||||
@@ -264,10 +259,9 @@ class TestAccountsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 1
|
||||
assert len(data) == 1
|
||||
|
||||
transaction = data["data"][0]
|
||||
transaction = data[0]
|
||||
assert transaction["internal_transaction_id"] == "txn-123"
|
||||
assert transaction["institution_id"] == "REVOLUT_REVOLT21"
|
||||
assert transaction["iban"] == "LT313250081177977789"
|
||||
@@ -321,9 +315,8 @@ class TestAccountsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["id"] == "test-account-123"
|
||||
assert data["data"]["display_name"] == "My Custom Account Name"
|
||||
assert data["id"] == "test-account-123"
|
||||
assert data["display_name"] == "My Custom Account Name"
|
||||
|
||||
def test_update_account_not_found(
|
||||
self, api_client, mock_config, mock_auth_token, mock_db_path
|
||||
|
||||
@@ -19,8 +19,7 @@ class TestBackupAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["s3"] is None
|
||||
assert data["s3"] is None
|
||||
|
||||
def test_get_backup_settings_with_s3_config(self, api_client, mock_config):
|
||||
"""Test getting backup settings with S3 configuration."""
|
||||
@@ -42,10 +41,9 @@ class TestBackupAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["s3"] is not None
|
||||
assert data["s3"] is not None
|
||||
|
||||
s3_config = data["data"]["s3"]
|
||||
s3_config = data["s3"]
|
||||
assert s3_config["access_key_id"] == "***" # Masked
|
||||
assert s3_config["secret_access_key"] == "***" # Masked
|
||||
assert s3_config["bucket_name"] == "test-bucket"
|
||||
@@ -77,8 +75,7 @@ class TestBackupAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["updated"] is True
|
||||
assert data["updated"] is True
|
||||
|
||||
# Verify connection test was called
|
||||
mock_test_connection.assert_called_once()
|
||||
@@ -132,8 +129,7 @@ class TestBackupAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["connected"] is True
|
||||
assert data["connected"] is True
|
||||
|
||||
# Verify connection test was called
|
||||
mock_test_connection.assert_called_once()
|
||||
@@ -158,9 +154,9 @@ class TestBackupAPI:
|
||||
|
||||
response = api_client.post("/api/v1/backup/test", json=request_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert "S3 connection test failed" in data["detail"]
|
||||
|
||||
def test_test_backup_connection_invalid_service(self, api_client):
|
||||
"""Test backup connection test with invalid service."""
|
||||
@@ -214,10 +210,9 @@ class TestBackupAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 2
|
||||
assert len(data) == 2
|
||||
assert (
|
||||
data["data"][0]["key"]
|
||||
data[0]["key"]
|
||||
== "leggen_backups/database_backup_20250101_120000.db"
|
||||
)
|
||||
|
||||
@@ -230,8 +225,7 @@ class TestBackupAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"] == []
|
||||
assert data == []
|
||||
|
||||
@patch("leggen.services.backup_service.BackupService.backup_database")
|
||||
@patch("leggen.utils.paths.path_manager.get_database_path")
|
||||
@@ -261,9 +255,8 @@ class TestBackupAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["operation"] == "backup"
|
||||
assert data["data"]["completed"] is True
|
||||
assert data["operation"] == "backup"
|
||||
assert data["completed"] is True
|
||||
|
||||
# Verify backup was called
|
||||
mock_backup_db.assert_called_once()
|
||||
|
||||
@@ -33,10 +33,9 @@ class TestBanksAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 2
|
||||
assert data["data"][0]["id"] == "REVOLUT_REVOLT21"
|
||||
assert data["data"][1]["id"] == "BANCOBPI_BBPIPTPL"
|
||||
assert len(data) == 2
|
||||
assert data[0]["id"] == "REVOLUT_REVOLT21"
|
||||
assert data[1]["id"] == "BANCOBPI_BBPIPTPL"
|
||||
|
||||
@respx.mock
|
||||
def test_get_institutions_invalid_country(self, api_client, mock_config):
|
||||
@@ -92,9 +91,8 @@ class TestBanksAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["id"] == "req-123"
|
||||
assert data["data"]["institution_id"] == "REVOLUT_REVOLT21"
|
||||
assert data["id"] == "req-123"
|
||||
assert data["institution_id"] == "REVOLUT_REVOLT21"
|
||||
|
||||
@respx.mock
|
||||
def test_get_bank_status_success(self, api_client, mock_config, mock_auth_token):
|
||||
@@ -128,10 +126,9 @@ class TestBanksAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 1
|
||||
assert data["data"][0]["bank_id"] == "REVOLUT_REVOLT21"
|
||||
assert data["data"][0]["status_display"] == "LINKED"
|
||||
assert len(data) == 1
|
||||
assert data[0]["bank_id"] == "REVOLUT_REVOLT21"
|
||||
assert data[0]["status_display"] == "LINKED"
|
||||
|
||||
def test_get_supported_countries(self, api_client):
|
||||
"""Test supported countries endpoint."""
|
||||
@@ -139,11 +136,10 @@ class TestBanksAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) > 0
|
||||
assert len(data) > 0
|
||||
|
||||
# Check some expected countries
|
||||
country_codes = [country["code"] for country in data["data"]]
|
||||
country_codes = [country["code"] for country in data]
|
||||
assert "PT" in country_codes
|
||||
assert "GB" in country_codes
|
||||
assert "DE" in country_codes
|
||||
|
||||
@@ -58,7 +58,6 @@ class TestTransactionsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 2
|
||||
|
||||
# Check first transaction summary
|
||||
@@ -105,7 +104,6 @@ class TestTransactionsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
transaction = data["data"][0]
|
||||
@@ -160,7 +158,6 @@ class TestTransactionsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
# Verify the database service was called with correct filters
|
||||
mock_get_transactions.assert_called_once_with(
|
||||
@@ -193,11 +190,10 @@ class TestTransactionsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 0
|
||||
assert data["pagination"]["total"] == 0
|
||||
assert data["pagination"]["page"] == 1
|
||||
assert data["pagination"]["total_pages"] == 0
|
||||
assert data["total"] == 0
|
||||
assert data["page"] == 1
|
||||
assert data["total_pages"] == 0
|
||||
|
||||
def test_get_transactions_database_error(
|
||||
self, api_client, mock_config, mock_auth_token
|
||||
@@ -254,21 +250,19 @@ class TestTransactionsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
stats = data["data"]
|
||||
assert stats["period_days"] == 30
|
||||
assert stats["total_transactions"] == 3
|
||||
assert stats["booked_transactions"] == 2
|
||||
assert stats["pending_transactions"] == 1
|
||||
assert stats["total_income"] == 100.00
|
||||
assert stats["total_expenses"] == 35.80 # abs(-10.50) + abs(-25.30)
|
||||
assert stats["net_change"] == 64.20 # 100.00 - 35.80
|
||||
assert stats["accounts_included"] == 2 # Two unique account IDs
|
||||
assert data["period_days"] == 30
|
||||
assert data["total_transactions"] == 3
|
||||
assert data["booked_transactions"] == 2
|
||||
assert data["pending_transactions"] == 1
|
||||
assert data["total_income"] == 100.00
|
||||
assert data["total_expenses"] == 35.80 # abs(-10.50) + abs(-25.30)
|
||||
assert data["net_change"] == 64.20 # 100.00 - 35.80
|
||||
assert data["accounts_included"] == 2 # Two unique account IDs
|
||||
|
||||
# Average transaction: ((-10.50) + 100.00 + (-25.30)) / 3 = 64.20 / 3 = 21.4
|
||||
expected_avg = round(64.20 / 3, 2)
|
||||
assert stats["average_transaction"] == expected_avg
|
||||
assert data["average_transaction"] == expected_avg
|
||||
|
||||
def test_get_transaction_stats_with_account_filter(
|
||||
self, api_client, mock_config, mock_auth_token
|
||||
@@ -317,15 +311,13 @@ class TestTransactionsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
stats = data["data"]
|
||||
assert stats["total_transactions"] == 0
|
||||
assert stats["total_income"] == 0.0
|
||||
assert stats["total_expenses"] == 0.0
|
||||
assert stats["net_change"] == 0.0
|
||||
assert stats["average_transaction"] == 0 # Division by zero handled
|
||||
assert stats["accounts_included"] == 0
|
||||
assert data["total_transactions"] == 0
|
||||
assert data["total_income"] == 0.0
|
||||
assert data["total_expenses"] == 0.0
|
||||
assert data["net_change"] == 0.0
|
||||
assert data["average_transaction"] == 0 # Division by zero handled
|
||||
assert data["accounts_included"] == 0
|
||||
|
||||
def test_get_transaction_stats_database_error(
|
||||
self, api_client, mock_config, mock_auth_token
|
||||
@@ -368,7 +360,7 @@ class TestTransactionsAPI:
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["period_days"] == 7
|
||||
assert data["period_days"] == 7
|
||||
|
||||
# Verify the date range was calculated correctly for 7 days
|
||||
mock_get_transactions.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user