Files
boc/backend/tenant/tenant.go
T

266 lines
7.4 KiB
Go
Raw Normal View History

package tenant
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
)
// Manager hanterar multi-tenancy för hela BOC-systemet
type Manager struct {
db *sql.DB
cache map[string]*Tenant
cacheMu sync.RWMutex
cacheTTL time.Duration
}
// Tenant representerar en verksamhet i systemet
type Tenant struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Domain string `json:"domain"`
OrgNumber string `json:"org_number,omitempty"` // Svenskt org.nr
VatNumber string `json:"vat_number,omitempty"` // Momsregistreringsnummer
Address string `json:"address,omitempty"`
Phone string `json:"phone,omitempty"`
Email string `json:"email,omitempty"`
Currency string `json:"currency"` // SEK, EUR, USD
Language string `json:"language"` // sv, en, etc.
Timezone string `json:"timezone"`
FiscalYear string `json:"fiscal_year"` // 2026-01-01
Status string `json:"status"` // active, suspended, archived
ParentID *string `json:"parent_id,omitempty"` // För koncernstruktur
LedgerDB string `json:"ledger_db,omitempty"` // Separat ledger-databas
Settings Settings `json:"settings"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Settings innehåller tenant-specifika konfigurationer
type Settings struct {
AccountingStandard string `json:"accounting_standard"` // BAS2024, etc.
VatRate float64 `json:"vat_rate"`
PayrollProvider string `json:"payroll_provider,omitempty"`
BankAccounts []BankAccount `json:"bank_accounts,omitempty"`
Integrations map[string]string `json:"integrations,omitempty"`
Features []string `json:"features,omitempty"` // Vilka moduler som är aktiva
}
// BankAccount representerar ett bankkonto för en tenant
type BankAccount struct {
Name string `json:"name"`
AccountNo string `json:"account_no"`
BIC string `json:"bic,omitempty"`
IBAN string `json:"iban,omitempty"`
IsDefault bool `json:"is_default"`
}
// NewManager skapar en ny tenant manager
func NewManager(db *sql.DB) *Manager {
return &Manager{
db: db,
cache: make(map[string]*Tenant),
cacheTTL: 5 * time.Minute,
}
}
// GetTenant hämtar en tenant baserat på ID
func (m *Manager) GetTenant(ctx context.Context, tenantID string) (*Tenant, error) {
// Kolla cache först
m.cacheMu.RLock()
if tenant, ok := m.cache[tenantID]; ok {
m.cacheMu.RUnlock()
return tenant, nil
}
m.cacheMu.RUnlock()
// Hämta från databas
var t Tenant
var settingsJSON []byte
var parentID sql.NullString
var fiscalYearStart sql.NullTime
var orgNumber, vatNumber, address, phone, email, ledgerDB sql.NullString
err := m.db.QueryRowContext(ctx, `
SELECT id, name, slug, domain, org_number, vat_number, address, phone, email,
currency, language, timezone, fiscal_year_start, status, parent_id,
ledger_db, settings, created_at, updated_at
FROM boc_tenants
WHERE id = $1
`, tenantID).Scan(
&t.ID, &t.Name, &t.Slug, &t.Domain, &orgNumber,
&vatNumber, &address, &phone, &email,
&t.Currency, &t.Language, &t.Timezone, &fiscalYearStart,
&t.Status, &parentID, &ledgerDB, &settingsJSON,
&t.CreatedAt, &t.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("tenant not found: %s", tenantID)
}
return nil, fmt.Errorf("failed to get tenant: %w", err)
}
if orgNumber.Valid {
t.OrgNumber = orgNumber.String
}
if vatNumber.Valid {
t.VatNumber = vatNumber.String
}
if address.Valid {
t.Address = address.String
}
if phone.Valid {
t.Phone = phone.String
}
if email.Valid {
t.Email = email.String
}
if ledgerDB.Valid {
t.LedgerDB = ledgerDB.String
}
if fiscalYearStart.Valid {
t.FiscalYear = fiscalYearStart.Time.Format("2006-01-02")
}
if parentID.Valid {
t.ParentID = &parentID.String
}
// Parse settings JSON
if len(settingsJSON) > 0 {
// TODO: Parse settings
}
// Uppdatera cache
m.cacheMu.Lock()
m.cache[tenantID] = &t
m.cacheMu.Unlock()
return &t, nil
}
// GetTenantBySlug hämtar tenant baserat på slug
func (m *Manager) GetTenantBySlug(ctx context.Context, slug string) (*Tenant, error) {
var tenantID string
err := m.db.QueryRowContext(ctx, `
SELECT id FROM boc_tenants WHERE slug = $1 AND status = 'active'
`, slug).Scan(&tenantID)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("tenant not found: %s", slug)
}
return nil, err
}
return m.GetTenant(ctx, tenantID)
}
// ListTenants listar alla aktiva tenants
func (m *Manager) ListTenants(ctx context.Context) ([]*Tenant, error) {
rows, err := m.db.QueryContext(ctx, `
SELECT id, name, slug, domain, status, currency, language, created_at
FROM boc_tenants
WHERE status = 'active'
ORDER BY name
`)
if err != nil {
return nil, err
}
defer rows.Close()
var tenants []*Tenant
for rows.Next() {
var t Tenant
rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Domain, &t.Status, &t.Currency, &t.Language, &t.CreatedAt)
tenants = append(tenants, &t)
}
return tenants, nil
}
// ListChildTenants listar dotterbolag för en koncern
func (m *Manager) ListChildTenants(ctx context.Context, parentID string) ([]*Tenant, error) {
rows, err := m.db.QueryContext(ctx, `
SELECT id, name, slug, domain, status, currency, language, created_at
FROM boc_tenants
WHERE parent_id = $1 AND status = 'active'
ORDER BY name
`, parentID)
if err != nil {
return nil, err
}
defer rows.Close()
var tenants []*Tenant
for rows.Next() {
var t Tenant
rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Domain, &t.Status, &t.Currency, &t.Language, &t.CreatedAt)
tenants = append(tenants, &t)
}
return tenants, nil
}
// CreateTenant skapar en ny verksamhet
func (m *Manager) CreateTenant(ctx context.Context, tenant *Tenant) error {
_, err := m.db.ExecContext(ctx, `
INSERT INTO boc_tenants (
id, name, slug, domain, org_number, vat_number, address, phone, email,
currency, language, timezone, fiscal_year_start, status, parent_id, ledger_db
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
`,
tenant.ID, tenant.Name, tenant.Slug, tenant.Domain, tenant.OrgNumber,
tenant.VatNumber, tenant.Address, tenant.Phone, tenant.Email,
tenant.Currency, tenant.Language, tenant.Timezone, tenant.FiscalYear,
tenant.Status, tenant.ParentID, tenant.LedgerDB,
)
return err
}
// GetTenantLedgerDB returnerar ledger-databas för en tenant
func (m *Manager) GetTenantLedgerDB(ctx context.Context, tenantID string) (string, error) {
tenant, err := m.GetTenant(ctx, tenantID)
if err != nil {
return "", err
}
// Om tenant har egen ledger-databas, använd den
if tenant.LedgerDB != "" {
return tenant.LedgerDB, nil
}
// Annars använd default
return "postgres://postgres:postgres@localhost:5432/aamos_ledger?sslmode=disable", nil
}
// IsFeatureEnabled kollar om en modul är aktiverad för tenant
func (m *Manager) IsFeatureEnabled(ctx context.Context, tenantID string, feature string) bool {
tenant, err := m.GetTenant(ctx, tenantID)
if err != nil {
return false
}
for _, f := range tenant.Settings.Features {
if f == feature {
return true
}
}
return false
}
// InvalidateCache rensar cache för en tenant
func (m *Manager) InvalidateCache(tenantID string) {
m.cacheMu.Lock()
delete(m.cache, tenantID)
m.cacheMu.Unlock()
}