266 lines
7.4 KiB
Go
266 lines
7.4 KiB
Go
|
|
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()
|
||
|
|
}
|