package services // FirmDashboardDefaultService manages paliad.firm_dashboard_default — the // optional firm-wide dashboard layout that DashboardLayoutService prefers // over the code-resident FactoryDefaultLayout when seeding a new user's // row or resetting an existing layout. // // Design: docs/design-dashboard-configurable-2026-05-20.md §8.2 // (firm-wide default, deferred to v1.1 — activated in Slice C). // // Single optional row identified by id=1. Get returns (spec, true, nil) // when set, (zero, false, nil) when never set. Set overwrites; Clear // deletes (so GetOrSeed reverts to FactoryDefaultLayout). // // The HTTP layer (handlers/firm_dashboard_default.go) enforces admin-only // via auth.RequireAdmin. The service itself takes no admin parameter — // it trusts its callers because the only writer is the admin handler; // the read path is used by DashboardLayoutService on every seed/reset. import ( "context" "database/sql" "encoding/json" "errors" "fmt" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) // FirmDashboardDefaultService manages paliad.firm_dashboard_default. type FirmDashboardDefaultService struct { db *sqlx.DB } // NewFirmDashboardDefaultService wires the service. func NewFirmDashboardDefaultService(db *sqlx.DB) *FirmDashboardDefaultService { return &FirmDashboardDefaultService{db: db} } // Get returns (spec, true, nil) if a firm default is set, (zero, false, // nil) otherwise. SanitizeForRead is applied so callers always receive a // version-coherent spec; if anything had to be dropped (e.g. an admin // stashed a layout that references widgets we later removed), the // cleanup is in-memory only and the next admin write will persist it. func (s *FirmDashboardDefaultService) Get(ctx context.Context) (DashboardLayoutSpec, bool, error) { var raw json.RawMessage err := s.db.GetContext(ctx, &raw, ` SELECT layout_json FROM paliad.firm_dashboard_default WHERE id = 1 `) if errors.Is(err, sql.ErrNoRows) { return DashboardLayoutSpec{}, false, nil } if err != nil { return DashboardLayoutSpec{}, false, fmt.Errorf("get firm dashboard default: %w", err) } var spec DashboardLayoutSpec if err := json.Unmarshal(raw, &spec); err != nil { // Stored row is unparseable — treat as missing so the seed path // reverts to FactoryDefaultLayout rather than crash. return DashboardLayoutSpec{}, false, nil } spec.SanitizeForRead() return spec, true, nil } // Set persists the layout as the firm-wide default. Validates against the // catalog so an admin can't seed a layout that violates the contract. // updatedBy is recorded for audit; passing uuid.Nil clears the column. func (s *FirmDashboardDefaultService) Set(ctx context.Context, spec DashboardLayoutSpec, updatedBy uuid.UUID) (DashboardLayoutSpec, error) { if err := spec.Validate(); err != nil { return DashboardLayoutSpec{}, err } bytes, err := json.Marshal(spec) if err != nil { return DashboardLayoutSpec{}, fmt.Errorf("firm dashboard default marshal: %w", err) } var updaterArg interface{} if updatedBy != uuid.Nil { updaterArg = updatedBy } _, err = s.db.ExecContext(ctx, ` INSERT INTO paliad.firm_dashboard_default (id, layout_json, updated_by, updated_at) VALUES (1, $1, $2, now()) ON CONFLICT (id) DO UPDATE SET layout_json = EXCLUDED.layout_json, updated_by = EXCLUDED.updated_by, updated_at = now() `, json.RawMessage(bytes), updaterArg) if err != nil { return DashboardLayoutSpec{}, fmt.Errorf("firm dashboard default upsert: %w", err) } return spec, nil } // Clear deletes the firm default so seeds/resets revert to FactoryDefault- // Layout. Idempotent — clearing an already-absent row is a no-op. func (s *FirmDashboardDefaultService) Clear(ctx context.Context) error { _, err := s.db.ExecContext(ctx, `DELETE FROM paliad.firm_dashboard_default WHERE id = 1`) if err != nil { return fmt.Errorf("firm dashboard default clear: %w", err) } return nil }