diff --git a/backend/src/modules/accounts/accounts.service.ts b/backend/src/modules/accounts/accounts.service.ts index 3eca89f..36660dd 100644 --- a/backend/src/modules/accounts/accounts.service.ts +++ b/backend/src/modules/accounts/accounts.service.ts @@ -42,10 +42,10 @@ export class AccountsService { throw new BadRequestException(`Account number ${dto.accountNumber} already exists`); } - const rows = await this.tenant.query( + const insertResult = await this.tenant.query( `INSERT INTO accounts (account_number, name, description, account_type, fund_type, parent_account_id, is_1099_reportable) VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING *`, + RETURNING id`, [ dto.accountNumber, dto.name, @@ -56,7 +56,8 @@ export class AccountsService { dto.is1099Reportable || false, ], ); - const account = rows[0]; + const accountId = Array.isArray(insertResult[0]) ? insertResult[0][0].id : insertResult[0].id; + const account = await this.findOne(accountId); // Create opening balance journal entry if initialBalance is provided and non-zero if (dto.initialBalance && dto.initialBalance !== 0) { @@ -64,7 +65,7 @@ export class AccountsService { const year = now.getFullYear(); const month = now.getMonth() + 1; - // Find or use the current fiscal period + // Find the current fiscal period const periods = await this.tenant.query( 'SELECT id FROM fiscal_periods WHERE year = $1 AND month = $2', [year, month], @@ -75,11 +76,18 @@ export class AccountsService { // Determine debit/credit based on account type const isDebitNormal = ['asset', 'expense'].includes(dto.accountType); - const debit = isDebitNormal ? absAmount : 0; - const credit = isDebitNormal ? 0 : absAmount; + const acctDebit = isDebitNormal ? absAmount : 0; + const acctCredit = isDebitNormal ? 0 : absAmount; + + // Determine equity offset account based on fund type + const equityAccountNumber = dto.fundType === 'reserve' ? 3100 : 3000; + const equityRows = await this.tenant.query( + 'SELECT id FROM accounts WHERE account_number = $1', + [equityAccountNumber], + ); // Create the journal entry - const jeRows = await this.tenant.query( + const jeInsert = await this.tenant.query( `INSERT INTO journal_entries (entry_date, description, entry_type, fiscal_period_id, is_posted, posted_at, created_by) VALUES (CURRENT_DATE, $1, 'opening_balance', $2, true, NOW(), $3) RETURNING id`, @@ -89,12 +97,21 @@ export class AccountsService { '00000000-0000-0000-0000-000000000000', ], ); + const jeId = Array.isArray(jeInsert[0]) ? jeInsert[0][0].id : jeInsert[0].id; - if (jeRows.length) { + // Line 1: debit/credit the target account + await this.tenant.query( + `INSERT INTO journal_entry_lines (journal_entry_id, account_id, debit, credit, memo) + VALUES ($1, $2, $3, $4, $5)`, + [jeId, accountId, acctDebit, acctCredit, 'Opening balance'], + ); + + // Line 2: balancing entry to equity offset account + if (equityRows.length) { await this.tenant.query( `INSERT INTO journal_entry_lines (journal_entry_id, account_id, debit, credit, memo) VALUES ($1, $2, $3, $4, $5)`, - [jeRows[0].id, account.id, debit, credit, 'Opening balance'], + [jeId, equityRows[0].id, acctCredit, acctDebit, `Opening balance for ${dto.name}`], ); } } @@ -137,11 +154,11 @@ export class AccountsService { sets.push(`updated_at = NOW()`); params.push(id); - const rows = await this.tenant.query( - `UPDATE accounts SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`, + await this.tenant.query( + `UPDATE accounts SET ${sets.join(', ')} WHERE id = $${idx}`, params, ); - return rows[0]; + return this.findOne(id); } async setPrimary(id: string) { diff --git a/frontend/src/pages/accounts/AccountsPage.tsx b/frontend/src/pages/accounts/AccountsPage.tsx index 989f1e7..954073c 100644 --- a/frontend/src/pages/accounts/AccountsPage.tsx +++ b/frontend/src/pages/accounts/AccountsPage.tsx @@ -20,6 +20,8 @@ import { Tooltip, SimpleGrid, Alert, + Divider, + Textarea, } from '@mantine/core'; import { DateInput } from '@mantine/dates'; import { useForm } from '@mantine/form'; @@ -39,6 +41,24 @@ import { import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../../services/api'; +const INVESTMENT_TYPES = ['inv_cd', 'inv_money_market', 'inv_treasury', 'inv_savings', 'inv_brokerage']; + +const isInvestmentType = (t: string) => INVESTMENT_TYPES.includes(t); + +const ACCOUNT_TYPE_OPTIONS = [ + { value: 'asset', label: 'Asset' }, + { value: 'liability', label: 'Liability' }, + { value: 'equity', label: 'Equity' }, + { value: 'income', label: 'Income' }, + { value: 'expense', label: 'Expense' }, + { value: '__divider', label: '── Investment Accounts ──', disabled: true }, + { value: 'inv_cd', label: 'Investment — CD' }, + { value: 'inv_money_market', label: 'Investment — Money Market' }, + { value: 'inv_treasury', label: 'Investment — Treasury' }, + { value: 'inv_savings', label: 'Investment — Savings' }, + { value: 'inv_brokerage', label: 'Investment — Brokerage' }, +]; + interface Account { id: string; account_number: number; @@ -142,10 +162,19 @@ export function AccountsPage() { fundType: 'operating', is1099Reportable: false, initialBalance: 0, + // Investment fields + institution: '', + accountNumberLast4: '', + principal: 0, + interestRate: 0, + maturityDate: null as Date | null, + purchaseDate: null as Date | null, + investmentNotes: '', }, validate: { - accountNumber: (v) => (v > 0 ? null : 'Required'), + accountNumber: (v, values) => isInvestmentType(values.accountType) ? null : (v > 0 ? null : 'Required'), name: (v) => (v.length > 0 ? null : 'Required'), + principal: (v, values) => isInvestmentType(values.accountType) ? (v > 0 ? null : 'Required') : null, }, }); @@ -168,11 +197,33 @@ export function AccountsPage() { if (editing) { return api.put(`/accounts/${editing.id}`, values); } + // Investment account creation + if (isInvestmentType(values.accountType)) { + const investmentTypeMap: Record = { + inv_cd: 'cd', inv_money_market: 'money_market', inv_treasury: 'treasury', + inv_savings: 'savings', inv_brokerage: 'other', + }; + return api.post('/investment-accounts', { + name: values.name, + institution: values.institution || null, + account_number_last4: values.accountNumberLast4 || null, + investment_type: investmentTypeMap[values.accountType] || 'other', + fund_type: values.fundType, + principal: values.principal, + interest_rate: values.interestRate || 0, + maturity_date: values.maturityDate ? values.maturityDate.toISOString().split('T')[0] : null, + purchase_date: values.purchaseDate ? values.purchaseDate.toISOString().split('T')[0] : null, + current_value: values.principal, + notes: values.investmentNotes || null, + }); + } return api.post('/accounts', values); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['accounts'] }); - notifications.show({ message: editing ? 'Account updated' : 'Account created', color: 'green' }); + queryClient.invalidateQueries({ queryKey: ['investments'] }); + const isInv = isInvestmentType(form.values.accountType); + notifications.show({ message: editing ? 'Account updated' : (isInv ? 'Investment created' : 'Account created'), color: 'green' }); close(); setEditing(null); form.reset(); @@ -421,47 +472,135 @@ export function AccountsPage() { {/* Create / Edit Account Modal */} - +
createMutation.mutate(values))}> - - - - + {!editing && ( - - - {!editing && ( - )} + + + + {!isInvestmentType(form.values.accountType) && ( + <> + + + {editing && ( + + + {/* Investment-specific fields */} + {!editing && isInvestmentType(form.values.accountType) && ( + <> + + + + + + + + + + + +