// Package ledger provides a client for aamos-ledger integration. // Uses direct DB connection for reliability (Linus-style: simple > clever). package ledger import ( "context" "encoding/json" "fmt" "net/http" "os" "time" ) var baseURL = getEnv("LEDGER_URL", "http://localhost:3250") var ledgerDBURL = getEnv("LEDGER_DB_URL", "postgres://postgres:postgres@localhost:5432/aamos_ledger?sslmode=disable") func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // Client handles communication with aamos-ledger type Client struct { baseURL string client *http.Client } // NewClient creates a new ledger client func NewClient() *Client { return &Client{ baseURL: baseURL, client: &http.Client{Timeout: 10 * time.Second}, } } // Get proxies a GET request to the ledger and returns the JSON response. func (c *Client) Get(ctx context.Context, path string) (map[string]interface{}, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } resp, err := c.client.Do(req) if err != nil { return nil, fmt.Errorf("ledger unavailable: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("ledger error: status %d", resp.StatusCode) } var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode error: %w", err) } return result, nil } // Handler wraps the client for HTTP handlers type Handler struct { client *Client realClient *RealClient } // NewHandler creates a new ledger HTTP handler func NewHandler() *Handler { h := &Handler{client: NewClient()} // Try to create real client (direct DB connection) if realClient, err := NewRealClient(ledgerDBURL); err == nil { h.realClient = realClient } return h } // Proxy handles any ledger endpoint with a single method func (h *Handler) Proxy(w http.ResponseWriter, r *http.Request, ledgerPath string) { result, err := h.client.Get(r.Context(), ledgerPath) if err != nil { // Fallback to mock data when ledger is unavailable h.mockResponse(w, r, ledgerPath) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // mockResponse returns mock data for development func (h *Handler) mockResponse(w http.ResponseWriter, r *http.Request, ledgerPath string) { w.Header().Set("Content-Type", "application/json") switch ledgerPath { case "/api/ledger/reports/balance": json.NewEncoder(w).Encode(map[string]interface{}{ "assets": []map[string]interface{}{ {"account": "1930 - Checkkonto", "amount": 245000}, {"account": "1940 - Sparkonto", "amount": 500000}, {"account": "1510 - Kundfordringar", "amount": 125000}, }, "liabilities": []map[string]interface{}{ {"account": "2440 - Leverantörsskulder", "amount": 85000}, {"account": "2013 - Aktiekapital", "amount": 100000}, }, "equity": []map[string]interface{}{ {"account": "2091 - Balanserad vinst", "amount": 485000}, }, "total_assets": 870000, "total_liabilities": 185000, "total_equity": 685000, "period": time.Now().Format("2006-01"), }) case "/api/ledger/reports/income": json.NewEncoder(w).Encode(map[string]interface{}{ "revenue": []map[string]interface{}{ {"account": "3001 - Försäljning tjänster", "amount": 450000}, {"account": "3002 - Försäljning produkter", "amount": 125000}, }, "expenses": []map[string]interface{}{ {"account": "6100 - Löner", "amount": 180000}, {"account": "6200 - Hyra", "amount": 45000}, {"account": "6300 - Marknadsföring", "amount": 35000}, {"account": "6400 - IT-kostnader", "amount": 25000}, }, "total_revenue": 575000, "total_expenses": 285000, "net_income": 290000, "period": time.Now().Format("2006-01"), }) case "/api/ledger/tax/moms": json.NewEncoder(w).Encode(map[string]interface{}{ "moms_in": 143750, "moms_ut": 71250, "moms_att_betala": 72500, "period": time.Now().Format("2006-01"), }) case "/api/ledger/accounts": json.NewEncoder(w).Encode(map[string]interface{}{ "accounts": []map[string]interface{}{ {"id": "1930", "name": "Checkkonto", "type": "asset", "balance": 245000}, {"id": "1940", "name": "Sparkonto", "type": "asset", "balance": 500000}, {"id": "1510", "name": "Kundfordringar", "type": "asset", "balance": 125000}, {"id": "2440", "name": "Leverantörsskulder", "type": "liability", "balance": 85000}, {"id": "2013", "name": "Aktiekapital", "type": "equity", "balance": 100000}, {"id": "2091", "name": "Balanserad vinst", "type": "equity", "balance": 485000}, {"id": "3001", "name": "Försäljning tjänster", "type": "revenue", "balance": 450000}, {"id": "6100", "name": "Löner", "type": "expense", "balance": 180000}, }, }) case "/api/ledger/invoices": json.NewEncoder(w).Encode(map[string]interface{}{ "invoices": []map[string]interface{}{ {"id": "INV-001", "customer": "Test AB", "amount": 25000, "status": "paid", "due_date": "2026-07-30"}, {"id": "INV-002", "customer": "Acme Corp", "amount": 45000, "status": "pending", "due_date": "2026-08-15"}, {"id": "INV-003", "customer": "Stark Industries", "amount": 125000, "status": "overdue", "due_date": "2026-06-30"}, }, "total": 3, }) case "/api/ledger/reports/cashflow": json.NewEncoder(w).Encode(map[string]interface{}{ "inflow": 320000, "outflow": 180000, "net": 140000, "period": time.Now().Format("2006-01"), }) case "/api/ledger/budget": json.NewEncoder(w).Encode(map[string]interface{}{ "budget": 500000, "actual": 245000, "remaining": 255000, "period": time.Now().Format("2006-01"), }) case "/api/ledger/expenses": json.NewEncoder(w).Encode(map[string]interface{}{ "expenses": []map[string]interface{}{ {"id": "EXP-001", "category": "Boende", "amount": 8500, "date": "2026-07-01"}, {"id": "EXP-002", "category": "Mat", "amount": 3200, "date": "2026-07-05"}, {"id": "EXP-003", "category": "Resa", "amount": 4500, "date": "2026-07-10"}, }, "total": 3, }) default: json.NewEncoder(w).Encode(map[string]string{"error": "unknown endpoint"}) } } // Convenience methods - use real client if available, fallback to proxy/mock func (h *Handler) GetBalanceSheet(w http.ResponseWriter, r *http.Request) { if h.realClient != nil { result, err := h.realClient.GetBalanceSheet(r.Context()) if err == nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) return } } h.Proxy(w, r, "/api/ledger/reports/balance") } func (h *Handler) GetIncomeStatement(w http.ResponseWriter, r *http.Request) { h.Proxy(w, r, "/api/ledger/reports/income") } func (h *Handler) GetMomsReport(w http.ResponseWriter, r *http.Request) { h.Proxy(w, r, "/api/ledger/tax/moms") } func (h *Handler) GetAccounts(w http.ResponseWriter, r *http.Request) { if h.realClient != nil { accounts, err := h.realClient.GetAccounts(r.Context()) if err == nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"accounts": accounts}) return } } h.Proxy(w, r, "/api/ledger/accounts") } func (h *Handler) GetInvoices(w http.ResponseWriter, r *http.Request) { h.Proxy(w, r, "/api/ledger/invoices") } func (h *Handler) GetCashflow(w http.ResponseWriter, r *http.Request) { h.Proxy(w, r, "/api/ledger/reports/cashflow") } func (h *Handler) GetBudget(w http.ResponseWriter, r *http.Request) { h.Proxy(w, r, "/api/ledger/budget") } func (h *Handler) CreateExpense(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotImplemented) json.NewEncoder(w).Encode(map[string]string{"error": "not implemented"}) } func (h *Handler) ListExpenses(w http.ResponseWriter, r *http.Request) { h.Proxy(w, r, "/api/ledger/expenses") } // GetTransactions returnerar transaktioner från ledger func (h *Handler) GetTransactions(w http.ResponseWriter, r *http.Request) { if h.realClient != nil { transactions, err := h.realClient.GetTransactions(r.Context()) if err == nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"transactions": transactions}) return } } // Fallback: returnera mock data w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "transactions": []map[string]interface{}{ {"id": "TXN-001", "date": "2026-07-01", "description": "Försäljning", "amount": 25000, "type": "income"}, {"id": "TXN-002", "date": "2026-07-05", "description": "Löner", "amount": -18000, "type": "expense"}, {"id": "TXN-003", "date": "2026-07-10", "description": "Hyra", "amount": -4500, "type": "expense"}, }, "total": 3, }) }