feat: BOC ledger-integration frontend + backend fixes

- Uppdatera robust_client.go med rätt tabellnamn (boc_chart_of_accounts, boc_journal_lines, boc_journal_entries)
- Uppdatera kolumnnamn (entry_id istället för journal_entry_id, account_code istället för code)
- Hantera både stora och små bokstäver för account_type
- Lägg till ledgerApi i frontend med BalanceSheet, IncomeStatement, MomsReport
- Uppdatera DashboardPage med Ledger KPI-kort
- Lägg till LEDGER_DB_URL i docker-compose.yml
- Uppdatera Dockerfile till golang:1.25-alpine
This commit is contained in:
Bernt
2026-07-29 19:53:39 +00:00
parent f367eaadb8
commit bd28b60338
14 changed files with 671 additions and 488 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# BOC Backend Dockerfile
# Multi-stage build for minimal image
FROM golang:1.22-alpine AS builder
FROM golang:1.25-alpine AS builder
WORKDIR /app
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -26,7 +26,7 @@ func Load() *Config {
return &Config{
Port: getEnv("PORT", "9092"),
DBURL: getEnv("DB_URL", "postgres://boc:boc@localhost:5432/boc?sslmode=disable"),
LedgerDBURL: getEnv("LEDGER_DB_URL", "postgres://postgres:postgres@localhost:5432/aamos_ledger?sslmode=disable"),
LedgerDBURL: getEnv("LEDGER_DB_URL", "postgres://wavult_admin:efG15aKjqgu7uotZoAiLTRBtBDMoXITxIe9Hi6EB@platform-identity-core.cvi0qcksmsfj.eu-north-1.rds.amazonaws.com:5432/amos?sslmode=disable"),
JWTSecret: getEnv("JWT_SECRET", "w+Qkf/CoDda3Ba7vZLKokrGHiwUV5Ak/3tiBmFAvRC8="),
AMOSBaseURL: getEnv("AMOS_BASE_URL", "http://localhost:9000"),
CORSOrigins: splitComma(getEnv("CORS_ORIGINS", "http://localhost:3000")),
+89
View File
@@ -0,0 +1,89 @@
package handlers
import (
"encoding/json"
"net/http"
"boc/ledger"
)
var ledgerDBURL = getEnv("LEDGER_DB_URL", "postgres://wavult_admin:efG15aKjqgu7uotZoAiLTRBtBDMoXITxIe9Hi6EB@platform-identity-core.cvi0qcksmsfj.eu-north-1.rds.amazonaws.com:5432/amos?sslmode=disable")
// LedgerV2Handler uses direct DB connection for reliable data
type LedgerV2Handler struct {
client *ledger.RobustClient
}
// NewLedgerV2Handler creates a handler with DB connection
func NewLedgerV2Handler() (*LedgerV2Handler, error) {
client, err := ledger.NewRobustClient(ledgerDBURL)
if err != nil {
return nil, err
}
return &LedgerV2Handler{client: client}, nil
}
// GetAccounts returns all BAS accounts
func (h *LedgerV2Handler) GetAccounts(w http.ResponseWriter, r *http.Request) {
accounts, err := h.client.GetAccounts(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"accounts": accounts,
})
}
// GetBalanceSheet returns balance sheet for a period
func (h *LedgerV2Handler) GetBalanceSheet(w http.ResponseWriter, r *http.Request) {
period := r.URL.Query().Get("period")
if period == "" {
period = "2026-01"
}
bs, err := h.client.GetBalanceSheet(r.Context(), period)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(bs)
}
// GetIncomeStatement returns P&L for a period
func (h *LedgerV2Handler) GetIncomeStatement(w http.ResponseWriter, r *http.Request) {
period := r.URL.Query().Get("period")
if period == "" {
period = "2026-01"
}
is, err := h.client.GetIncomeStatement(r.Context(), period)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(is)
}
// GetMomsReport returns VAT report for a period
func (h *LedgerV2Handler) GetMomsReport(w http.ResponseWriter, r *http.Request) {
period := r.URL.Query().Get("period")
if period == "" {
period = "2026-01"
}
report, err := h.client.GetMomsReport(r.Context(), period)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(report)
}
+26 -26
View File
@@ -44,13 +44,13 @@ type LedgerAccount struct {
// 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,
SELECT a.account_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
FROM boc_chart_of_accounts a
LEFT JOIN boc_journal_lines jl ON a.id = jl.account_id
GROUP BY a.id, a.account_code, a.name, a.account_type
ORDER BY a.account_code
`)
if err != nil {
return nil, fmt.Errorf("query accounts: %w", err)
@@ -87,14 +87,14 @@ func (c *RobustClient) GetBalanceSheet(ctx context.Context, period string) (*Bal
}
rows, err := c.db.QueryContext(ctx, `
SELECT a.code, a.name, a.account_type,
SELECT a.account_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
FROM boc_chart_of_accounts a
LEFT JOIN boc_journal_lines jl ON a.id = jl.account_id
LEFT JOIN boc_journal_entries je ON jl.entry_id = je.id AND je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month')
GROUP BY a.id, a.account_code, a.name, a.account_type
ORDER BY a.account_code
`, period+"-01")
if err != nil {
return nil, fmt.Errorf("query balance sheet: %w", err)
@@ -109,13 +109,13 @@ func (c *RobustClient) GetBalanceSheet(ctx context.Context, period string) (*Bal
}
switch a.AccountType {
case "Asset":
case "Asset", "asset":
bs.Assets = append(bs.Assets, a)
bs.TotalAssets += a.Balance
case "Liability":
case "Liability", "liability":
bs.Liabilities = append(bs.Liabilities, a)
bs.TotalLiabilities += a.Balance
case "Equity":
case "Equity", "equity":
bs.Equity = append(bs.Equity, a)
bs.TotalEquity += a.Balance
}
@@ -141,15 +141,15 @@ func (c *RobustClient) GetIncomeStatement(ctx context.Context, period string) (*
}
rows, err := c.db.QueryContext(ctx, `
SELECT a.code, a.name, a.account_type,
SELECT a.account_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')
FROM boc_chart_of_accounts a
LEFT JOIN boc_journal_lines jl ON a.id = jl.account_id
LEFT JOIN boc_journal_entries je ON jl.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
GROUP BY a.id, a.account_code, a.name, a.account_type
ORDER BY a.account_code
`, period+"-01")
if err != nil {
return nil, fmt.Errorf("query income statement: %w", err)
@@ -164,10 +164,10 @@ func (c *RobustClient) GetIncomeStatement(ctx context.Context, period string) (*
}
switch a.AccountType {
case "Revenue":
case "Revenue", "revenue":
is.Revenues = append(is.Revenues, a)
is.TotalRevenue += a.Balance
case "Expense":
case "Expense", "expense":
is.Expenses = append(is.Expenses, a)
is.TotalExpense += a.Balance
}
@@ -197,11 +197,11 @@ func (c *RobustClient) GetMomsReport(ctx context.Context, period string) (*MomsR
// 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
FROM boc_journal_lines jl
JOIN boc_journal_entries je ON jl.entry_id = je.id
JOIN boc_chart_of_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%'
AND a.account_code LIKE '26%'
`, period+"-01").Scan(&report.MomsUt)
if err != nil {
return nil, fmt.Errorf("query moms ut: %w", err)