This commit is contained in:
2026-03-26 13:02:43 +00:00
parent a689f8b9da
commit eb7b519e49
64 changed files with 7081 additions and 5572 deletions

View File

@@ -0,0 +1,191 @@
package plans
import (
"context"
"errors"
"strings"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/internal/modules/common"
)
type Module struct {
runtime *common.Runtime
}
func New(runtime *common.Runtime) *Module {
return &Module{runtime: runtime}
}
func (m *Module) ListPlans(ctx context.Context) (*ListPlansResult, error) {
if _, err := m.runtime.Authenticate(ctx); err != nil {
return nil, err
}
var plans []model.Plan
if err := m.runtime.DB().WithContext(ctx).Where("is_active = ?", true).Find(&plans).Error; err != nil {
m.runtime.Logger().Error("Failed to fetch plans", "error", err)
return nil, status.Error(codes.Internal, "Failed to fetch plans")
}
items := make([]PlanView, 0, len(plans))
for i := range plans {
items = append(items, PlanView{Plan: &plans[i]})
}
return &ListPlansResult{Items: items}, nil
}
func (m *Module) ListAdminPlans(ctx context.Context) (*ListAdminPlansResult, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
var plans []model.Plan
if err := m.runtime.DB().WithContext(ctx).Order("price ASC").Find(&plans).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list plans")
}
items := make([]AdminPlanView, 0, len(plans))
for i := range plans {
payload, err := m.BuildAdminPlan(ctx, &plans[i])
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list plans")
}
items = append(items, payload)
}
return &ListAdminPlansResult{Items: items}, nil
}
func (m *Module) CreateAdminPlan(ctx context.Context, cmd CreateAdminPlanCommand) (*AdminPlanView, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
if msg := validateAdminPlanInput(cmd.Name, cmd.Cycle, cmd.Price, cmd.StorageLimit, cmd.UploadLimit); msg != "" {
return nil, status.Error(codes.InvalidArgument, msg)
}
plan := &model.Plan{ID: uuid.New().String(), Name: strings.TrimSpace(cmd.Name), Description: common.NullableTrimmedStringPtr(cmd.Description), Features: append([]string(nil), cmd.Features...), Price: cmd.Price, Cycle: strings.TrimSpace(cmd.Cycle), StorageLimit: cmd.StorageLimit, UploadLimit: cmd.UploadLimit, DurationLimit: 0, QualityLimit: "", IsActive: model.BoolPtr(cmd.IsActive)}
if err := m.runtime.DB().WithContext(ctx).Create(plan).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to create plan")
}
payload, err := m.BuildAdminPlan(ctx, plan)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create plan")
}
return &payload, nil
}
func (m *Module) UpdateAdminPlan(ctx context.Context, cmd UpdateAdminPlanCommand) (*AdminPlanView, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
if cmd.ID == "" {
return nil, status.Error(codes.NotFound, "Plan not found")
}
if msg := validateAdminPlanInput(cmd.Name, cmd.Cycle, cmd.Price, cmd.StorageLimit, cmd.UploadLimit); msg != "" {
return nil, status.Error(codes.InvalidArgument, msg)
}
var plan model.Plan
if err := m.runtime.DB().WithContext(ctx).Where("id = ?", cmd.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(cmd.Name)
plan.Description = common.NullableTrimmedStringPtr(cmd.Description)
plan.Features = append([]string(nil), cmd.Features...)
plan.Price = cmd.Price
plan.Cycle = strings.TrimSpace(cmd.Cycle)
plan.StorageLimit = cmd.StorageLimit
plan.UploadLimit = cmd.UploadLimit
plan.IsActive = model.BoolPtr(cmd.IsActive)
if err := m.runtime.DB().WithContext(ctx).Save(&plan).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to update plan")
}
payload, err := m.BuildAdminPlan(ctx, &plan)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to update plan")
}
return &payload, nil
}
func (m *Module) DeleteAdminPlan(ctx context.Context, cmd DeleteAdminPlanCommand) (*DeleteAdminPlanResult, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
if cmd.ID == "" {
return nil, status.Error(codes.NotFound, "Plan not found")
}
var plan model.Plan
if err := m.runtime.DB().WithContext(ctx).Where("id = ?", cmd.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 := m.runtime.DB().WithContext(ctx).Model(&model.Payment{}).Where("plan_id = ?", cmd.ID).Count(&paymentCount).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
var subscriptionCount int64
if err := m.runtime.DB().WithContext(ctx).Model(&model.PlanSubscription{}).Where("plan_id = ?", cmd.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 := m.runtime.DB().WithContext(ctx).Model(&model.Plan{}).Where("id = ?", cmd.ID).Update("is_active", inactive).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to deactivate plan")
}
return &DeleteAdminPlanResult{Message: "Plan deactivated", Mode: "deactivated"}, nil
}
if err := m.runtime.DB().WithContext(ctx).Where("id = ?", cmd.ID).Delete(&model.Plan{}).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
return &DeleteAdminPlanResult{Message: "Plan deleted", Mode: "deleted"}, nil
}
func (m *Module) BuildAdminPlan(ctx context.Context, plan *model.Plan) (AdminPlanView, error) {
if plan == nil {
return AdminPlanView{}, nil
}
userCount, paymentCount, subscriptionCount, err := m.loadAdminPlanUsageCounts(ctx, plan.ID)
if err != nil {
return AdminPlanView{}, err
}
return AdminPlanView{ID: plan.ID, Name: plan.Name, Description: common.NullableTrimmedString(plan.Description), Features: append([]string(nil), plan.Features...), Price: plan.Price, Cycle: plan.Cycle, StorageLimit: plan.StorageLimit, UploadLimit: plan.UploadLimit, DurationLimit: int32(plan.DurationLimit), QualityLimit: plan.QualityLimit, IsActive: common.BoolValue(plan.IsActive), UserCount: userCount, PaymentCount: paymentCount, SubscriptionCount: subscriptionCount}, nil
}
func (m *Module) loadAdminPlanUsageCounts(ctx context.Context, planID string) (int64, int64, int64, error) {
var userCount int64
if err := m.runtime.DB().WithContext(ctx).Model(&model.User{}).Where("plan_id = ?", planID).Count(&userCount).Error; err != nil {
return 0, 0, 0, err
}
var paymentCount int64
if err := m.runtime.DB().WithContext(ctx).Model(&model.Payment{}).Where("plan_id = ?", planID).Count(&paymentCount).Error; err != nil {
return 0, 0, 0, err
}
var subscriptionCount int64
if err := m.runtime.DB().WithContext(ctx).Model(&model.PlanSubscription{}).Where("plan_id = ?", planID).Count(&subscriptionCount).Error; err != nil {
return 0, 0, 0, err
}
return userCount, paymentCount, subscriptionCount, nil
}
func validateAdminPlanInput(name, cycle string, price float64, storageLimit int64, uploadLimit int32) string {
if strings.TrimSpace(name) == "" {
return "Name is required"
}
if strings.TrimSpace(cycle) == "" {
return "Cycle is required"
}
if price < 0 {
return "Price must be greater than or equal to 0"
}
if storageLimit <= 0 {
return "Storage limit must be greater than 0"
}
if uploadLimit <= 0 {
return "Upload limit must be greater than 0"
}
return ""
}