package service import ( "context" "fmt" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/group" "github.com/Wei-Shaw/sub2api/ent/subscriptionplan" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" ) // --- Plan CRUD --- // PlanGroupInfo holds the group details needed for subscription plan display. type PlanGroupInfo struct { Platform string `json:"platform"` Name string `json:"name"` RateMultiplier float64 `json:"rate_multiplier"` DailyLimitUSD *float64 `json:"daily_limit_usd"` WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` ModelScopes []string `json:"supported_model_scopes"` } // GetGroupPlatformMap returns a map of group_id → platform for the given plans. func (s *PaymentConfigService) GetGroupPlatformMap(ctx context.Context, plans []*dbent.SubscriptionPlan) map[int64]string { info := s.GetGroupInfoMap(ctx, plans) m := make(map[int64]string, len(info)) for id, gi := range info { m[id] = gi.Platform } return m } // GetGroupInfoMap returns a map of group_id → PlanGroupInfo for the given plans. func (s *PaymentConfigService) GetGroupInfoMap(ctx context.Context, plans []*dbent.SubscriptionPlan) map[int64]PlanGroupInfo { ids := make([]int64, 0, len(plans)) seen := make(map[int64]bool) for _, p := range plans { if !seen[p.GroupID] { seen[p.GroupID] = true ids = append(ids, p.GroupID) } } if len(ids) == 0 { return nil } groups, err := s.entClient.Group.Query().Where(group.IDIn(ids...)).All(ctx) if err != nil { return nil } m := make(map[int64]PlanGroupInfo, len(groups)) for _, g := range groups { m[int64(g.ID)] = PlanGroupInfo{ Platform: g.Platform, Name: g.Name, RateMultiplier: g.RateMultiplier, DailyLimitUSD: g.DailyLimitUsd, WeeklyLimitUSD: g.WeeklyLimitUsd, MonthlyLimitUSD: g.MonthlyLimitUsd, ModelScopes: g.SupportedModelScopes, } } return m } func (s *PaymentConfigService) ListPlans(ctx context.Context) ([]*dbent.SubscriptionPlan, error) { return s.entClient.SubscriptionPlan.Query().Order(subscriptionplan.BySortOrder()).All(ctx) } func (s *PaymentConfigService) ListPlansForSale(ctx context.Context) ([]*dbent.SubscriptionPlan, error) { return s.entClient.SubscriptionPlan.Query().Where(subscriptionplan.ForSaleEQ(true)).Order(subscriptionplan.BySortOrder()).All(ctx) } func (s *PaymentConfigService) CreatePlan(ctx context.Context, req CreatePlanRequest) (*dbent.SubscriptionPlan, error) { b := s.entClient.SubscriptionPlan.Create(). SetGroupID(req.GroupID).SetName(req.Name).SetDescription(req.Description). SetPrice(req.Price).SetValidityDays(req.ValidityDays).SetValidityUnit(req.ValidityUnit). SetFeatures(req.Features).SetProductName(req.ProductName). SetForSale(req.ForSale).SetSortOrder(req.SortOrder) if req.OriginalPrice != nil { b.SetOriginalPrice(*req.OriginalPrice) } return b.Save(ctx) } // UpdatePlan updates a subscription plan by ID (patch semantics). // NOTE: This function exceeds 30 lines due to per-field nil-check patch update boilerplate. func (s *PaymentConfigService) UpdatePlan(ctx context.Context, id int64, req UpdatePlanRequest) (*dbent.SubscriptionPlan, error) { u := s.entClient.SubscriptionPlan.UpdateOneID(id) if req.GroupID != nil { u.SetGroupID(*req.GroupID) } if req.Name != nil { u.SetName(*req.Name) } if req.Description != nil { u.SetDescription(*req.Description) } if req.Price != nil { u.SetPrice(*req.Price) } if req.OriginalPrice != nil { u.SetOriginalPrice(*req.OriginalPrice) } if req.ValidityDays != nil { u.SetValidityDays(*req.ValidityDays) } if req.ValidityUnit != nil { u.SetValidityUnit(*req.ValidityUnit) } if req.Features != nil { u.SetFeatures(*req.Features) } if req.ProductName != nil { u.SetProductName(*req.ProductName) } if req.ForSale != nil { u.SetForSale(*req.ForSale) } if req.SortOrder != nil { u.SetSortOrder(*req.SortOrder) } return u.Save(ctx) } func (s *PaymentConfigService) DeletePlan(ctx context.Context, id int64) error { count, err := s.countPendingOrdersByPlan(ctx, id) if err != nil { return fmt.Errorf("check pending orders: %w", err) } if count > 0 { return infraerrors.Conflict("PENDING_ORDERS", fmt.Sprintf("this plan has %d in-progress orders and cannot be deleted — wait for orders to complete first", count)) } return s.entClient.SubscriptionPlan.DeleteOneID(id).Exec(ctx) } // GetPlan returns a subscription plan by ID. func (s *PaymentConfigService) GetPlan(ctx context.Context, id int64) (*dbent.SubscriptionPlan, error) { plan, err := s.entClient.SubscriptionPlan.Get(ctx, id) if err != nil { return nil, infraerrors.NotFound("PLAN_NOT_FOUND", "subscription plan not found") } return plan, nil }