package repository import ( "baron-sso-backend/internal/domain" "context" "errors" "strconv" "strings" "time" "gorm.io/gorm" ) type TenantRepository interface { Create(ctx context.Context, tenant *domain.Tenant) error Update(ctx context.Context, tenant *domain.Tenant) error FindByID(ctx context.Context, id string) (*domain.Tenant, error) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) FindByName(ctx context.Context, name string) (*domain.Tenant, error) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) ListByType(ctx context.Context, tenantType string) ([]domain.Tenant, error) DeleteBulk(ctx context.Context, ids []string) error } type tenantRepository struct { db *gorm.DB } func NewTenantRepository(db *gorm.DB) TenantRepository { return &tenantRepository{db: db} } func (r *tenantRepository) Create(ctx context.Context, tenant *domain.Tenant) error { tenant.Slug = strings.ToLower(strings.TrimSpace(tenant.Slug)) return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if tenant.Slug != "" { suffix := "-deleted-" + strconv.FormatInt(time.Now().UTC().UnixNano(), 10) if err := tx.Unscoped(). Model(&domain.Tenant{}). Where("slug = ? AND deleted_at IS NOT NULL", tenant.Slug). Update("slug", gorm.Expr("slug || ?", suffix)).Error; err != nil { return err } } return tx.Create(tenant).Error }) } func (r *tenantRepository) Update(ctx context.Context, tenant *domain.Tenant) error { return r.db.WithContext(ctx).Save(tenant).Error } func (r *tenantRepository) FindByID(ctx context.Context, id string) (*domain.Tenant, error) { var tenant domain.Tenant if err := r.db.WithContext(ctx).Preload("Domains").First(&tenant, "id = ?", id).Error; err != nil { return nil, err } return &tenant, nil } func (r *tenantRepository) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) { var tenant domain.Tenant if err := r.db.WithContext(ctx).Preload("Domains").Where("slug = ?", strings.ToLower(slug)).First(&tenant).Error; err != nil { return nil, err } return &tenant, nil } func (r *tenantRepository) FindByName(ctx context.Context, name string) (*domain.Tenant, error) { var tenant domain.Tenant if err := r.db.WithContext(ctx).Preload("Domains").Where("name = ?", name).First(&tenant).Error; err != nil { return nil, err } return &tenant, nil } func (r *tenantRepository) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) { var tenantDomain domain.TenantDomain if err := r.db.WithContext(ctx).Where("domain = ?", domainName).First(&tenantDomain).Error; err != nil { return nil, err } var tenant domain.Tenant if err := r.db.WithContext(ctx).Preload("Domains").First(&tenant, "id = ?", tenantDomain.TenantID).Error; err != nil { return nil, err } return &tenant, nil } func (r *tenantRepository) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) { var tenants []domain.Tenant if len(ids) == 0 { return tenants, nil } if err := r.db.WithContext(ctx).Preload("Domains").Where("id IN ?", ids).Find(&tenants).Error; err != nil { return nil, err } return tenants, nil } func (r *tenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error { var existing domain.TenantDomain err := r.db.WithContext(ctx).Unscoped(). Where("tenant_id = ? AND domain = ?", tenantID, domainName). First(&existing).Error if err == nil { return r.db.WithContext(ctx).Unscoped().Model(&existing).Updates(map[string]any{ "verified": verified, "deleted_at": nil, }).Error } if !errors.Is(err, gorm.ErrRecordNotFound) { return err } td := domain.TenantDomain{ TenantID: tenantID, Domain: domainName, Verified: verified, } return r.db.WithContext(ctx).Create(&td).Error } func (r *tenantRepository) List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) { var tenants []domain.Tenant var total int64 db := r.db.WithContext(ctx).Model(&domain.Tenant{}) if parentID != "" { db = db.Where("parent_id = ?", parentID) } if err := db.Count(&total).Error; err != nil { return nil, 0, err } if err := db.Order("created_at desc, id desc").Limit(limit).Offset(offset).Preload("Domains").Find(&tenants).Error; err != nil { return nil, 0, err } return tenants, total, nil } func (r *tenantRepository) ListByType(ctx context.Context, tenantType string) ([]domain.Tenant, error) { var tenants []domain.Tenant if err := r.db.WithContext(ctx).Where("type = ?", tenantType).Preload("Domains").Find(&tenants).Error; err != nil { return nil, err } return tenants, nil } func (r *tenantRepository) DeleteBulk(ctx context.Context, ids []string) error { if len(ids) == 0 { return nil } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 1. Release slugs for all target tenants to allow reuse suffix := "-deleted-" + time.Now().Format("20060102150405") if err := tx.Model(&domain.Tenant{}).Where("id IN ?", ids). Update("slug", gorm.Expr("slug || ?", suffix)).Error; err != nil { return err } // 2. Soft delete tenants if err := tx.Where("id IN ?", ids).Delete(&domain.Tenant{}).Error; err != nil { return err } // 3. Also delete related UserGroups if any (Type USER_GROUP tenants have records in user_groups table) if err := tx.Where("id IN ?", ids).Delete(&domain.UserGroup{}).Error; err != nil { return err } return nil }) }