package ledger import ( "context" "database/sql" "fmt" "time" _ "github.com/lib/pq" ) // RobustClient connects directly to aamos-ledger database // and provides a stable API for BOC type RobustClient struct { db *sql.DB } // NewRobustClient creates a client connected to ledger DB func NewRobustClient(dbURL string) (*RobustClient, 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 &RobustClient{db: db}, nil } // LedgerAccount represents a BAS account type LedgerAccount struct { Code string `json:"code"` Name string `json:"name"` AccountType string `json:"account_type"` Balance float64 `json:"balance"` } // GetAccounts returns all BAS accounts with balances func (c *RobustClient) GetAccounts(ctx context.Context) ([]LedgerAccount, 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 []LedgerAccount for rows.Next() { var a LedgerAccount 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() } // BalanceSheet represents a balance sheet report type BalanceSheet struct { Assets []LedgerAccount `json:"assets"` Liabilities []LedgerAccount `json:"liabilities"` Equity []LedgerAccount `json:"equity"` TotalAssets float64 `json:"total_assets"` TotalLiabilities float64 `json:"total_liabilities"` TotalEquity float64 `json:"total_equity"` Period string `json:"period"` } // GetBalanceSheet returns assets, liabilities, equity for a period func (c *RobustClient) GetBalanceSheet(ctx context.Context, period string) (*BalanceSheet, error) { if period == "" { period = time.Now().Format("2006-01") } 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 LEFT JOIN journal_entries je ON jl.journal_entry_id = je.id AND je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month') GROUP BY a.id, a.code, a.name, a.account_type ORDER BY a.code `, period+"-01") if err != nil { return nil, fmt.Errorf("query balance sheet: %w", err) } defer rows.Close() bs := &BalanceSheet{Period: period} for rows.Next() { var a LedgerAccount if err := rows.Scan(&a.Code, &a.Name, &a.AccountType, &a.Balance); err != nil { return nil, fmt.Errorf("scan account: %w", err) } switch a.AccountType { case "Asset": bs.Assets = append(bs.Assets, a) bs.TotalAssets += a.Balance case "Liability": bs.Liabilities = append(bs.Liabilities, a) bs.TotalLiabilities += a.Balance case "Equity": bs.Equity = append(bs.Equity, a) bs.TotalEquity += a.Balance } } return bs, rows.Err() } // IncomeStatement represents a P&L report type IncomeStatement struct { Revenues []LedgerAccount `json:"revenues"` Expenses []LedgerAccount `json:"expenses"` TotalRevenue float64 `json:"total_revenue"` TotalExpense float64 `json:"total_expense"` NetIncome float64 `json:"net_income"` Period string `json:"period"` } // GetIncomeStatement returns P&L for a period func (c *RobustClient) GetIncomeStatement(ctx context.Context, period string) (*IncomeStatement, error) { if period == "" { period = time.Now().Format("2006-01") } 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 LEFT JOIN journal_entries je ON jl.journal_entry_id = je.id AND je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month') WHERE a.account_type IN ('Revenue', 'Expense') GROUP BY a.id, a.code, a.name, a.account_type ORDER BY a.code `, period+"-01") if err != nil { return nil, fmt.Errorf("query income statement: %w", err) } defer rows.Close() is := &IncomeStatement{Period: period} for rows.Next() { var a LedgerAccount if err := rows.Scan(&a.Code, &a.Name, &a.AccountType, &a.Balance); err != nil { return nil, fmt.Errorf("scan account: %w", err) } switch a.AccountType { case "Revenue": is.Revenues = append(is.Revenues, a) is.TotalRevenue += a.Balance case "Expense": is.Expenses = append(is.Expenses, a) is.TotalExpense += a.Balance } } is.NetIncome = is.TotalRevenue - is.TotalExpense return is, rows.Err() } // MomsReport represents Swedish VAT report type MomsReport struct { MomsIn float64 `json:"moms_in"` MomsUt float64 `json:"moms_ut"` MomsAttBetala float64 `json:"moms_att_betala"` Period string `json:"period"` } // GetMomsReport returns VAT report for a period func (c *RobustClient) GetMomsReport(ctx context.Context, period string) (*MomsReport, error) { if period == "" { period = time.Now().Format("2006-01") } var report MomsReport report.Period = period // Moms in (utgående moms från försäljning) err := c.db.QueryRowContext(ctx, ` SELECT COALESCE(SUM(jl.credit), 0) FROM journal_lines jl JOIN journal_entries je ON jl.journal_entry_id = je.id JOIN accounts a ON jl.account_id = a.id WHERE je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month') AND a.code LIKE '26%' `, period+"-01").Scan(&report.MomsUt) if err != nil { return nil, fmt.Errorf("query moms ut: %w", err) } // Moms att betala (förenklad - i verkligheten mer komplex) report.MomsAttBetala = report.MomsUt - report.MomsIn return &report, nil } // Close closes the database connection func (c *RobustClient) Close() error { return c.db.Close() }