package mysql import ( "context" "errors" "gorm.io/gorm" "gorm.io/gorm/clause" "wucher/internal/domain/auth" "wucher/internal/domain/user" "wucher/internal/shared/pkg/uuidv7" ) type UserRepository struct { db *gorm.DB } func NewUserRepository(db *gorm.DB) *UserRepository { return &UserRepository{db: db} } func (r *UserRepository) Create(ctx context.Context, u *user.User) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if actor := actorUserIDFromContext(ctx); len(actor) > 0 { u.CreatedBy = actor u.UpdatedBy = actor } if err := tx.Create(u).Error; err != nil { return err } return replaceUserRolesTx(ctx, tx, u.ID, u.RoleID, u.RoleIDs) }) } func (r *UserRepository) Update(ctx context.Context, u *user.User) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if actor := actorUserIDFromContext(ctx); len(actor) > 0 { u.UpdatedBy = actor } // Persist only user columns and avoid relation side-effects from preloaded structs. if err := tx.Omit(clause.Associations).Save(u).Error; err != nil { return err } // Keep legacy and current image columns in sync when image patch is requested. if u.ProfileAttachmentPatchSet { updates := map[string]any{ "profile_attachment_id": nil, "image_attachment_id": nil, } if len(u.ProfileAttachmentID) == 16 { updates["profile_attachment_id"] = append([]byte(nil), u.ProfileAttachmentID...) updates["image_attachment_id"] = append([]byte(nil), u.ProfileAttachmentID...) } if err := tx.Model(&user.User{}).Where("id = ?", u.ID).Updates(updates).Error; err != nil { return err } } if u.RoleIDs == nil { return nil } return replaceUserRolesTx(ctx, tx, u.ID, u.RoleID, u.RoleIDs) }) } func (r *UserRepository) Delete(ctx context.Context, id []byte) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Model(&user.User{}). Where("id = ?", id). Updates(map[string]any{ "image_attachment_id": nil, "profile_attachment_id": nil, }).Error; err != nil { return err } if idStr, err := uuidv7.BytesToString(id); err == nil && idStr != "" { if err := tx.Exec("DELETE FROM attachments WHERE ref_type = ? AND ref_id = ?", "user_profile", idStr).Error; err != nil { return err } } if err := tx.Where("user_id = ?", id).Delete(&auth.UserRole{}).Error; err != nil { return err } return tx.Delete(&user.User{}, "id = ?", id).Error }) } func (r *UserRepository) GetByID(ctx context.Context, id []byte) (*user.User, error) { var u user.User err := r.db.WithContext(ctx). Model(&user.User{}). Select("users.*, roles.name AS role_name, roles.description AS role_description"). Joins("LEFT JOIN roles ON roles.id = users.role_id"). Preload("ProfileAttachment"). Preload("ProfileAttachment.File"). Where("users.id = ?", id). First(&u).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, err } if err := attachUserDomainRoles(ctx, r.db, &u); err != nil { return nil, err } return &u, nil } func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*user.User, error) { var u user.User err := r.db.WithContext(ctx). Model(&user.User{}). Select("users.*, roles.name AS role_name, roles.description AS role_description"). Joins("LEFT JOIN roles ON roles.id = users.role_id"). Preload("ProfileAttachment"). Preload("ProfileAttachment.File"). Where("users.email = ?", email). First(&u).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, err } if err := attachUserDomainRoles(ctx, r.db, &u); err != nil { return nil, err } return &u, nil } func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*user.User, error) { var u user.User err := r.db.WithContext(ctx). Model(&user.User{}). Select("users.*, roles.name AS role_name, roles.description AS role_description"). Joins("LEFT JOIN roles ON roles.id = users.role_id"). Preload("ProfileAttachment"). Preload("ProfileAttachment.File"). Where("users.username = ?", username). First(&u).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, err } if err := attachUserDomainRoles(ctx, r.db, &u); err != nil { return nil, err } return &u, nil } func (r *UserRepository) GetLastNameByID(ctx context.Context, id []byte) (string, error) { var u user.User err := r.db.WithContext(ctx). Table("users"). Select("last_name"). Where("id = ?", id). First(&u).Error if errors.Is(err, gorm.ErrRecordNotFound) { return "", nil } if err != nil { return "", err } return u.LastName, nil } // GetShortNameByID returns the user's profile short name (pilot or technician // 3-letter code, e.g. "GAN"), or an empty string when the user has no profile // short name set. Callers apply their own fallback (e.g. initials). func (r *UserRepository) GetShortNameByID(ctx context.Context, id []byte) (string, error) { var out struct { ShortName string `gorm:"column:short_name"` } err := r.db.WithContext(ctx). Table("users u"). Select("COALESCE(NULLIF(pp.short_name, ''), NULLIF(tp.short_name, ''), '') AS short_name"). Joins("LEFT JOIN pilot_profiles pp ON pp.user_id = u.id"). Joins("LEFT JOIN technician_profiles tp ON tp.user_id = u.id"). Where("u.id = ?", id). Take(&out).Error if errors.Is(err, gorm.ErrRecordNotFound) { return "", nil } if err != nil { return "", err } return out.ShortName, nil } func (r *UserRepository) List(ctx context.Context, filter string, sort string, limit, offset int) ([]user.User, int64, error) { var users []user.User var total int64 base := r.db.WithContext(ctx). Model(&user.User{}). Joins("LEFT JOIN roles ON roles.id = users.role_id") if filter != "" { like := "%" + filter + "%" base = base.Where("users.email LIKE ? OR users.username LIKE ? OR users.first_name LIKE ? OR users.last_name LIKE ?", like, like, like, like) } query := base.Select("users.*, roles.name AS role_name, roles.description AS role_description") query = query.Preload("ProfileAttachment").Preload("ProfileAttachment.File") if sort != "" { query = query.Order(sort) } if err := base.Count(&total).Error; err != nil { return nil, 0, err } if limit > 0 { query = query.Limit(limit).Offset(offset) } if err := query.Find(&users).Error; err != nil { return nil, 0, err } ptrs := make([]*user.User, 0, len(users)) for i := range users { ptrs = append(ptrs, &users[i]) } if err := attachUserDomainRoles(ctx, r.db, ptrs...); err != nil { return nil, 0, err } return users, total, nil }