draft
This commit is contained in:
56
internal/modules/plans/handler.go
Normal file
56
internal/modules/plans/handler.go
Normal 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
|
||||
}
|
||||
191
internal/modules/plans/module.go
Normal file
191
internal/modules/plans/module.go
Normal 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 ""
|
||||
}
|
||||
62
internal/modules/plans/presenter.go
Normal file
62
internal/modules/plans/presenter.go
Normal 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}
|
||||
}
|
||||
64
internal/modules/plans/types.go
Normal file
64
internal/modules/plans/types.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user