/** * Account Service - API calls for account management */ import api from './api'; import type { Account, AccountFormInput, TransferFormInput, ApiResponse } from '../types'; /** * Map account data from API (snake_case) to frontend (camelCase) */ function mapAccountFromApi(data: Record): Account { return { id: data.id as number, name: data.name as string, type: data.type as Account['type'], balance: data.balance as number, currency: data.currency as Account['currency'], icon: data.icon as string, isCredit: data.is_credit as boolean, billingDate: data.billing_date as number | undefined, paymentDate: data.payment_date as number | undefined, createdAt: data.created_at as string, updatedAt: data.updated_at as string, sortOrder: data.sort_order as number, warningThreshold: data.warning_threshold as number | undefined, lastSyncTime: data.last_sync_time as string | undefined, accountCode: data.account_code as string | undefined, accountType: data.account_type as 'asset' | 'liability' | undefined, tags: (data.tags as { id: number; name: string; color: string; created_at: string }[] | undefined)?.map(t => ({ id: t.id, name: t.name, color: t.color, createdAt: t.created_at, })), }; } /** * Get all accounts */ export async function getAccounts(): Promise { const response = await api.get[]>>('/accounts'); const rawData = response.data || []; return rawData.map(mapAccountFromApi); } /** * Get a single account by ID */ export async function getAccount(id: number): Promise { const response = await api.get>>(`/accounts/${id}`); if (!response.data) { throw new Error('Account not found'); } return mapAccountFromApi(response.data); } /** * Create a new account */ export async function createAccount(data: AccountFormInput): Promise { // Convert camelCase to snake_case for backend const payload = { name: data.name, type: data.type, balance: data.balance, currency: data.currency, icon: data.icon, billing_date: data.billingDate, payment_date: data.paymentDate, warning_threshold: data.warningThreshold, account_code: data.accountCode, tag_ids: data.tagIds, }; const response = await api.post>('/accounts', payload); if (!response.data) { throw new Error(response.error || 'Failed to create account'); } return response.data; } /** * Update an existing account */ export async function updateAccount(id: number, data: Partial): Promise { // Convert camelCase to snake_case for backend const payload: Record = {}; if (data.name !== undefined) payload.name = data.name; if (data.type !== undefined) payload.type = data.type; if (data.balance !== undefined) payload.balance = data.balance; if (data.currency !== undefined) payload.currency = data.currency; if (data.icon !== undefined) payload.icon = data.icon; if (data.billingDate !== undefined) payload.billing_date = data.billingDate; if (data.paymentDate !== undefined) payload.payment_date = data.paymentDate; if (data.sortOrder !== undefined) payload.sort_order = data.sortOrder; if (data.warningThreshold !== undefined) payload.warning_threshold = data.warningThreshold; if (data.accountCode !== undefined) payload.account_code = data.accountCode; if (data.accountType !== undefined) payload.account_type = data.accountType; if (data.tagIds !== undefined) payload.tag_ids = data.tagIds; const response = await api.put>(`/accounts/${id}`, payload); if (!response.data) { throw new Error(response.error || 'Failed to update account'); } return response.data; } /** * Delete an account */ export async function deleteAccount(id: number): Promise { await api.delete>(`/accounts/${id}`); } /** * Transfer between accounts */ export async function transferBetweenAccounts(data: TransferFormInput): Promise { // Convert camelCase to snake_case for backend const payload = { from_account_id: data.fromAccountId, to_account_id: data.toAccountId, amount: data.amount, note: data.note, }; const response = await api.post>('/accounts/transfer', payload); if (!response.success && response.error) { throw new Error(response.error); } } /** * Get accounts grouped by type */ export function groupAccountsByType(accounts: Account[] | undefined): Record { if (!accounts || !Array.isArray(accounts)) { return {}; } return accounts.reduce( (groups, account) => { const type = account.type; if (!groups[type]) { groups[type] = []; } groups[type].push(account); return groups; }, {} as Record ); } /** * Calculate total balance for accounts */ export function calculateTotalBalance(accounts: Account[] | undefined): number { if (!accounts || !Array.isArray(accounts)) { return 0; } return accounts.reduce((total, account) => total + account.balance, 0); } /** * Calculate total assets (positive balances) */ export function calculateTotalAssets(accounts: Account[] | undefined): number { if (!accounts || !Array.isArray(accounts)) { return 0; } return accounts .filter((account) => account.balance > 0) .reduce((total, account) => total + account.balance, 0); } /** * Calculate total liabilities (negative balances) */ export function calculateTotalLiabilities(accounts: Account[] | undefined): number { if (!accounts || !Array.isArray(accounts)) { return 0; } return accounts .filter((account) => account.balance < 0) .reduce((total, account) => total + Math.abs(account.balance), 0); } /** * Reorder accounts by updating their sort_order * Requirements: 1.3, 1.4 */ export async function reorderAccounts(accountIds: number[]): Promise { // Update each account's sort order const updates = accountIds.map((id, index) => updateAccount(id, { sortOrder: index }) ); await Promise.all(updates); } /** * Calculate total assets (only asset-type accounts) * Requirements: 1.2 */ export function calculateAssetTypeTotal(accounts: Account[] | undefined): number { if (!accounts || !Array.isArray(accounts)) { return 0; } return accounts .filter((account) => account.accountType === 'asset' || (!account.accountType && account.balance > 0)) .reduce((total, account) => total + account.balance, 0); } export default { getAccounts, getAccount, createAccount, updateAccount, deleteAccount, transferBetweenAccounts, groupAccountsByType, calculateTotalBalance, calculateTotalAssets, calculateTotalLiabilities, reorderAccounts, calculateAssetTypeTotal, }; // ============================================ // Sub-Account and Savings Pot Functions // Feature: financial-core-upgrade // ============================================ import type { AccountWithSubAccounts, CreateSubAccountInput, UpdateSubAccountInput, SavingsPotOperationResult, SavingsPotDepositInput, SavingsPotWithdrawInput, SavingsPotProgress, } from '../types'; import { calculateTotalBalance as calcTotalBalance, calculateSavingsPotProgress } from '../utils/balanceCalculator'; /** * Get sub-accounts for a parent account * Validates: Requirements 9.1, 9.5 */ export async function getSubAccounts(parentAccountId: number): Promise { const response = await api.get>( `/accounts/${parentAccountId}/sub-accounts` ); return response.data || []; } /** * Create a new sub-account * Validates: Requirements 9.2, 9.6 */ export async function createSubAccount( parentAccountId: number, data: CreateSubAccountInput ): Promise { const payload = { name: data.name, sub_account_type: data.subAccountType, balance: data.balance || 0, target_amount: data.targetAmount, target_date: data.targetDate, annual_rate: data.annualRate, interest_enabled: data.interestEnabled, icon: data.icon, currency: data.currency, }; const response = await api.post>( `/accounts/${parentAccountId}/sub-accounts`, payload ); if (!response.data) { throw new Error(response.error || 'Failed to create sub-account'); } return response.data; } /** * Update a sub-account * Validates: Requirements 9.3, 9.7 */ export async function updateSubAccount( parentAccountId: number, subAccountId: number, data: UpdateSubAccountInput ): Promise { const payload: Record = {}; if (data.name !== undefined) payload.name = data.name; if (data.targetAmount !== undefined) payload.target_amount = data.targetAmount; if (data.targetDate !== undefined) payload.target_date = data.targetDate; if (data.annualRate !== undefined) payload.annual_rate = data.annualRate; if (data.interestEnabled !== undefined) payload.interest_enabled = data.interestEnabled; if (data.icon !== undefined) payload.icon = data.icon; const response = await api.put>( `/accounts/${parentAccountId}/sub-accounts/${subAccountId}`, payload ); if (!response.data) { throw new Error(response.error || 'Failed to update sub-account'); } return response.data; } /** * Delete a sub-account * Validates: Requirements 9.4, 9.8 */ export async function deleteSubAccount( parentAccountId: number, subAccountId: number ): Promise { await api.delete>( `/accounts/${parentAccountId}/sub-accounts/${subAccountId}` ); } /** * Deposit to a savings pot * Validates: Requirements 10.1, 10.3, 10.5 */ export async function depositToSavingsPot( savingsPotId: number, data: SavingsPotDepositInput ): Promise { const response = await api.post>( `/savings-pot/${savingsPotId}/deposit`, { amount: data.amount } ); if (!response.data) { throw new Error(response.error || 'Failed to deposit to savings pot'); } return response.data; } /** * Withdraw from a savings pot * Validates: Requirements 10.2, 10.4, 10.6 */ export async function withdrawFromSavingsPot( savingsPotId: number, data: SavingsPotWithdrawInput ): Promise { const response = await api.post>( `/savings-pot/${savingsPotId}/withdraw`, { amount: data.amount } ); if (!response.data) { throw new Error(response.error || 'Failed to withdraw from savings pot'); } return response.data; } /** * Get savings pot details with progress * Validates: Requirements 10.7, 10.8 */ export async function getSavingsPot(savingsPotId: number): Promise { const response = await api.get>( `/savings-pot/${savingsPotId}` ); if (!response.data) { throw new Error(response.error || 'Savings pot not found'); } // Calculate progress const progress = calculateSavingsPotProgress(response.data); return { ...response.data, progress, }; } /** * Calculate total balance for an account with sub-accounts * Uses the formula: 总余额 = 可用余额 + 冻结余额 + Σ(非存钱罐子账户余额) * Validates: Requirements 1.3, 14.1 */ export function calculateAccountTotalBalance(account: AccountWithSubAccounts): number { return calcTotalBalance(account); } /** * Get all accounts with their sub-accounts */ export async function getAccountsWithSubAccounts(): Promise { const accounts = await getAccounts(); // Fetch sub-accounts for each parent account const accountsWithSubs = await Promise.all( accounts.map(async (account) => { try { const subAccounts = await getSubAccounts(account.id); return { ...account, subAccounts, } as AccountWithSubAccounts; } catch { return { ...account, subAccounts: [], } as AccountWithSubAccounts; } }) ); return accountsWithSubs; }