Files
boc/backend/ledger/client.go
T

268 lines
8.7 KiB
Go
Raw Normal View History

// 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,
})
}