feat: Implement video workflow repository and related services

- Added videoWorkflowRepository with methods to manage video and user interactions.
- Introduced catalog_mapper for converting database models to protobuf representations.
- Created domain_helpers for normalizing domain and ad format values.
- Defined service interfaces for payment, account, notification, domain, ad template, player config, video, and user management.
- Implemented OAuth helpers for generating state and caching keys.
- Developed payment_proto_helpers for mapping payment-related models to protobuf.
- Added service policy helpers to enforce plan requirements and user permissions.
- Created user_mapper for converting user payloads to protobuf format.
- Implemented value_helpers for handling various value conversions and nil checks.
- Developed video_helpers for normalizing video statuses and managing storage types.
- Created video_mapper for mapping video models to protobuf format.
- Implemented render workflow for managing video creation and job processing.
This commit is contained in:
2026-03-26 18:38:47 +07:00
parent fbbecd7674
commit a0ae2b681a
55 changed files with 3464 additions and 13091 deletions

View File

@@ -19,25 +19,11 @@ func (s *appServices) ListAdminPayments(ctx context.Context, req *appv1.ListAdmi
}
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
limitInt := int(limit)
userID := strings.TrimSpace(req.GetUserId())
statusFilter := strings.TrimSpace(req.GetStatus())
db := s.db.WithContext(ctx).Model(&model.Payment{})
if userID != "" {
db = db.Where("user_id = ?", userID)
}
if statusFilter != "" {
db = db.Where("UPPER(status) = ?", strings.ToUpper(statusFilter))
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list payments")
}
var payments []model.Payment
if err := db.Order("created_at DESC").Offset(offset).Limit(limitInt).Find(&payments).Error; err != nil {
payments, total, err := s.paymentRepository.ListForAdmin(ctx, userID, statusFilter, limit, offset)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list payments")
}
@@ -62,15 +48,15 @@ func (s *appServices) GetAdminPayment(ctx context.Context, req *appv1.GetAdminPa
return nil, status.Error(codes.NotFound, "Payment not found")
}
var payment model.Payment
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&payment).Error; err != nil {
payment, err := s.paymentRepository.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Payment not found")
}
return nil, status.Error(codes.Internal, "Failed to get payment")
}
payload, err := s.buildAdminPayment(ctx, &payment)
payload, err := s.buildAdminPayment(ctx, payment)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to get payment")
}
@@ -148,8 +134,8 @@ func (s *appServices) UpdateAdminPayment(ctx context.Context, req *appv1.UpdateA
return nil, status.Error(codes.InvalidArgument, "Invalid payment status")
}
var payment model.Payment
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&payment).Error; err != nil {
payment, err := s.paymentRepository.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Payment not found")
}
@@ -162,12 +148,12 @@ func (s *appServices) UpdateAdminPayment(ctx context.Context, req *appv1.UpdateA
return nil, status.Error(codes.InvalidArgument, "Cannot transition payment to SUCCESS from admin update; recreate through the payment flow instead")
}
payment.Status = model.StringPtr(newStatus)
if err := s.db.WithContext(ctx).Save(&payment).Error; err != nil {
if err := s.paymentRepository.Save(ctx, payment); err != nil {
return nil, status.Error(codes.Internal, "Failed to update payment")
}
}
payload, err := s.buildAdminPayment(ctx, &payment)
payload, err := s.buildAdminPayment(ctx, payment)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to update payment")
}
@@ -178,8 +164,8 @@ func (s *appServices) ListAdminPlans(ctx context.Context, _ *appv1.ListAdminPlan
return nil, err
}
var plans []model.Plan
if err := s.db.WithContext(ctx).Order("price ASC").Find(&plans).Error; err != nil {
plans, err := s.planRepository.ListAll(ctx)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list plans")
}
@@ -216,7 +202,7 @@ func (s *appServices) CreateAdminPlan(ctx context.Context, req *appv1.CreateAdmi
IsActive: model.BoolPtr(req.GetIsActive()),
}
if err := s.db.WithContext(ctx).Create(plan).Error; err != nil {
if err := s.planRepository.Create(ctx, plan); err != nil {
return nil, status.Error(codes.Internal, "Failed to create plan")
}
@@ -239,8 +225,8 @@ func (s *appServices) UpdateAdminPlan(ctx context.Context, req *appv1.UpdateAdmi
return nil, status.Error(codes.InvalidArgument, msg)
}
var plan model.Plan
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
plan, err := s.planRepository.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Plan not found")
}
@@ -256,11 +242,11 @@ func (s *appServices) UpdateAdminPlan(ctx context.Context, req *appv1.UpdateAdmi
plan.UploadLimit = req.GetUploadLimit()
plan.IsActive = model.BoolPtr(req.GetIsActive())
if err := s.db.WithContext(ctx).Save(&plan).Error; err != nil {
if err := s.planRepository.Save(ctx, plan); err != nil {
return nil, status.Error(codes.Internal, "Failed to update plan")
}
payload, err := s.buildAdminPlan(ctx, &plan)
payload, err := s.buildAdminPlan(ctx, plan)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to update plan")
}
@@ -276,32 +262,32 @@ func (s *appServices) DeleteAdminPlan(ctx context.Context, req *appv1.DeleteAdmi
return nil, status.Error(codes.NotFound, "Plan not found")
}
var plan model.Plan
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
_, err := s.planRepository.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Plan not found")
}
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
var paymentCount int64
if err := s.db.WithContext(ctx).Model(&model.Payment{}).Where("plan_id = ?", id).Count(&paymentCount).Error; err != nil {
paymentCount, err := s.planRepository.CountPaymentsByPlan(ctx, id)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
var subscriptionCount int64
if err := s.db.WithContext(ctx).Model(&model.PlanSubscription{}).Where("plan_id = ?", id).Count(&subscriptionCount).Error; err != nil {
subscriptionCount, err := s.planRepository.CountSubscriptionsByPlan(ctx, id)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
if paymentCount > 0 || subscriptionCount > 0 {
inactive := false
if err := s.db.WithContext(ctx).Model(&model.Plan{}).Where("id = ?", id).Update("is_active", inactive).Error; err != nil {
if err := s.planRepository.SetActive(ctx, id, inactive); err != nil {
return nil, status.Error(codes.Internal, "Failed to deactivate plan")
}
return &appv1.DeleteAdminPlanResponse{Message: "Plan deactivated", Mode: "deactivated"}, nil
}
if err := s.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Plan{}).Error; err != nil {
if err := s.planRepository.DeleteByID(ctx, id); err != nil {
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
return &appv1.DeleteAdminPlanResponse{Message: "Plan deleted", Mode: "deleted"}, nil
@@ -312,26 +298,11 @@ func (s *appServices) ListAdminAdTemplates(ctx context.Context, req *appv1.ListA
}
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
limitInt := int(limit)
search := strings.TrimSpace(protoStringValue(req.Search))
userID := strings.TrimSpace(protoStringValue(req.UserId))
db := s.db.WithContext(ctx).Model(&model.AdTemplate{})
if search != "" {
like := "%" + search + "%"
db = db.Where("name ILIKE ?", like)
}
if userID != "" {
db = db.Where("user_id = ?", userID)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list ad templates")
}
var templates []model.AdTemplate
if err := db.Order("created_at DESC").Offset(offset).Limit(limitInt).Find(&templates).Error; err != nil {
templates, total, err := s.adTemplateRepository.ListForAdmin(ctx, search, userID, limit, offset)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list ad templates")
}
@@ -361,15 +332,15 @@ func (s *appServices) GetAdminAdTemplate(ctx context.Context, req *appv1.GetAdmi
return nil, status.Error(codes.NotFound, "Ad template not found")
}
var item model.AdTemplate
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
item, err := s.adTemplateRepository.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
return nil, status.Error(codes.Internal, "Failed to load ad template")
}
payload, err := s.buildAdminAdTemplate(ctx, &item)
payload, err := s.buildAdminAdTemplate(ctx, item)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to load ad template")
}
@@ -385,8 +356,8 @@ func (s *appServices) CreateAdminAdTemplate(ctx context.Context, req *appv1.Crea
return nil, status.Error(codes.InvalidArgument, msg)
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.GetUserId())).First(&user).Error; err != nil {
user, err := s.userRepository.GetByID(ctx, strings.TrimSpace(req.GetUserId()))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.InvalidArgument, "User not found")
}
@@ -408,14 +379,7 @@ func (s *appServices) CreateAdminAdTemplate(ctx context.Context, req *appv1.Crea
item.IsDefault = false
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := s.unsetAdminDefaultTemplates(ctx, tx, item.UserID, ""); err != nil {
return err
}
}
return tx.Create(item).Error
}); err != nil {
if err := s.adTemplateRepository.CreateWithDefault(ctx, item.UserID, item); err != nil {
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
@@ -439,16 +403,16 @@ func (s *appServices) UpdateAdminAdTemplate(ctx context.Context, req *appv1.Upda
return nil, status.Error(codes.InvalidArgument, msg)
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.GetUserId())).First(&user).Error; err != nil {
user, err := s.userRepository.GetByID(ctx, strings.TrimSpace(req.GetUserId()))
if 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 save ad template")
}
var item model.AdTemplate
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
item, err := s.adTemplateRepository.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
@@ -467,18 +431,11 @@ func (s *appServices) UpdateAdminAdTemplate(ctx context.Context, req *appv1.Upda
item.IsDefault = false
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := s.unsetAdminDefaultTemplates(ctx, tx, item.UserID, item.ID); err != nil {
return err
}
}
return tx.Save(&item).Error
}); err != nil {
if err := s.adTemplateRepository.SaveWithDefault(ctx, item.UserID, item); err != nil {
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
payload, err := s.buildAdminAdTemplate(ctx, &item)
payload, err := s.buildAdminAdTemplate(ctx, item)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
@@ -494,19 +451,7 @@ func (s *appServices) DeleteAdminAdTemplate(ctx context.Context, req *appv1.Dele
return nil, status.Error(codes.NotFound, "Ad template not found")
}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.Video{}).Where("ad_id = ?", id).Update("ad_id", nil).Error; err != nil {
return err
}
res := tx.Where("id = ?", id).Delete(&model.AdTemplate{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
err := s.adTemplateRepository.DeleteByIDAndClearVideos(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Ad template not found")
@@ -522,26 +467,11 @@ func (s *appServices) ListAdminPlayerConfigs(ctx context.Context, req *appv1.Lis
}
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
limitInt := int(limit)
search := strings.TrimSpace(protoStringValue(req.Search))
userID := strings.TrimSpace(protoStringValue(req.UserId))
db := s.db.WithContext(ctx).Model(&model.PlayerConfig{})
if search != "" {
like := "%" + search + "%"
db = db.Where("name ILIKE ?", like)
}
if userID != "" {
db = db.Where("user_id = ?", userID)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list player configs")
}
var configs []model.PlayerConfig
if err := db.Order("created_at DESC").Offset(offset).Limit(limitInt).Find(&configs).Error; err != nil {
configs, total, err := s.playerConfigRepo.ListForAdmin(ctx, search, userID, limit, offset)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list player configs")
}
@@ -572,15 +502,15 @@ func (s *appServices) GetAdminPlayerConfig(ctx context.Context, req *appv1.GetAd
return nil, status.Error(codes.NotFound, "Player config not found")
}
var item model.PlayerConfig
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
item, err := s.playerConfigRepo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Player config not found")
}
return nil, status.Error(codes.Internal, "Failed to load player config")
}
payload, err := s.buildAdminPlayerConfig(ctx, &item)
payload, err := s.buildAdminPlayerConfig(ctx, item)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to load player config")
}
@@ -596,8 +526,8 @@ func (s *appServices) CreateAdminPlayerConfig(ctx context.Context, req *appv1.Cr
return nil, status.Error(codes.InvalidArgument, msg)
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.GetUserId())).First(&user).Error; err != nil {
user, err := s.userRepository.GetByID(ctx, strings.TrimSpace(req.GetUserId()))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.InvalidArgument, "User not found")
}
@@ -625,14 +555,7 @@ func (s *appServices) CreateAdminPlayerConfig(ctx context.Context, req *appv1.Cr
item.IsDefault = false
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := s.unsetAdminDefaultPlayerConfigs(ctx, tx, item.UserID, ""); err != nil {
return err
}
}
return tx.Create(item).Error
}); err != nil {
if err := s.playerConfigRepo.CreateWithDefault(ctx, item.UserID, item); err != nil {
return nil, status.Error(codes.Internal, "Failed to save player config")
}
@@ -657,16 +580,16 @@ func (s *appServices) UpdateAdminPlayerConfig(ctx context.Context, req *appv1.Up
return nil, status.Error(codes.InvalidArgument, msg)
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.GetUserId())).First(&user).Error; err != nil {
user, err := s.userRepository.GetByID(ctx, strings.TrimSpace(req.GetUserId()))
if 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 save player config")
}
var item model.PlayerConfig
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
item, err := s.playerConfigRepo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Player config not found")
}
@@ -695,18 +618,11 @@ func (s *appServices) UpdateAdminPlayerConfig(ctx context.Context, req *appv1.Up
item.IsDefault = false
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := s.unsetAdminDefaultPlayerConfigs(ctx, tx, item.UserID, item.ID); err != nil {
return err
}
}
return tx.Save(&item).Error
}); err != nil {
if err := s.playerConfigRepo.SaveWithDefault(ctx, item.UserID, item); err != nil {
return nil, status.Error(codes.Internal, "Failed to save player config")
}
payload, err := s.buildAdminPlayerConfig(ctx, &item)
payload, err := s.buildAdminPlayerConfig(ctx, item)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to save player config")
}
@@ -723,11 +639,11 @@ func (s *appServices) DeleteAdminPlayerConfig(ctx context.Context, req *appv1.De
return nil, status.Error(codes.NotFound, "Player config not found")
}
res := s.db.WithContext(ctx).Where("id = ?", id).Delete(&model.PlayerConfig{})
if res.Error != nil {
rowsAffected, err := s.playerConfigRepo.DeleteByID(ctx, id)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to delete player config")
}
if res.RowsAffected == 0 {
if rowsAffected == 0 {
return nil, status.Error(codes.NotFound, "Player config not found")
}