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,56 @@
package plans
import (
"context"
appv1 "stream.api/internal/gen/proto/app/v1"
)
type Handler struct {
appv1.UnimplementedPlansServiceServer
module *Module
}
var _ appv1.PlansServiceServer = (*Handler)(nil)
func NewHandler(module *Module) *Handler { return &Handler{module: module} }
func (h *Handler) ListPlans(ctx context.Context, _ *appv1.ListPlansRequest) (*appv1.ListPlansResponse, error) {
result, err := h.module.ListPlans(ctx)
if err != nil {
return nil, err
}
return presentListPlansResponse(result), nil
}
func (h *Handler) ListAdminPlans(ctx context.Context, _ *appv1.ListAdminPlansRequest) (*appv1.ListAdminPlansResponse, error) {
result, err := h.module.ListAdminPlans(ctx)
if err != nil {
return nil, err
}
return presentListAdminPlansResponse(result), nil
}
func (h *Handler) CreateAdminPlan(ctx context.Context, req *appv1.CreateAdminPlanRequest) (*appv1.CreateAdminPlanResponse, error) {
result, err := h.module.CreateAdminPlan(ctx, CreateAdminPlanCommand{Name: req.GetName(), Description: req.Description, Features: req.GetFeatures(), Price: req.GetPrice(), Cycle: req.GetCycle(), StorageLimit: req.GetStorageLimit(), UploadLimit: req.GetUploadLimit(), IsActive: req.GetIsActive()})
if err != nil {
return nil, err
}
return presentCreateAdminPlanResponse(*result), nil
}
func (h *Handler) UpdateAdminPlan(ctx context.Context, req *appv1.UpdateAdminPlanRequest) (*appv1.UpdateAdminPlanResponse, error) {
result, err := h.module.UpdateAdminPlan(ctx, UpdateAdminPlanCommand{ID: req.GetId(), Name: req.GetName(), Description: req.Description, Features: req.GetFeatures(), Price: req.GetPrice(), Cycle: req.GetCycle(), StorageLimit: req.GetStorageLimit(), UploadLimit: req.GetUploadLimit(), IsActive: req.GetIsActive()})
if err != nil {
return nil, err
}
return presentUpdateAdminPlanResponse(*result), nil
}
func (h *Handler) DeleteAdminPlan(ctx context.Context, req *appv1.DeleteAdminPlanRequest) (*appv1.DeleteAdminPlanResponse, error) {
result, err := h.module.DeleteAdminPlan(ctx, DeleteAdminPlanCommand{ID: req.GetId()})
if err != nil {
return nil, err
}
return presentDeleteAdminPlanResponse(result), nil
}

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 ""
}

View File

@@ -0,0 +1,62 @@
package plans
import appv1 "stream.api/internal/gen/proto/app/v1"
func presentListPlansResponse(result *ListPlansResult) *appv1.ListPlansResponse {
items := make([]*appv1.Plan, 0, len(result.Items))
for _, item := range result.Items {
items = append(items, &appv1.Plan{
Id: item.Plan.ID,
Name: item.Plan.Name,
Description: item.Plan.Description,
Price: item.Plan.Price,
Cycle: item.Plan.Cycle,
StorageLimit: item.Plan.StorageLimit,
UploadLimit: item.Plan.UploadLimit,
DurationLimit: item.Plan.DurationLimit,
QualityLimit: item.Plan.QualityLimit,
Features: item.Plan.Features,
IsActive: item.Plan.IsActive != nil && *item.Plan.IsActive,
})
}
return &appv1.ListPlansResponse{Plans: items}
}
func presentAdminPlan(view AdminPlanView) *appv1.AdminPlan {
return &appv1.AdminPlan{
Id: view.ID,
Name: view.Name,
Description: view.Description,
Features: view.Features,
Price: view.Price,
Cycle: view.Cycle,
StorageLimit: view.StorageLimit,
UploadLimit: view.UploadLimit,
DurationLimit: view.DurationLimit,
QualityLimit: view.QualityLimit,
IsActive: view.IsActive,
UserCount: view.UserCount,
PaymentCount: view.PaymentCount,
SubscriptionCount: view.SubscriptionCount,
}
}
func presentListAdminPlansResponse(result *ListAdminPlansResult) *appv1.ListAdminPlansResponse {
items := make([]*appv1.AdminPlan, 0, len(result.Items))
for _, item := range result.Items {
items = append(items, presentAdminPlan(item))
}
return &appv1.ListAdminPlansResponse{Plans: items}
}
func presentCreateAdminPlanResponse(view AdminPlanView) *appv1.CreateAdminPlanResponse {
return &appv1.CreateAdminPlanResponse{Plan: presentAdminPlan(view)}
}
func presentUpdateAdminPlanResponse(view AdminPlanView) *appv1.UpdateAdminPlanResponse {
return &appv1.UpdateAdminPlanResponse{Plan: presentAdminPlan(view)}
}
func presentDeleteAdminPlanResponse(result *DeleteAdminPlanResult) *appv1.DeleteAdminPlanResponse {
return &appv1.DeleteAdminPlanResponse{Message: result.Message, Mode: result.Mode}
}

View File

@@ -0,0 +1,64 @@
package plans
import "stream.api/internal/database/model"
type PlanView struct {
Plan *model.Plan
}
type ListPlansResult struct {
Items []PlanView
}
type AdminPlanView struct {
ID string
Name string
Description *string
Features []string
Price float64
Cycle string
StorageLimit int64
UploadLimit int32
DurationLimit int32
QualityLimit string
IsActive bool
UserCount int64
PaymentCount int64
SubscriptionCount int64
}
type ListAdminPlansResult struct {
Items []AdminPlanView
}
type CreateAdminPlanCommand struct {
Name string
Description *string
Features []string
Price float64
Cycle string
StorageLimit int64
UploadLimit int32
IsActive bool
}
type UpdateAdminPlanCommand struct {
ID string
Name string
Description *string
Features []string
Price float64
Cycle string
StorageLimit int64
UploadLimit int32
IsActive bool
}
type DeleteAdminPlanCommand struct {
ID string
}
type DeleteAdminPlanResult struct {
Message string
Mode string
}