Files
stream.api/internal/api/admin/plans.go
2026-03-13 02:17:18 +00:00

303 lines
9.8 KiB
Go

//go:build ignore
// +build ignore
package admin
import (
"errors"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/response"
)
type AdminPlanPayload struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Features []string `json:"features,omitempty"`
Price float64 `json:"price"`
Cycle string `json:"cycle"`
StorageLimit int64 `json:"storage_limit"`
UploadLimit int32 `json:"upload_limit"`
DurationLimit int32 `json:"duration_limit"`
QualityLimit string `json:"quality_limit"`
IsActive bool `json:"is_active"`
UserCount int64 `json:"user_count"`
PaymentCount int64 `json:"payment_count"`
SubscriptionCount int64 `json:"subscription_count"`
}
type SavePlanRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Features []string `json:"features"`
Price float64 `json:"price" binding:"required"`
Cycle string `json:"cycle" binding:"required"`
StorageLimit int64 `json:"storage_limit" binding:"required"`
UploadLimit int32 `json:"upload_limit" binding:"required"`
IsActive *bool `json:"is_active"`
}
func buildAdminPlanPayload(plan model.Plan, userCount, paymentCount, subscriptionCount int64) AdminPlanPayload {
return AdminPlanPayload{
ID: plan.ID,
Name: plan.Name,
Description: adminStringValue(plan.Description),
Features: adminStringSliceValue(plan.Features),
Price: plan.Price,
Cycle: plan.Cycle,
StorageLimit: plan.StorageLimit,
UploadLimit: plan.UploadLimit,
DurationLimit: plan.DurationLimit,
QualityLimit: plan.QualityLimit,
IsActive: adminBoolValue(plan.IsActive, true),
UserCount: userCount,
PaymentCount: paymentCount,
SubscriptionCount: subscriptionCount,
}
}
func (h *Handler) loadPlanUsageCounts(ctx *gin.Context, planID string) (int64, int64, int64, error) {
var userCount int64
if err := h.db.WithContext(ctx.Request.Context()).Model(&model.User{}).Where("plan_id = ?", planID).Count(&userCount).Error; err != nil {
return 0, 0, 0, err
}
var paymentCount int64
if err := h.db.WithContext(ctx.Request.Context()).Model(&model.Payment{}).Where("plan_id = ?", planID).Count(&paymentCount).Error; err != nil {
return 0, 0, 0, err
}
var subscriptionCount int64
if err := h.db.WithContext(ctx.Request.Context()).Model(&model.PlanSubscription{}).Where("plan_id = ?", planID).Count(&subscriptionCount).Error; err != nil {
return 0, 0, 0, err
}
return userCount, paymentCount, subscriptionCount, nil
}
func validatePlanRequest(req *SavePlanRequest) string {
if strings.TrimSpace(req.Name) == "" {
return "Name is required"
}
if strings.TrimSpace(req.Cycle) == "" {
return "Cycle is required"
}
if req.Price < 0 {
return "Price must be greater than or equal to 0"
}
if req.StorageLimit <= 0 {
return "Storage limit must be greater than 0"
}
if req.UploadLimit <= 0 {
return "Upload limit must be greater than 0"
}
return ""
}
// @Summary List Plans
// @Description Get all plans with usage counts (admin only)
// @Tags admin
// @Produce json
// @Success 200 {object} response.Response
// @Router /admin/plans [get]
// @Security BearerAuth
func (h *Handler) ListPlans(c *gin.Context) {
ctx := c.Request.Context()
var plans []model.Plan
if err := h.db.WithContext(ctx).Order("price ASC").Find(&plans).Error; err != nil {
h.logger.Error("Failed to list plans", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to list plans")
return
}
result := make([]AdminPlanPayload, 0, len(plans))
for _, plan := range plans {
userCount, paymentCount, subscriptionCount, err := h.loadPlanUsageCounts(c, plan.ID)
if err != nil {
h.logger.Error("Failed to load plan usage", "error", err, "plan_id", plan.ID)
response.Error(c, http.StatusInternalServerError, "Failed to list plans")
return
}
result = append(result, buildAdminPlanPayload(plan, userCount, paymentCount, subscriptionCount))
}
response.Success(c, gin.H{"plans": result})
}
// @Summary Create Plan
// @Description Create a plan (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param request body SavePlanRequest true "Plan payload"
// @Success 201 {object} response.Response
// @Router /admin/plans [post]
// @Security BearerAuth
func (h *Handler) CreatePlan(c *gin.Context) {
var req SavePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if msg := validatePlanRequest(&req); msg != "" {
response.Error(c, http.StatusBadRequest, msg)
return
}
plan := &model.Plan{
ID: uuid.New().String(),
Name: strings.TrimSpace(req.Name),
Description: adminStringPtr(req.Description),
Features: adminStringSlice(req.Features),
Price: req.Price,
Cycle: strings.TrimSpace(req.Cycle),
StorageLimit: req.StorageLimit,
UploadLimit: req.UploadLimit,
DurationLimit: 0,
QualityLimit: "",
IsActive: func() *bool {
value := true
if req.IsActive != nil {
value = *req.IsActive
}
return &value
}(),
}
if err := h.db.WithContext(c.Request.Context()).Create(plan).Error; err != nil {
h.logger.Error("Failed to create plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create plan")
return
}
response.Created(c, gin.H{"plan": buildAdminPlanPayload(*plan, 0, 0, 0)})
}
// @Summary Update Plan
// @Description Update a plan (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "Plan ID"
// @Param request body SavePlanRequest true "Plan payload"
// @Success 200 {object} response.Response
// @Router /admin/plans/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdatePlan(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
var req SavePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if msg := validatePlanRequest(&req); msg != "" {
response.Error(c, http.StatusBadRequest, msg)
return
}
ctx := c.Request.Context()
var plan model.Plan
if err := h.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to update plan")
return
}
plan.Name = strings.TrimSpace(req.Name)
plan.Description = adminStringPtr(req.Description)
plan.Features = adminStringSlice(req.Features)
plan.Price = req.Price
plan.Cycle = strings.TrimSpace(req.Cycle)
plan.StorageLimit = req.StorageLimit
plan.UploadLimit = req.UploadLimit
if req.IsActive != nil {
plan.IsActive = req.IsActive
}
if err := h.db.WithContext(ctx).Save(&plan).Error; err != nil {
h.logger.Error("Failed to update plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update plan")
return
}
userCount, paymentCount, subscriptionCount, err := h.loadPlanUsageCounts(c, plan.ID)
if err != nil {
h.logger.Error("Failed to load plan usage", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update plan")
return
}
response.Success(c, gin.H{"plan": buildAdminPlanPayload(plan, userCount, paymentCount, subscriptionCount)})
}
// @Summary Delete Plan
// @Description Delete a plan, or deactivate it if already used (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "Plan ID"
// @Success 200 {object} response.Response
// @Router /admin/plans/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeletePlan(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
ctx := c.Request.Context()
var plan model.Plan
if err := h.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to delete plan")
return
}
var paymentCount int64
if err := h.db.WithContext(ctx).Model(&model.Payment{}).Where("plan_id = ?", id).Count(&paymentCount).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to delete plan")
return
}
var subscriptionCount int64
if err := h.db.WithContext(ctx).Model(&model.PlanSubscription{}).Where("plan_id = ?", id).Count(&subscriptionCount).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to delete plan")
return
}
if paymentCount > 0 || subscriptionCount > 0 {
inactive := false
if err := h.db.WithContext(ctx).Model(&model.Plan{}).Where("id = ?", id).Update("is_active", inactive).Error; err != nil {
h.logger.Error("Failed to deactivate plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to deactivate plan")
return
}
response.Success(c, gin.H{"message": "Plan deactivated", "mode": "deactivated"})
return
}
if err := h.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Plan{}).Error; err != nil {
h.logger.Error("Failed to delete plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to delete plan")
return
}
response.Success(c, gin.H{"message": "Plan deleted", "mode": "deleted"})
}