package repository import ( "database/sql" "time" "aggios-app/backend/internal/domain" "github.com/google/uuid" ) // TenantRepository handles database operations for tenants type TenantRepository struct { db *sql.DB } // NewTenantRepository creates a new tenant repository func NewTenantRepository(db *sql.DB) *TenantRepository { return &TenantRepository{db: db} } // DB returns the underlying database connection func (r *TenantRepository) DB() *sql.DB { return r.db } // Create creates a new tenant func (r *TenantRepository) Create(tenant *domain.Tenant) error { query := ` INSERT INTO tenants ( id, name, domain, subdomain, cnpj, razao_social, email, phone, website, address, neighborhood, number, complement, city, state, zip, description, industry, team_size, primary_color, secondary_color, logo_url, logo_horizontal_url, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25) RETURNING id, created_at, updated_at ` now := time.Now() tenant.ID = uuid.New() tenant.CreatedAt = now tenant.UpdatedAt = now return r.db.QueryRow( query, tenant.ID, tenant.Name, tenant.Domain, tenant.Subdomain, tenant.CNPJ, tenant.RazaoSocial, tenant.Email, tenant.Phone, tenant.Website, tenant.Address, tenant.Neighborhood, tenant.Number, tenant.Complement, tenant.City, tenant.State, tenant.Zip, tenant.Description, tenant.Industry, tenant.TeamSize, tenant.PrimaryColor, tenant.SecondaryColor, tenant.LogoURL, tenant.LogoHorizontalURL, tenant.CreatedAt, tenant.UpdatedAt, ).Scan(&tenant.ID, &tenant.CreatedAt, &tenant.UpdatedAt) } // FindByID finds a tenant by ID func (r *TenantRepository) FindByID(id uuid.UUID) (*domain.Tenant, error) { query := ` SELECT id, name, domain, subdomain, cnpj, razao_social, email, phone, website, address, neighborhood, number, complement, city, state, zip, description, industry, team_size, primary_color, secondary_color, logo_url, logo_horizontal_url, is_active, created_at, updated_at FROM tenants WHERE id = $1 ` tenant := &domain.Tenant{} var cnpj, razaoSocial, email, phone, website, address, neighborhood, number, complement, city, state, zip, description, industry, teamSize, primaryColor, secondaryColor, logoURL, logoHorizontalURL sql.NullString err := r.db.QueryRow(query, id).Scan( &tenant.ID, &tenant.Name, &tenant.Domain, &tenant.Subdomain, &cnpj, &razaoSocial, &email, &phone, &website, &address, &neighborhood, &number, &complement, &city, &state, &zip, &description, &industry, &teamSize, &primaryColor, &secondaryColor, &logoURL, &logoHorizontalURL, &tenant.IsActive, &tenant.CreatedAt, &tenant.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } // Handle nullable fields if cnpj.Valid { tenant.CNPJ = cnpj.String } if razaoSocial.Valid { tenant.RazaoSocial = razaoSocial.String } if email.Valid { tenant.Email = email.String } if phone.Valid { tenant.Phone = phone.String } if website.Valid { tenant.Website = website.String } if address.Valid { tenant.Address = address.String } if neighborhood.Valid { tenant.Neighborhood = neighborhood.String } if number.Valid { tenant.Number = number.String } if complement.Valid { tenant.Complement = complement.String } if city.Valid { tenant.City = city.String } if state.Valid { tenant.State = state.String } if zip.Valid { tenant.Zip = zip.String } if description.Valid { tenant.Description = description.String } if industry.Valid { tenant.Industry = industry.String } if teamSize.Valid { tenant.TeamSize = teamSize.String } if primaryColor.Valid { tenant.PrimaryColor = primaryColor.String } if secondaryColor.Valid { tenant.SecondaryColor = secondaryColor.String } if logoURL.Valid { tenant.LogoURL = logoURL.String } if logoHorizontalURL.Valid { tenant.LogoHorizontalURL = logoHorizontalURL.String } return tenant, nil } // FindBySubdomain finds a tenant by subdomain func (r *TenantRepository) FindBySubdomain(subdomain string) (*domain.Tenant, error) { query := ` SELECT id, name, domain, subdomain, created_at, updated_at FROM tenants WHERE subdomain = $1 ` tenant := &domain.Tenant{} err := r.db.QueryRow(query, subdomain).Scan( &tenant.ID, &tenant.Name, &tenant.Domain, &tenant.Subdomain, &tenant.CreatedAt, &tenant.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil } return tenant, err } // SubdomainExists checks if a subdomain is already taken func (r *TenantRepository) SubdomainExists(subdomain string) (bool, error) { var exists bool query := `SELECT EXISTS(SELECT 1 FROM tenants WHERE subdomain = $1)` err := r.db.QueryRow(query, subdomain).Scan(&exists) return exists, err } // FindAll returns all tenants func (r *TenantRepository) FindAll() ([]*domain.Tenant, error) { query := ` SELECT id, name, domain, subdomain, email, phone, cnpj, logo_url, is_active, created_at, updated_at FROM tenants ORDER BY created_at DESC ` rows, err := r.db.Query(query) if err != nil { return nil, err } defer rows.Close() var tenants []*domain.Tenant for rows.Next() { tenant := &domain.Tenant{} var email, phone, cnpj, logoURL sql.NullString err := rows.Scan( &tenant.ID, &tenant.Name, &tenant.Domain, &tenant.Subdomain, &email, &phone, &cnpj, &logoURL, &tenant.IsActive, &tenant.CreatedAt, &tenant.UpdatedAt, ) if err != nil { return nil, err } if email.Valid { tenant.Email = email.String } if phone.Valid { tenant.Phone = phone.String } if cnpj.Valid { tenant.CNPJ = cnpj.String } if logoURL.Valid { tenant.LogoURL = logoURL.String } tenants = append(tenants, tenant) } if tenants == nil { return []*domain.Tenant{}, nil } return tenants, nil } // Delete removes a tenant (and cascades to related data) func (r *TenantRepository) Delete(id uuid.UUID) error { // Start transaction tx, err := r.db.Begin() if err != nil { return err } defer tx.Rollback() // Delete all users associated with this tenant first _, err = tx.Exec(`DELETE FROM users WHERE tenant_id = $1`, id) if err != nil { return err } // Delete the tenant result, err := tx.Exec(`DELETE FROM tenants WHERE id = $1`, id) if err != nil { return err } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return sql.ErrNoRows } // Commit transaction return tx.Commit() } // UpdateProfile updates tenant profile information func (r *TenantRepository) UpdateProfile(id uuid.UUID, updates map[string]interface{}) error { query := ` UPDATE tenants SET name = COALESCE($1, name), cnpj = COALESCE($2, cnpj), razao_social = COALESCE($3, razao_social), email = COALESCE($4, email), phone = COALESCE($5, phone), website = COALESCE($6, website), address = COALESCE($7, address), neighborhood = COALESCE($8, neighborhood), number = COALESCE($9, number), complement = COALESCE($10, complement), city = COALESCE($11, city), state = COALESCE($12, state), zip = COALESCE($13, zip), description = COALESCE($14, description), industry = COALESCE($15, industry), team_size = COALESCE($16, team_size), primary_color = COALESCE($17, primary_color), secondary_color = COALESCE($18, secondary_color), logo_url = COALESCE($19, logo_url), logo_horizontal_url = COALESCE($20, logo_horizontal_url), updated_at = $21 WHERE id = $22 ` _, err := r.db.Exec( query, updates["name"], updates["cnpj"], updates["razao_social"], updates["email"], updates["phone"], updates["website"], updates["address"], updates["neighborhood"], updates["number"], updates["complement"], updates["city"], updates["state"], updates["zip"], updates["description"], updates["industry"], updates["team_size"], updates["primary_color"], updates["secondary_color"], updates["logo_url"], updates["logo_horizontal_url"], time.Now(), id, ) return err } // UpdateStatus updates the is_active status of a tenant func (r *TenantRepository) UpdateStatus(id uuid.UUID, isActive bool) error { query := `UPDATE tenants SET is_active = $1, updated_at = $2 WHERE id = $3` _, err := r.db.Exec(query, isActive, time.Now(), id) return err }