feat: Add player_configs feature and migrate user preferences
- Implemented player_configs table to store multiple player configurations per user. - Migrated existing player settings from user_preferences to player_configs. - Removed player-related columns from user_preferences. - Added referral state fields to user for tracking referral rewards. - Created migration scripts for database changes and data migration. - Added test cases for app services and usage helpers. - Introduced video job service interfaces and implementations.
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
appv1 "stream.api/internal/gen/proto/app/v1"
|
||||
"stream.api/internal/video"
|
||||
)
|
||||
|
||||
func (s *appServices) GetAdminDashboard(ctx context.Context, _ *appv1.GetAdminDashboardRequest) (*appv1.GetAdminDashboardResponse, error) {
|
||||
@@ -95,20 +96,19 @@ func (s *appServices) GetAdminUser(ctx context.Context, req *appv1.GetAdminUserR
|
||||
return nil, status.Error(codes.Internal, "Failed to get user")
|
||||
}
|
||||
|
||||
payload, err := s.buildAdminUser(ctx, &user)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to get user")
|
||||
}
|
||||
|
||||
var subscription model.PlanSubscription
|
||||
var subscriptionPayload *appv1.PlanSubscription
|
||||
if err := s.db.WithContext(ctx).Where("user_id = ?", id).Order("created_at DESC").First(&subscription).Error; err == nil {
|
||||
subscriptionPayload = toProtoPlanSubscription(&subscription)
|
||||
var subscription *model.PlanSubscription
|
||||
var subscriptionRecord model.PlanSubscription
|
||||
if err := s.db.WithContext(ctx).Where("user_id = ?", id).Order("created_at DESC").First(&subscriptionRecord).Error; err == nil {
|
||||
subscription = &subscriptionRecord
|
||||
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.Internal, "Failed to get user")
|
||||
}
|
||||
|
||||
return &appv1.GetAdminUserResponse{User: &appv1.AdminUserDetail{User: payload, Subscription: subscriptionPayload}}, nil
|
||||
detail, err := s.buildAdminUserDetail(ctx, &user, subscription)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to get user")
|
||||
}
|
||||
return &appv1.GetAdminUserResponse{User: detail}, nil
|
||||
}
|
||||
func (s *appServices) CreateAdminUser(ctx context.Context, req *appv1.CreateAdminUserRequest) (*appv1.CreateAdminUserResponse, error) {
|
||||
if _, err := s.requireAdmin(ctx); err != nil {
|
||||
@@ -243,6 +243,89 @@ func (s *appServices) UpdateAdminUser(ctx context.Context, req *appv1.UpdateAdmi
|
||||
}
|
||||
return &appv1.UpdateAdminUserResponse{User: payload}, nil
|
||||
}
|
||||
func (s *appServices) UpdateAdminUserReferralSettings(ctx context.Context, req *appv1.UpdateAdminUserReferralSettingsRequest) (*appv1.UpdateAdminUserReferralSettingsResponse, error) {
|
||||
if _, err := s.requireAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(req.GetId())
|
||||
if id == "" {
|
||||
return nil, status.Error(codes.NotFound, "User not found")
|
||||
}
|
||||
if req.ClearReferrer != nil && req.GetClearReferrer() && req.RefUsername != nil && strings.TrimSpace(req.GetRefUsername()) != "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Cannot set and clear referrer at the same time")
|
||||
}
|
||||
if req.ClearReferralRewardBps != nil && req.GetClearReferralRewardBps() && req.ReferralRewardBps != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "Cannot set and clear referral reward override at the same time")
|
||||
}
|
||||
if req.ReferralRewardBps != nil {
|
||||
bps := req.GetReferralRewardBps()
|
||||
if bps < 0 || bps > 10000 {
|
||||
return nil, status.Error(codes.InvalidArgument, "Referral reward bps must be between 0 and 10000")
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.NotFound, "User not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "Failed to update referral settings")
|
||||
}
|
||||
|
||||
updates := map[string]any{}
|
||||
if req.RefUsername != nil || (req.ClearReferrer != nil && req.GetClearReferrer()) {
|
||||
if referralRewardProcessed(&user) {
|
||||
return nil, status.Error(codes.InvalidArgument, "Cannot change referrer after reward has been granted")
|
||||
}
|
||||
if req.ClearReferrer != nil && req.GetClearReferrer() {
|
||||
updates["referred_by_user_id"] = nil
|
||||
} else if req.RefUsername != nil {
|
||||
referrer, err := s.loadReferralUserByUsernameStrict(ctx, req.GetRefUsername())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if referrer.ID == user.ID {
|
||||
return nil, status.Error(codes.InvalidArgument, "User cannot refer themselves")
|
||||
}
|
||||
updates["referred_by_user_id"] = referrer.ID
|
||||
}
|
||||
}
|
||||
if req.ReferralEligible != nil {
|
||||
updates["referral_eligible"] = req.GetReferralEligible()
|
||||
}
|
||||
if req.ClearReferralRewardBps != nil && req.GetClearReferralRewardBps() {
|
||||
updates["referral_reward_bps"] = nil
|
||||
} else if req.ReferralRewardBps != nil {
|
||||
updates["referral_reward_bps"] = req.GetReferralRewardBps()
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
result := s.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to update referral settings")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, status.Error(codes.NotFound, "User not found")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to update referral settings")
|
||||
}
|
||||
var subscription *model.PlanSubscription
|
||||
var subscriptionRecord model.PlanSubscription
|
||||
if err := s.db.WithContext(ctx).Where("user_id = ?", id).Order("created_at DESC").First(&subscriptionRecord).Error; err == nil {
|
||||
subscription = &subscriptionRecord
|
||||
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.Internal, "Failed to update referral settings")
|
||||
}
|
||||
payload, err := s.buildAdminUserDetail(ctx, &user, subscription)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to update referral settings")
|
||||
}
|
||||
return &appv1.UpdateAdminUserReferralSettingsResponse{User: payload}, nil
|
||||
}
|
||||
func (s *appServices) UpdateAdminUserRole(ctx context.Context, req *appv1.UpdateAdminUserRoleRequest) (*appv1.UpdateAdminUserRoleResponse, error) {
|
||||
adminResult, err := s.requireAdmin(ctx)
|
||||
if err != nil {
|
||||
@@ -299,7 +382,6 @@ func (s *appServices) DeleteAdminUser(ctx context.Context, req *appv1.DeleteAdmi
|
||||
model interface{}
|
||||
where string
|
||||
}{
|
||||
{&model.VideoAdConfig{}, "user_id = ?"},
|
||||
{&model.AdTemplate{}, "user_id = ?"},
|
||||
{&model.Notification{}, "user_id = ?"},
|
||||
{&model.Domain{}, "user_id = ?"},
|
||||
@@ -395,6 +477,9 @@ func (s *appServices) CreateAdminVideo(ctx context.Context, req *appv1.CreateAdm
|
||||
if _, err := s.requireAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.videoService == nil {
|
||||
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
|
||||
}
|
||||
|
||||
userID := strings.TrimSpace(req.GetUserId())
|
||||
title := strings.TrimSpace(req.GetTitle())
|
||||
@@ -406,49 +491,30 @@ func (s *appServices) CreateAdminVideo(ctx context.Context, req *appv1.CreateAdm
|
||||
return nil, status.Error(codes.InvalidArgument, "Size must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.InvalidArgument, "User not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "Failed to create video")
|
||||
}
|
||||
|
||||
statusValue := normalizeVideoStatusValue(req.GetStatus())
|
||||
processingStatus := strings.ToUpper(statusValue)
|
||||
storageType := detectStorageType(videoURL)
|
||||
video := &model.Video{
|
||||
ID: uuid.New().String(),
|
||||
UserID: user.ID,
|
||||
Name: title,
|
||||
Title: title,
|
||||
Description: nullableTrimmedString(req.Description),
|
||||
URL: videoURL,
|
||||
Size: req.GetSize(),
|
||||
Duration: req.GetDuration(),
|
||||
Format: strings.TrimSpace(req.GetFormat()),
|
||||
Status: model.StringPtr(statusValue),
|
||||
ProcessingStatus: model.StringPtr(processingStatus),
|
||||
StorageType: model.StringPtr(storageType),
|
||||
}
|
||||
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(video).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return s.saveAdminVideoAdConfig(ctx, tx, video.ID, user.ID, nullableTrimmedString(req.AdTemplateId))
|
||||
created, err := s.videoService.CreateVideo(ctx, video.CreateVideoInput{
|
||||
UserID: userID,
|
||||
Title: title,
|
||||
Description: req.Description,
|
||||
URL: videoURL,
|
||||
Size: req.GetSize(),
|
||||
Duration: req.GetDuration(),
|
||||
Format: strings.TrimSpace(req.GetFormat()),
|
||||
AdTemplateID: nullableTrimmedString(req.AdTemplateId),
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Ad template not found") {
|
||||
switch {
|
||||
case errors.Is(err, video.ErrUserNotFound):
|
||||
return nil, status.Error(codes.InvalidArgument, "User not found")
|
||||
case errors.Is(err, video.ErrAdTemplateNotFound):
|
||||
return nil, status.Error(codes.InvalidArgument, "Ad template not found")
|
||||
case errors.Is(err, video.ErrJobServiceUnavailable):
|
||||
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
|
||||
default:
|
||||
return nil, status.Error(codes.Internal, "Failed to create video")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "Failed to create video")
|
||||
}
|
||||
|
||||
payload, err := s.buildAdminVideo(ctx, video)
|
||||
payload, err := s.buildAdminVideo(ctx, created.Video)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to create video")
|
||||
}
|
||||
@@ -524,12 +590,7 @@ func (s *appServices) UpdateAdminVideo(ctx context.Context, req *appv1.UpdateAdm
|
||||
return err
|
||||
}
|
||||
}
|
||||
if oldUserID != user.ID {
|
||||
if err := tx.Model(&model.VideoAdConfig{}).Where("video_id = ?", video.ID).Update("user_id", user.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.saveAdminVideoAdConfig(ctx, tx, video.ID, user.ID, nullableTrimmedString(req.AdTemplateId))
|
||||
return s.saveAdminVideoAdConfig(ctx, tx, &video, user.ID, nullableTrimmedString(req.AdTemplateId))
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Ad template not found") {
|
||||
@@ -563,9 +624,6 @@ func (s *appServices) DeleteAdminVideo(ctx context.Context, req *appv1.DeleteAdm
|
||||
}
|
||||
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("video_id = ?", video.ID).Delete(&model.VideoAdConfig{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("id = ?", video.ID).Delete(&model.Video{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user