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() }