- Implemented notification event publishing with a new NotificationEventPublisher interface. - Created a noopNotificationEventPublisher for testing purposes. - Added functionality to publish notification created events via MQTT. - Introduced a new stream event publisher for handling job logs and updates. - Added database migration for popup_ads table. - Created tests for notification events and popup ads functionality. - Established MQTT connection and publishing helpers for event messages.
796 lines
28 KiB
Go
796 lines
28 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"gorm.io/gorm"
|
|
appv1 "stream.api/internal/api/proto/app/v1"
|
|
"stream.api/internal/database/model"
|
|
)
|
|
|
|
func (s *appServices) ListAdminPayments(ctx context.Context, req *appv1.ListAdminPaymentsRequest) (*appv1.ListAdminPaymentsResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
|
|
userID := strings.TrimSpace(req.GetUserId())
|
|
statusFilter := strings.TrimSpace(req.GetStatus())
|
|
|
|
payments, total, err := s.paymentRepository.ListForAdmin(ctx, userID, statusFilter, limit, offset)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to list payments")
|
|
}
|
|
|
|
items := make([]*appv1.AdminPayment, 0, len(payments))
|
|
for _, payment := range payments {
|
|
payload, err := s.buildAdminPayment(ctx, &payment)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to list payments")
|
|
}
|
|
items = append(items, payload)
|
|
}
|
|
|
|
return &appv1.ListAdminPaymentsResponse{Payments: items, Total: total, Page: page, Limit: limit}, nil
|
|
}
|
|
func (s *appServices) GetAdminPayment(ctx context.Context, req *appv1.GetAdminPaymentRequest) (*appv1.GetAdminPaymentResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Payment not found")
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to get payment")
|
|
}
|
|
|
|
return &appv1.GetAdminPaymentResponse{Payment: payload}, nil
|
|
}
|
|
func (s *appServices) CreateAdminPayment(ctx context.Context, req *appv1.CreateAdminPaymentRequest) (*appv1.CreateAdminPaymentResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userID := strings.TrimSpace(req.GetUserId())
|
|
planID := strings.TrimSpace(req.GetPlanId())
|
|
if userID == "" || planID == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "User ID and plan ID are required")
|
|
}
|
|
if !isAllowedTermMonths(req.GetTermMonths()) {
|
|
return nil, status.Error(codes.InvalidArgument, "Term months must be one of 1, 3, 6, or 12")
|
|
}
|
|
|
|
paymentMethod := normalizePaymentMethod(req.GetPaymentMethod())
|
|
if paymentMethod == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "Payment method must be wallet or topup")
|
|
}
|
|
|
|
user, err := s.loadPaymentUserForAdmin(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
planRecord, err := s.loadPaymentPlanForAdmin(ctx, planID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resultValue, err := s.executePaymentFlow(ctx, paymentExecutionInput{
|
|
UserID: user.ID,
|
|
Plan: planRecord,
|
|
TermMonths: req.GetTermMonths(),
|
|
PaymentMethod: paymentMethod,
|
|
TopupAmount: req.TopupAmount,
|
|
})
|
|
if err != nil {
|
|
if _, ok := status.FromError(err); ok {
|
|
return nil, err
|
|
}
|
|
return nil, status.Error(codes.Internal, "Failed to create payment")
|
|
}
|
|
|
|
payload, err := s.buildAdminPayment(ctx, resultValue.Payment)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to create payment")
|
|
}
|
|
return &appv1.CreateAdminPaymentResponse{
|
|
Payment: payload,
|
|
Subscription: toProtoPlanSubscription(resultValue.Subscription),
|
|
WalletBalance: resultValue.WalletBalance,
|
|
InvoiceId: resultValue.InvoiceID,
|
|
}, nil
|
|
}
|
|
func (s *appServices) UpdateAdminPayment(ctx context.Context, req *appv1.UpdateAdminPaymentRequest) (*appv1.UpdateAdminPaymentResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Payment not found")
|
|
}
|
|
|
|
newStatus := strings.ToUpper(strings.TrimSpace(req.GetStatus()))
|
|
if newStatus == "" {
|
|
newStatus = "SUCCESS"
|
|
}
|
|
if newStatus != "SUCCESS" && newStatus != "FAILED" && newStatus != "PENDING" {
|
|
return nil, status.Error(codes.InvalidArgument, "Invalid payment status")
|
|
}
|
|
|
|
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 update payment")
|
|
}
|
|
|
|
currentStatus := strings.ToUpper(normalizePaymentStatus(payment.Status))
|
|
if currentStatus != newStatus {
|
|
if (currentStatus == "FAILED" || currentStatus == "PENDING") && newStatus == "SUCCESS" {
|
|
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.paymentRepository.Save(ctx, payment); err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to update payment")
|
|
}
|
|
}
|
|
|
|
payload, err := s.buildAdminPayment(ctx, payment)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to update payment")
|
|
}
|
|
return &appv1.UpdateAdminPaymentResponse{Payment: payload}, nil
|
|
}
|
|
func (s *appServices) ListAdminPlans(ctx context.Context, _ *appv1.ListAdminPlansRequest) (*appv1.ListAdminPlansResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
plans, err := s.planRepository.ListAll(ctx)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to list plans")
|
|
}
|
|
|
|
items := make([]*appv1.AdminPlan, 0, len(plans))
|
|
for i := range plans {
|
|
payload, err := s.buildAdminPlan(ctx, &plans[i])
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to list plans")
|
|
}
|
|
items = append(items, payload)
|
|
}
|
|
return &appv1.ListAdminPlansResponse{Plans: items}, nil
|
|
}
|
|
func (s *appServices) CreateAdminPlan(ctx context.Context, req *appv1.CreateAdminPlanRequest) (*appv1.CreateAdminPlanResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if msg := validateAdminPlanInput(req.GetName(), req.GetCycle(), req.GetPrice(), req.GetStorageLimit(), req.GetUploadLimit()); msg != "" {
|
|
return nil, status.Error(codes.InvalidArgument, msg)
|
|
}
|
|
|
|
plan := &model.Plan{
|
|
ID: uuid.New().String(),
|
|
Name: strings.TrimSpace(req.GetName()),
|
|
Description: nullableTrimmedStringPtr(req.Description),
|
|
Features: append([]string(nil), req.GetFeatures()...),
|
|
Price: req.GetPrice(),
|
|
Cycle: strings.TrimSpace(req.GetCycle()),
|
|
StorageLimit: req.GetStorageLimit(),
|
|
UploadLimit: req.GetUploadLimit(),
|
|
DurationLimit: 0,
|
|
QualityLimit: "",
|
|
IsActive: model.BoolPtr(req.GetIsActive()),
|
|
}
|
|
|
|
if err := s.planRepository.Create(ctx, plan); err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to create plan")
|
|
}
|
|
|
|
payload, err := s.buildAdminPlan(ctx, plan)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to create plan")
|
|
}
|
|
return &appv1.CreateAdminPlanResponse{Plan: payload}, nil
|
|
}
|
|
func (s *appServices) UpdateAdminPlan(ctx context.Context, req *appv1.UpdateAdminPlanRequest) (*appv1.UpdateAdminPlanResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Plan not found")
|
|
}
|
|
if msg := validateAdminPlanInput(req.GetName(), req.GetCycle(), req.GetPrice(), req.GetStorageLimit(), req.GetUploadLimit()); msg != "" {
|
|
return nil, status.Error(codes.InvalidArgument, msg)
|
|
}
|
|
|
|
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")
|
|
}
|
|
return nil, status.Error(codes.Internal, "Failed to update plan")
|
|
}
|
|
|
|
plan.Name = strings.TrimSpace(req.GetName())
|
|
plan.Description = nullableTrimmedStringPtr(req.Description)
|
|
plan.Features = append([]string(nil), req.GetFeatures()...)
|
|
plan.Price = req.GetPrice()
|
|
plan.Cycle = strings.TrimSpace(req.GetCycle())
|
|
plan.StorageLimit = req.GetStorageLimit()
|
|
plan.UploadLimit = req.GetUploadLimit()
|
|
plan.IsActive = model.BoolPtr(req.GetIsActive())
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to update plan")
|
|
}
|
|
return &appv1.UpdateAdminPlanResponse{Plan: payload}, nil
|
|
}
|
|
func (s *appServices) DeleteAdminPlan(ctx context.Context, req *appv1.DeleteAdminPlanRequest) (*appv1.DeleteAdminPlanResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Plan not found")
|
|
}
|
|
|
|
_, 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")
|
|
}
|
|
|
|
paymentCount, err := s.planRepository.CountPaymentsByPlan(ctx, id)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to delete plan")
|
|
}
|
|
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.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.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
|
|
}
|
|
func (s *appServices) ListAdminAdTemplates(ctx context.Context, req *appv1.ListAdminAdTemplatesRequest) (*appv1.ListAdminAdTemplatesResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
|
|
search := strings.TrimSpace(protoStringValue(req.Search))
|
|
userID := strings.TrimSpace(protoStringValue(req.UserId))
|
|
|
|
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")
|
|
}
|
|
|
|
items := make([]*appv1.AdminAdTemplate, 0, len(templates))
|
|
for i := range templates {
|
|
payload, err := s.buildAdminAdTemplate(ctx, &templates[i])
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to list ad templates")
|
|
}
|
|
items = append(items, payload)
|
|
}
|
|
|
|
return &appv1.ListAdminAdTemplatesResponse{
|
|
Templates: items,
|
|
Total: total,
|
|
Page: page,
|
|
Limit: limit,
|
|
}, nil
|
|
}
|
|
func (s *appServices) GetAdminAdTemplate(ctx context.Context, req *appv1.GetAdminAdTemplateRequest) (*appv1.GetAdminAdTemplateResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Ad template not found")
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to load ad template")
|
|
}
|
|
return &appv1.GetAdminAdTemplateResponse{Template: payload}, nil
|
|
}
|
|
func (s *appServices) CreateAdminAdTemplate(ctx context.Context, req *appv1.CreateAdminAdTemplateRequest) (*appv1.CreateAdminAdTemplateResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
duration := req.Duration
|
|
if msg := validateAdminAdTemplateInput(req.GetUserId(), req.GetName(), req.GetVastTagUrl(), req.GetAdFormat(), duration); msg != "" {
|
|
return nil, status.Error(codes.InvalidArgument, msg)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
item := &model.AdTemplate{
|
|
ID: uuid.New().String(),
|
|
UserID: user.ID,
|
|
Name: strings.TrimSpace(req.GetName()),
|
|
Description: nullableTrimmedStringPtr(req.Description),
|
|
VastTagURL: strings.TrimSpace(req.GetVastTagUrl()),
|
|
AdFormat: model.StringPtr(normalizeAdFormat(req.GetAdFormat())),
|
|
Duration: duration,
|
|
IsActive: model.BoolPtr(req.GetIsActive()),
|
|
IsDefault: req.GetIsDefault(),
|
|
}
|
|
if !boolValue(item.IsActive) {
|
|
item.IsDefault = false
|
|
}
|
|
|
|
if err := s.adTemplateRepository.CreateWithDefault(ctx, item.UserID, item); err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save ad template")
|
|
}
|
|
|
|
payload, err := s.buildAdminAdTemplate(ctx, item)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save ad template")
|
|
}
|
|
return &appv1.CreateAdminAdTemplateResponse{Template: payload}, nil
|
|
}
|
|
func (s *appServices) UpdateAdminAdTemplate(ctx context.Context, req *appv1.UpdateAdminAdTemplateRequest) (*appv1.UpdateAdminAdTemplateResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Ad template not found")
|
|
}
|
|
duration := req.Duration
|
|
if msg := validateAdminAdTemplateInput(req.GetUserId(), req.GetName(), req.GetVastTagUrl(), req.GetAdFormat(), duration); msg != "" {
|
|
return nil, status.Error(codes.InvalidArgument, msg)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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 save ad template")
|
|
}
|
|
|
|
item.UserID = user.ID
|
|
item.Name = strings.TrimSpace(req.GetName())
|
|
item.Description = nullableTrimmedStringPtr(req.Description)
|
|
item.VastTagURL = strings.TrimSpace(req.GetVastTagUrl())
|
|
item.AdFormat = model.StringPtr(normalizeAdFormat(req.GetAdFormat()))
|
|
item.Duration = duration
|
|
item.IsActive = model.BoolPtr(req.GetIsActive())
|
|
item.IsDefault = req.GetIsDefault()
|
|
if !boolValue(item.IsActive) {
|
|
item.IsDefault = false
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save ad template")
|
|
}
|
|
return &appv1.UpdateAdminAdTemplateResponse{Template: payload}, nil
|
|
}
|
|
func (s *appServices) DeleteAdminAdTemplate(ctx context.Context, req *appv1.DeleteAdminAdTemplateRequest) (*appv1.MessageResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Ad template not found")
|
|
}
|
|
|
|
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")
|
|
}
|
|
return nil, status.Error(codes.Internal, "Failed to delete ad template")
|
|
}
|
|
return &appv1.MessageResponse{Message: "Ad template deleted"}, nil
|
|
}
|
|
|
|
func (s *appServices) ListAdminPopupAds(ctx context.Context, req *appv1.ListAdminPopupAdsRequest) (*appv1.ListAdminPopupAdsResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
|
|
search := strings.TrimSpace(protoStringValue(req.Search))
|
|
userID := strings.TrimSpace(protoStringValue(req.UserId))
|
|
|
|
items, total, err := s.popupAdRepository.ListForAdmin(ctx, search, userID, limit, offset)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to list popup ads")
|
|
}
|
|
|
|
payload := make([]*appv1.AdminPopupAd, 0, len(items))
|
|
for i := range items {
|
|
mapped, err := s.buildAdminPopupAd(ctx, &items[i])
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to list popup ads")
|
|
}
|
|
payload = append(payload, mapped)
|
|
}
|
|
|
|
return &appv1.ListAdminPopupAdsResponse{Items: payload, Total: total, Page: page, Limit: limit}, nil
|
|
}
|
|
func (s *appServices) GetAdminPopupAd(ctx context.Context, req *appv1.GetAdminPopupAdRequest) (*appv1.GetAdminPopupAdResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Popup ad not found")
|
|
}
|
|
item, err := s.popupAdRepository.GetByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, status.Error(codes.NotFound, "Popup ad not found")
|
|
}
|
|
return nil, status.Error(codes.Internal, "Failed to load popup ad")
|
|
}
|
|
payload, err := s.buildAdminPopupAd(ctx, item)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to load popup ad")
|
|
}
|
|
return &appv1.GetAdminPopupAdResponse{Item: payload}, nil
|
|
}
|
|
func (s *appServices) CreateAdminPopupAd(ctx context.Context, req *appv1.CreateAdminPopupAdRequest) (*appv1.CreateAdminPopupAdResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if msg := validateAdminPopupAdInput(req.GetUserId(), req.GetType(), req.GetLabel(), req.GetValue(), req.MaxTriggersPerSession); msg != "" {
|
|
return nil, status.Error(codes.InvalidArgument, msg)
|
|
}
|
|
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 popup ad")
|
|
}
|
|
maxTriggers := int32(3)
|
|
if req.MaxTriggersPerSession != nil {
|
|
maxTriggers = *req.MaxTriggersPerSession
|
|
}
|
|
item := &model.PopupAd{
|
|
ID: uuid.New().String(),
|
|
UserID: user.ID,
|
|
Type: strings.ToLower(strings.TrimSpace(req.GetType())),
|
|
Label: strings.TrimSpace(req.GetLabel()),
|
|
Value: strings.TrimSpace(req.GetValue()),
|
|
IsActive: model.BoolPtr(req.IsActive == nil || *req.IsActive),
|
|
MaxTriggersPerSession: int32Ptr(maxTriggers),
|
|
}
|
|
if err := s.popupAdRepository.Create(ctx, item); err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save popup ad")
|
|
}
|
|
payload, err := s.buildAdminPopupAd(ctx, item)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save popup ad")
|
|
}
|
|
return &appv1.CreateAdminPopupAdResponse{Item: payload}, nil
|
|
}
|
|
func (s *appServices) UpdateAdminPopupAd(ctx context.Context, req *appv1.UpdateAdminPopupAdRequest) (*appv1.UpdateAdminPopupAdResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Popup ad not found")
|
|
}
|
|
if msg := validateAdminPopupAdInput(req.GetUserId(), req.GetType(), req.GetLabel(), req.GetValue(), req.MaxTriggersPerSession); msg != "" {
|
|
return nil, status.Error(codes.InvalidArgument, msg)
|
|
}
|
|
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 popup ad")
|
|
}
|
|
item, err := s.popupAdRepository.GetByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, status.Error(codes.NotFound, "Popup ad not found")
|
|
}
|
|
return nil, status.Error(codes.Internal, "Failed to save popup ad")
|
|
}
|
|
item.UserID = user.ID
|
|
item.Type = strings.ToLower(strings.TrimSpace(req.GetType()))
|
|
item.Label = strings.TrimSpace(req.GetLabel())
|
|
item.Value = strings.TrimSpace(req.GetValue())
|
|
if req.IsActive != nil {
|
|
item.IsActive = model.BoolPtr(*req.IsActive)
|
|
}
|
|
if req.MaxTriggersPerSession != nil {
|
|
item.MaxTriggersPerSession = int32Ptr(*req.MaxTriggersPerSession)
|
|
}
|
|
if err := s.popupAdRepository.Save(ctx, item); err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save popup ad")
|
|
}
|
|
payload, err := s.buildAdminPopupAd(ctx, item)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save popup ad")
|
|
}
|
|
return &appv1.UpdateAdminPopupAdResponse{Item: payload}, nil
|
|
}
|
|
func (s *appServices) DeleteAdminPopupAd(ctx context.Context, req *appv1.DeleteAdminPopupAdRequest) (*appv1.MessageResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Popup ad not found")
|
|
}
|
|
rowsAffected, err := s.popupAdRepository.DeleteByID(ctx, id)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to delete popup ad")
|
|
}
|
|
if rowsAffected == 0 {
|
|
return nil, status.Error(codes.NotFound, "Popup ad not found")
|
|
}
|
|
return &appv1.MessageResponse{Message: "Popup ad deleted"}, nil
|
|
}
|
|
|
|
func (s *appServices) ListAdminPlayerConfigs(ctx context.Context, req *appv1.ListAdminPlayerConfigsRequest) (*appv1.ListAdminPlayerConfigsResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
|
|
search := strings.TrimSpace(protoStringValue(req.Search))
|
|
userID := strings.TrimSpace(protoStringValue(req.UserId))
|
|
|
|
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")
|
|
}
|
|
|
|
items := make([]*appv1.AdminPlayerConfig, 0, len(configs))
|
|
for i := range configs {
|
|
payload, err := s.buildAdminPlayerConfig(ctx, &configs[i])
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to list player configs")
|
|
}
|
|
items = append(items, payload)
|
|
}
|
|
|
|
return &appv1.ListAdminPlayerConfigsResponse{
|
|
Configs: items,
|
|
Total: total,
|
|
Page: page,
|
|
Limit: limit,
|
|
}, nil
|
|
}
|
|
|
|
func (s *appServices) GetAdminPlayerConfig(ctx context.Context, req *appv1.GetAdminPlayerConfigRequest) (*appv1.GetAdminPlayerConfigResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Player config not found")
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to load player config")
|
|
}
|
|
return &appv1.GetAdminPlayerConfigResponse{Config: payload}, nil
|
|
}
|
|
|
|
func (s *appServices) CreateAdminPlayerConfig(ctx context.Context, req *appv1.CreateAdminPlayerConfigRequest) (*appv1.CreateAdminPlayerConfigResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if msg := validateAdminPlayerConfigInput(req.GetUserId(), req.GetName()); msg != "" {
|
|
return nil, status.Error(codes.InvalidArgument, msg)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
item := &model.PlayerConfig{
|
|
ID: uuid.New().String(),
|
|
UserID: user.ID,
|
|
Name: strings.TrimSpace(req.GetName()),
|
|
Description: nullableTrimmedStringPtr(req.Description),
|
|
Autoplay: req.GetAutoplay(),
|
|
Loop: req.GetLoop(),
|
|
Muted: req.GetMuted(),
|
|
ShowControls: model.BoolPtr(req.GetShowControls()),
|
|
Pip: model.BoolPtr(req.GetPip()),
|
|
Airplay: model.BoolPtr(req.GetAirplay()),
|
|
Chromecast: model.BoolPtr(req.GetChromecast()),
|
|
IsActive: model.BoolPtr(req.GetIsActive()),
|
|
IsDefault: req.GetIsDefault(),
|
|
EncrytionM3u8: model.BoolPtr(req.EncrytionM3U8 == nil || *req.EncrytionM3U8),
|
|
LogoURL: nullableTrimmedStringPtr(req.LogoUrl),
|
|
}
|
|
if !boolValue(item.IsActive) {
|
|
item.IsDefault = false
|
|
}
|
|
|
|
if err := s.playerConfigRepo.CreateWithDefault(ctx, item.UserID, item); err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save player config")
|
|
}
|
|
|
|
payload, err := s.buildAdminPlayerConfig(ctx, item)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save player config")
|
|
}
|
|
return &appv1.CreateAdminPlayerConfigResponse{Config: payload}, nil
|
|
}
|
|
|
|
func (s *appServices) UpdateAdminPlayerConfig(ctx context.Context, req *appv1.UpdateAdminPlayerConfigRequest) (*appv1.UpdateAdminPlayerConfigResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Player config not found")
|
|
}
|
|
|
|
if msg := validateAdminPlayerConfigInput(req.GetUserId(), req.GetName()); msg != "" {
|
|
return nil, status.Error(codes.InvalidArgument, msg)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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 save player config")
|
|
}
|
|
|
|
item.UserID = user.ID
|
|
item.Name = strings.TrimSpace(req.GetName())
|
|
item.Description = nullableTrimmedStringPtr(req.Description)
|
|
item.Autoplay = req.GetAutoplay()
|
|
item.Loop = req.GetLoop()
|
|
item.Muted = req.GetMuted()
|
|
item.ShowControls = model.BoolPtr(req.GetShowControls())
|
|
item.Pip = model.BoolPtr(req.GetPip())
|
|
item.Airplay = model.BoolPtr(req.GetAirplay())
|
|
item.Chromecast = model.BoolPtr(req.GetChromecast())
|
|
item.IsActive = model.BoolPtr(req.GetIsActive())
|
|
item.IsDefault = req.GetIsDefault()
|
|
if req.EncrytionM3U8 != nil {
|
|
item.EncrytionM3u8 = model.BoolPtr(*req.EncrytionM3U8)
|
|
}
|
|
if req.LogoUrl != nil {
|
|
item.LogoURL = nullableTrimmedStringPtr(req.LogoUrl)
|
|
}
|
|
if !boolValue(item.IsActive) {
|
|
item.IsDefault = false
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to save player config")
|
|
}
|
|
return &appv1.UpdateAdminPlayerConfigResponse{Config: payload}, nil
|
|
}
|
|
|
|
func (s *appServices) DeleteAdminPlayerConfig(ctx context.Context, req *appv1.DeleteAdminPlayerConfigRequest) (*appv1.MessageResponse, error) {
|
|
if _, err := s.requireAdmin(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id := strings.TrimSpace(req.GetId())
|
|
if id == "" {
|
|
return nil, status.Error(codes.NotFound, "Player config not found")
|
|
}
|
|
|
|
rowsAffected, err := s.playerConfigRepo.DeleteByID(ctx, id)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "Failed to delete player config")
|
|
}
|
|
if rowsAffected == 0 {
|
|
return nil, status.Error(codes.NotFound, "Player config not found")
|
|
}
|
|
|
|
return &appv1.MessageResponse{Message: "Player config deleted"}, nil
|
|
}
|