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