// Package ledger - Real client for aamos-ledger integration // Uses direct DB connection for reliability package ledger import ( "context" "database/sql" "fmt" "time" _ "github.com/lib/pq" ) // RealClient connects directly to aamos-ledger database type RealClient struct { db *sql.DB } // NewRealClient creates a client connected to ledger DB func NewRealClient(dbURL string) (*RealClient, error) { db, err := sql.Open("postgres", dbURL) if err != nil { return nil, fmt.Errorf("failed to connect to ledger DB: %w", err) } db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) if err := db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping ledger DB: %w", err) } return &RealClient{db: db}, nil } // Account represents a BAS account type Account struct { Code string `json:"code"` Name string `json:"name"` AccountType string `json:"account_type"` Balance float64 `json:"balance"` } // GetAccounts returns all BAS accounts func (c *RealClient) GetAccounts(ctx context.Context) ([]Account, error) { rows, err := c.db.QueryContext(ctx, ` SELECT a.code, a.name, a.account_type, COALESCE(SUM(CASE WHEN jl.debit IS NOT NULL THEN jl.debit ELSE 0 END) - SUM(CASE WHEN jl.credit IS NOT NULL THEN jl.credit ELSE 0 END), 0) as balance FROM accounts a LEFT JOIN journal_lines jl ON a.id = jl.account_id GROUP BY a.id, a.code, a.name, a.account_type ORDER BY a.code `) if err != nil { return nil, fmt.Errorf("query accounts: %w", err) } defer rows.Close() var accounts []Account for rows.Next() { var a Account if err := rows.Scan(&a.Code, &a.Name, &a.AccountType, &a.Balance); err != nil { return nil, fmt.Errorf("scan account: %w", err) } accounts = append(accounts, a) } return accounts, rows.Err() } // GetBalanceSheet returns assets, liabilities, equity func (c *RealClient) GetBalanceSheet(ctx context.Context) (map[string]interface{}, error) { accounts, err := c.GetAccounts(ctx) if err != nil { return nil, err } var assets, liabilities, equity []Account var totalAssets, totalLiabilities, totalEquity float64 for _, a := range accounts { switch a.AccountType { case "Asset": assets = append(assets, a) totalAssets += a.Balance case "Liability": liabilities = append(liabilities, a) totalLiabilities += a.Balance case "Equity": equity = append(equity, a) totalEquity += a.Balance } } return map[string]interface{}{ "assets": accountsToMaps(assets), "liabilities": accountsToMaps(liabilities), "equity": accountsToMaps(equity), "total_assets": totalAssets, "total_liabilities": totalLiabilities, "total_equity": totalEquity, "period": time.Now().Format("2006-01"), }, nil } // GetTrialBalance returns trial balance func (c *RealClient) GetTrialBalance(ctx context.Context) ([]map[string]interface{}, error) { accounts, err := c.GetAccounts(ctx) if err != nil { return nil, err } var result []map[string]interface{} for _, a := range accounts { result = append(result, map[string]interface{}{ "account_number": a.Code, "account_name": a.Name, "balance": a.Balance, "account_type": a.AccountType, }) } return result, nil } // Close closes the database connection func (c *RealClient) Close() error { return c.db.Close() } func accountsToMaps(accounts []Account) []map[string]interface{} { var result []map[string]interface{} for _, a := range accounts { result = append(result, map[string]interface{}{ "account": a.Code + " - " + a.Name, "amount": a.Balance, }) } return result } // Transaction represents a ledger transaction type Transaction struct { ID string `json:"id"` Date string `json:"date"` Description string `json:"description"` Amount float64 `json:"amount"` Type string `json:"type"` AccountCode string `json:"account_code,omitempty"` AccountName string `json:"account_name,omitempty"` } // GetTransactions returns all journal entries as transactions func (c *RealClient) GetTransactions(ctx context.Context) ([]Transaction, error) { rows, err := c.db.QueryContext(ctx, ` SELECT je.id, je.entry_date, je.description, COALESCE(jl.debit, 0) - COALESCE(jl.credit, 0) as amount, CASE WHEN COALESCE(jl.debit, 0) > 0 THEN 'debit' ELSE 'credit' END as type, a.code, a.name FROM journal_entries je JOIN journal_lines jl ON je.id = jl.journal_entry_id JOIN accounts a ON jl.account_id = a.id ORDER BY je.entry_date DESC LIMIT 100 `) if err != nil { return nil, fmt.Errorf("query transactions: %w", err) } defer rows.Close() var transactions []Transaction for rows.Next() { var t Transaction if err := rows.Scan(&t.ID, &t.Date, &t.Description, &t.Amount, &t.Type, &t.AccountCode, &t.AccountName); err != nil { return nil, fmt.Errorf("scan transaction: %w", err) } transactions = append(transactions, t) } return transactions, rows.Err() }