draft grpc
This commit is contained in:
672
internal/rpc/app/service_admin_finance_catalog.go
Normal file
672
internal/rpc/app/service_admin_finance_catalog.go
Normal file
@@ -0,0 +1,672 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
appv1 "stream.api/internal/gen/proto/app/v1"
|
||||
)
|
||||
|
||||
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())
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
var payment model.Payment
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&payment).Error; 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")
|
||||
}
|
||||
|
||||
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 payment")
|
||||
}
|
||||
|
||||
var planRecord model.Plan
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", planID).First(&planRecord).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.InvalidArgument, "Plan not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "Failed to create payment")
|
||||
}
|
||||
if planRecord.IsActive == nil || !*planRecord.IsActive {
|
||||
return nil, status.Error(codes.InvalidArgument, "Plan is not active")
|
||||
}
|
||||
|
||||
totalAmount := planRecord.Price * float64(req.GetTermMonths())
|
||||
statusValue := "SUCCESS"
|
||||
provider := "INTERNAL"
|
||||
currency := normalizeCurrency(nil)
|
||||
transactionID := buildTransactionID("sub")
|
||||
now := time.Now().UTC()
|
||||
|
||||
paymentRecord := &model.Payment{
|
||||
ID: uuid.New().String(),
|
||||
UserID: user.ID,
|
||||
PlanID: &planRecord.ID,
|
||||
Amount: totalAmount,
|
||||
Currency: ¤cy,
|
||||
Status: &statusValue,
|
||||
Provider: &provider,
|
||||
TransactionID: &transactionID,
|
||||
}
|
||||
invoiceID := buildInvoiceID(paymentRecord.ID)
|
||||
var subscription *model.PlanSubscription
|
||||
var walletBalance float64
|
||||
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if _, err := lockUserForUpdate(ctx, tx, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentSubscription, err := model.GetLatestPlanSubscription(ctx, tx, user.ID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
baseExpiry := now
|
||||
if currentSubscription != nil && currentSubscription.ExpiresAt.After(baseExpiry) {
|
||||
baseExpiry = currentSubscription.ExpiresAt.UTC()
|
||||
}
|
||||
newExpiry := baseExpiry.AddDate(0, int(req.GetTermMonths()), 0)
|
||||
|
||||
currentWalletBalance, err := model.GetWalletBalance(ctx, tx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shortfall := maxFloat(totalAmount-currentWalletBalance, 0)
|
||||
if paymentMethod == paymentMethodWallet && shortfall > 0 {
|
||||
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Insufficient wallet balance", map[string]interface{}{
|
||||
"payment_method": paymentMethod,
|
||||
"wallet_balance": currentWalletBalance,
|
||||
"total_amount": totalAmount,
|
||||
"shortfall": shortfall,
|
||||
})
|
||||
}
|
||||
|
||||
topupAmount := 0.0
|
||||
if paymentMethod == paymentMethodTopup {
|
||||
if req.TopupAmount == nil {
|
||||
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount is required when payment method is topup", map[string]interface{}{
|
||||
"payment_method": paymentMethod,
|
||||
"wallet_balance": currentWalletBalance,
|
||||
"total_amount": totalAmount,
|
||||
"shortfall": shortfall,
|
||||
})
|
||||
}
|
||||
topupAmount = maxFloat(req.GetTopupAmount(), 0)
|
||||
if topupAmount <= 0 {
|
||||
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount must be greater than 0", map[string]interface{}{
|
||||
"payment_method": paymentMethod,
|
||||
"wallet_balance": currentWalletBalance,
|
||||
"total_amount": totalAmount,
|
||||
"shortfall": shortfall,
|
||||
})
|
||||
}
|
||||
if topupAmount < shortfall {
|
||||
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount must be greater than or equal to the required shortfall", map[string]interface{}{
|
||||
"payment_method": paymentMethod,
|
||||
"wallet_balance": currentWalletBalance,
|
||||
"total_amount": totalAmount,
|
||||
"shortfall": shortfall,
|
||||
"topup_amount": topupAmount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Create(paymentRecord).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
walletUsedAmount := totalAmount
|
||||
if paymentMethod == paymentMethodTopup {
|
||||
topupTransaction := &model.WalletTransaction{
|
||||
ID: uuid.New().String(),
|
||||
UserID: user.ID,
|
||||
Type: walletTransactionTypeTopup,
|
||||
Amount: maxFloat(req.GetTopupAmount(), 0),
|
||||
Currency: model.StringPtr(currency),
|
||||
Note: model.StringPtr(fmt.Sprintf("Wallet top-up for %s (%d months)", planRecord.Name, req.GetTermMonths())),
|
||||
PaymentID: &paymentRecord.ID,
|
||||
PlanID: &planRecord.ID,
|
||||
TermMonths: int32Ptr(req.GetTermMonths()),
|
||||
}
|
||||
if err := tx.Create(topupTransaction).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
debitTransaction := &model.WalletTransaction{
|
||||
ID: uuid.New().String(),
|
||||
UserID: user.ID,
|
||||
Type: walletTransactionTypeSubscriptionDebit,
|
||||
Amount: -totalAmount,
|
||||
Currency: model.StringPtr(currency),
|
||||
Note: model.StringPtr(fmt.Sprintf("Subscription payment for %s (%d months)", planRecord.Name, req.GetTermMonths())),
|
||||
PaymentID: &paymentRecord.ID,
|
||||
PlanID: &planRecord.ID,
|
||||
TermMonths: int32Ptr(req.GetTermMonths()),
|
||||
}
|
||||
if err := tx.Create(debitTransaction).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subscription = &model.PlanSubscription{
|
||||
ID: uuid.New().String(),
|
||||
UserID: user.ID,
|
||||
PaymentID: paymentRecord.ID,
|
||||
PlanID: planRecord.ID,
|
||||
TermMonths: req.GetTermMonths(),
|
||||
PaymentMethod: paymentMethod,
|
||||
WalletAmount: walletUsedAmount,
|
||||
TopupAmount: maxFloat(req.GetTopupAmount(), 0),
|
||||
StartedAt: now,
|
||||
ExpiresAt: newExpiry,
|
||||
}
|
||||
if err := tx.Create(subscription).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).Update("plan_id", planRecord.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notification := buildSubscriptionNotification(user.ID, paymentRecord.ID, invoiceID, &planRecord, subscription)
|
||||
if err := tx.Create(notification).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
walletBalance, err = model.GetWalletBalance(ctx, tx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
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, paymentRecord)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to create payment")
|
||||
}
|
||||
return &appv1.CreateAdminPaymentResponse{
|
||||
Payment: payload,
|
||||
Subscription: toProtoPlanSubscription(subscription),
|
||||
WalletBalance: walletBalance,
|
||||
InvoiceId: 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")
|
||||
}
|
||||
|
||||
var payment model.Payment
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&payment).Error; 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.db.WithContext(ctx).Save(&payment).Error; 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
|
||||
}
|
||||
|
||||
var plans []model.Plan
|
||||
if err := s.db.WithContext(ctx).Order("price ASC").Find(&plans).Error; 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.db.WithContext(ctx).Create(plan).Error; 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)
|
||||
}
|
||||
|
||||
var plan model.Plan
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; 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.db.WithContext(ctx).Save(&plan).Error; 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")
|
||||
}
|
||||
|
||||
var plan model.Plan
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; 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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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())
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
var item model.AdTemplate
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; 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)
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.GetUserId())).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 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(normalizeAdminAdFormatValue(req.GetAdFormat())),
|
||||
Duration: duration,
|
||||
IsActive: model.BoolPtr(req.GetIsActive()),
|
||||
IsDefault: req.GetIsDefault(),
|
||||
}
|
||||
if !boolValue(item.IsActive) {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.GetUserId())).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 save ad template")
|
||||
}
|
||||
|
||||
var item model.AdTemplate
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; 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(normalizeAdminAdFormatValue(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.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 {
|
||||
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.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("ad_template_id = ?", id).Delete(&model.VideoAdConfig{}).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
|
||||
})
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user