refactor: Extract service methods into separate files for better organization

- Move GetUsage, GetUsageHistory, GetUsageAnalytics, GetRevenueAnalytics, and GetPopularPlans to service_analytics.go
- Move validateRatePlan, validateSubscribeRequest, calculateNextBillingDate, and calculateCycleEnd to service_methods.go
- Move SubscribeToPlan, GetSubscription, UpdateSubscription, CancelSubscription, GetActiveSubscription, and ListSubscriptions to service_subscriptions.go
- Move RecordUsage to service_usage.go
This commit is contained in:
nutcas3
2026-05-02 00:21:11 +03:00
parent 4131b73559
commit 0d9b8f06b1
4 changed files with 334 additions and 359 deletions
@@ -23,7 +23,6 @@ func NewService(repo Repository, logger *logrus.Logger) *Service {
}
}
// CreateRatePlan creates a new rate plan with validation
func (s *Service) CreateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan, error) {
// Generate ID if not provided
if plan.ID == "" {
@@ -134,221 +133,6 @@ func (s *Service) SearchRatePlans(ctx context.Context, criteria SearchCriteria)
return s.repo.ListRatePlans(ctx, filter)
}
// SubscribeToPlan subscribes a profile to a rate plan
func (s *Service) SubscribeToPlan(ctx context.Context, req *SubscribeRequest) (*RatePlanSubscription, error) {
// Validate request
if err := s.validateSubscribeRequest(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Get the rate plan
plan, err := s.repo.GetRatePlan(ctx, req.RatePlanID)
if err != nil {
return nil, err
}
// Check if plan is active
if !plan.IsActive || plan.Status != PlanStatusActive {
return nil, fmt.Errorf("rate plan is not available for subscription")
}
// Check if profile already has an active subscription
activeSub, err := s.repo.GetActiveSubscription(ctx, req.ProfileID)
if err != nil {
return nil, err
}
if activeSub != nil {
return nil, fmt.Errorf("profile already has an active subscription")
}
// Create subscription
subscription := &RatePlanSubscription{
ID: uuid.New().String(),
ProfileID: req.ProfileID,
RatePlanID: req.RatePlanID,
Status: SubscriptionStatusActive,
StartedAt: time.Now(),
BillingCycle: plan.BillingCycle,
NextBillingDate: s.calculateNextBillingDate(plan.BillingCycle, time.Now()),
AutoRenew: req.AutoRenew,
CurrentCycle: time.Now(),
AppliedDiscounts: req.AppliedDiscounts,
Metadata: req.Metadata,
}
if err := s.repo.CreateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to create subscription")
return nil, err
}
s.logger.WithFields(logrus.Fields{
"subscription_id": subscription.ID,
"profile_id": req.ProfileID,
"rate_plan_id": req.RatePlanID,
}).Info("Subscription created successfully")
return subscription, nil
}
// GetSubscription retrieves a subscription by ID
func (s *Service) GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error) {
subscription, err := s.repo.GetSubscription(ctx, id)
if err != nil {
s.logger.WithError(err).WithField("subscription_id", id).Error("Failed to get subscription")
return nil, err
}
return subscription, nil
}
// UpdateSubscription updates an existing subscription
func (s *Service) UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) (*RatePlanSubscription, error) {
if err := s.repo.UpdateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to update subscription")
return nil, err
}
s.logger.WithField("subscription_id", subscription.ID).Info("Subscription updated successfully")
return subscription, nil
}
// CancelSubscription cancels a subscription
func (s *Service) CancelSubscription(ctx context.Context, subscriptionID string, reason string) error {
subscription, err := s.repo.GetSubscription(ctx, subscriptionID)
if err != nil {
return err
}
if subscription.Status != SubscriptionStatusActive {
return fmt.Errorf("subscription is not active")
}
now := time.Now()
subscription.Status = SubscriptionStatusCancelled
subscription.EndedAt = &now
subscription.UpdatedAt = now
if subscription.Metadata == nil {
subscription.Metadata = make(map[string]interface{})
}
subscription.Metadata["cancellation_reason"] = reason
if err := s.repo.UpdateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to cancel subscription")
return err
}
s.logger.WithField("subscription_id", subscriptionID).Info("Subscription cancelled successfully")
return nil
}
// GetActiveSubscription retrieves the active subscription for a profile
func (s *Service) GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error) {
subscription, err := s.repo.GetActiveSubscription(ctx, profileID)
if err != nil {
s.logger.WithError(err).WithField("profile_id", profileID).Error("Failed to get active subscription")
return nil, err
}
return subscription, nil
}
// ListSubscriptions retrieves subscriptions for a profile
func (s *Service) ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error) {
subscriptions, err := s.repo.ListSubscriptions(ctx, profileID, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to list subscriptions")
return nil, err
}
return subscriptions, nil
}
// RecordUsage records usage for a subscription
func (s *Service) RecordUsage(ctx context.Context, req *RecordUsageRequest) (*RatePlanUsage, error) {
// Get active subscription
subscription, err := s.repo.GetActiveSubscription(ctx, req.ProfileID)
if err != nil {
return nil, err
}
if subscription == nil {
return nil, fmt.Errorf("no active subscription found")
}
// Get current usage
currentUsage, err := s.repo.GetCurrentUsage(ctx, req.ProfileID)
if err != nil {
return nil, err
}
// Create or update usage record
var usage *RatePlanUsage
if currentUsage == nil {
// Create new usage record
usage = &RatePlanUsage{
ID: uuid.New().String(),
RatePlanID: subscription.RatePlanID,
ProfileID: req.ProfileID,
CycleStart: subscription.CurrentCycle,
CycleEnd: s.calculateCycleEnd(subscription.BillingCycle, subscription.CurrentCycle),
DataUsed: req.DataUsed,
VoiceUsed: req.VoiceUsed,
SMSUsed: req.SMSUsed,
}
if err := s.repo.CreateUsage(ctx, usage); err != nil {
s.logger.WithError(err).Error("Failed to create usage record")
return nil, err
}
} else {
// Update existing usage record
currentUsage.DataUsed += req.DataUsed
currentUsage.VoiceUsed += req.VoiceUsed
currentUsage.SMSUsed += req.SMSUsed
if err := s.repo.UpdateUsage(ctx, currentUsage); err != nil {
s.logger.WithError(err).Error("Failed to update usage record")
return nil, err
}
usage = currentUsage
}
s.logger.WithFields(logrus.Fields{
"profile_id": req.ProfileID,
"data_used": req.DataUsed,
"voice_used": req.VoiceUsed,
"sms_used": req.SMSUsed,
}).Info("Usage recorded successfully")
return usage, nil
}
// GetUsage retrieves usage for a profile
func (s *Service) GetUsage(ctx context.Context, profileID string) (*RatePlanUsage, error) {
usage, err := s.repo.GetCurrentUsage(ctx, profileID)
if err != nil {
s.logger.WithError(err).WithField("profile_id", profileID).Error("Failed to get usage")
return nil, err
}
return usage, nil
}
// GetUsageHistory retrieves usage history for a profile
func (s *Service) GetUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error) {
usageHistory, err := s.repo.ListUsageHistory(ctx, profileID, limit)
if err != nil {
s.logger.WithError(err).Error("Failed to get usage history")
return nil, err
}
return usageHistory, nil
}
// CalculateCost calculates the cost for a rate plan based on usage
func (s *Service) CalculateCost(ctx context.Context, req *CalculateCostRequest) (*RatePlanCostCalculation, error) {
// Get the rate plan
@@ -381,9 +165,10 @@ func (s *Service) CalculateCost(ctx context.Context, req *CalculateCostRequest)
for _, discountID := range req.AppliedDiscounts {
for _, discount := range plan.Discounts {
if discount.ID == discountID && discount.IsActive {
if discount.Type == DiscountTypePercentage {
switch discount.Type {
case DiscountTypePercentage:
discountCost += baseCost * discount.Value / 100
} else if discount.Type == DiscountTypeFixed {
case DiscountTypeFixed:
discountCost += discount.Value
}
}
@@ -411,144 +196,3 @@ func (s *Service) CalculateCost(ctx context.Context, req *CalculateCostRequest)
return calculation, nil
}
// GetUsageAnalytics retrieves usage analytics
func (s *Service) GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error) {
analytics, err := s.repo.GetUsageAnalytics(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get usage analytics")
return nil, err
}
return analytics, nil
}
// GetRevenueAnalytics retrieves revenue analytics
func (s *Service) GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error) {
analytics, err := s.repo.GetRevenueAnalytics(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get revenue analytics")
return nil, err
}
return analytics, nil
}
// GetPopularPlans retrieves the most popular rate plans
func (s *Service) GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error) {
plans, err := s.repo.GetPopularPlans(ctx, limit)
if err != nil {
s.logger.WithError(err).Error("Failed to get popular plans")
return nil, err
}
return plans, nil
}
// Helper methods
func (s *Service) validateRatePlan(plan *RatePlan) error {
if plan.Name == "" {
return fmt.Errorf("rate plan name is required")
}
if plan.CarrierID == "" {
return fmt.Errorf("carrier ID is required")
}
if plan.Region == "" {
return fmt.Errorf("region is required")
}
if plan.BasePrice < 0 {
return fmt.Errorf("base price cannot be negative")
}
if plan.Currency == "" {
return fmt.Errorf("currency is required")
}
if plan.BillingCycle == "" {
return fmt.Errorf("billing cycle is required")
}
if plan.ValidFrom.IsZero() {
return fmt.Errorf("valid from date is required")
}
return nil
}
func (s *Service) validateSubscribeRequest(req *SubscribeRequest) error {
if req.ProfileID == "" {
return fmt.Errorf("profile ID is required")
}
if req.RatePlanID == "" {
return fmt.Errorf("rate plan ID is required")
}
return nil
}
func (s *Service) calculateNextBillingDate(cycle BillingCycle, from time.Time) time.Time {
switch cycle {
case BillingCycleDaily:
return from.AddDate(0, 0, 1)
case BillingCycleWeekly:
return from.AddDate(0, 0, 7)
case BillingCycleMonthly:
return from.AddDate(0, 1, 0)
case BillingCycleQuarterly:
return from.AddDate(0, 3, 0)
case BillingCycleYearly:
return from.AddDate(1, 0, 0)
default:
return from.AddDate(0, 1, 0) // Default to monthly
}
}
func (s *Service) calculateCycleEnd(cycle BillingCycle, cycleStart time.Time) time.Time {
switch cycle {
case BillingCycleDaily:
return cycleStart.AddDate(0, 0, 1).Add(-time.Nanosecond)
case BillingCycleWeekly:
return cycleStart.AddDate(0, 0, 7).Add(-time.Nanosecond)
case BillingCycleMonthly:
return cycleStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
case BillingCycleQuarterly:
return cycleStart.AddDate(0, 3, 0).Add(-time.Nanosecond)
case BillingCycleYearly:
return cycleStart.AddDate(1, 0, 0).Add(-time.Nanosecond)
default:
return cycleStart.AddDate(0, 1, 0).Add(-time.Nanosecond) // Default to monthly
}
}
// Request/Response types
type SearchCriteria struct {
CarrierID string `json:"carrier_id,omitempty"`
Region string `json:"region,omitempty"`
PlanType PlanType `json:"plan_type,omitempty"`
MinPrice float64 `json:"min_price,omitempty"`
MaxPrice float64 `json:"max_price,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
SortBy string `json:"sort_by,omitempty"`
SortOrder string `json:"sort_order,omitempty"`
}
type SubscribeRequest struct {
ProfileID string `json:"profile_id"`
RatePlanID string `json:"rate_plan_id"`
AutoRenew bool `json:"auto_renew"`
AppliedDiscounts []string `json:"applied_discounts,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type RecordUsageRequest struct {
ProfileID string `json:"profile_id"`
DataUsed int64 `json:"data_used"` // in MB
VoiceUsed int64 `json:"voice_used"` // in minutes
SMSUsed int64 `json:"sms_used"` // count
}
type CalculateCostRequest struct {
RatePlanID string `json:"rate_plan_id"`
DataUsed int64 `json:"data_used"` // in MB
VoiceUsed int64 `json:"voice_used"` // in minutes
SMSUsed int64 `json:"sms_used"` // count
AppliedDiscounts []string `json:"applied_discounts,omitempty"`
}
@@ -0,0 +1,59 @@
package rateplan
import (
"context"
)
// GetUsage retrieves usage for a profile
func (s *Service) GetUsage(ctx context.Context, profileID string) (*RatePlanUsage, error) {
usage, err := s.repo.GetCurrentUsage(ctx, profileID)
if err != nil {
s.logger.WithError(err).WithField("profile_id", profileID).Error("Failed to get usage")
return nil, err
}
return usage, nil
}
// GetUsageHistory retrieves usage history for a profile
func (s *Service) GetUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error) {
usageHistory, err := s.repo.ListUsageHistory(ctx, profileID, limit)
if err != nil {
s.logger.WithError(err).Error("Failed to get usage history")
return nil, err
}
return usageHistory, nil
}
// GetUsageAnalytics retrieves usage analytics
func (s *Service) GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error) {
analytics, err := s.repo.GetUsageAnalytics(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get usage analytics")
return nil, err
}
return analytics, nil
}
// GetRevenueAnalytics retrieves revenue analytics
func (s *Service) GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error) {
analytics, err := s.repo.GetRevenueAnalytics(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get revenue analytics")
return nil, err
}
return analytics, nil
}
// GetPopularPlans retrieves the most popular rate plans
func (s *Service) GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error) {
plans, err := s.repo.GetPopularPlans(ctx, limit)
if err != nil {
s.logger.WithError(err).Error("Failed to get popular plans")
return nil, err
}
return plans, nil
}
@@ -0,0 +1,76 @@
package rateplan
import (
"fmt"
"time"
)
func (s *Service) validateRatePlan(plan *RatePlan) error {
if plan.Name == "" {
return fmt.Errorf("rate plan name is required")
}
if plan.CarrierID == "" {
return fmt.Errorf("carrier ID is required")
}
if plan.Region == "" {
return fmt.Errorf("region is required")
}
if plan.BasePrice < 0 {
return fmt.Errorf("base price cannot be negative")
}
if plan.Currency == "" {
return fmt.Errorf("currency is required")
}
if plan.BillingCycle == "" {
return fmt.Errorf("billing cycle is required")
}
if plan.ValidFrom.IsZero() {
return fmt.Errorf("valid from date is required")
}
return nil
}
func (s *Service) validateSubscribeRequest(req *SubscribeRequest) error {
if req.ProfileID == "" {
return fmt.Errorf("profile ID is required")
}
if req.RatePlanID == "" {
return fmt.Errorf("rate plan ID is required")
}
return nil
}
func (s *Service) calculateNextBillingDate(cycle BillingCycle, from time.Time) time.Time {
switch cycle {
case BillingCycleDaily:
return from.AddDate(0, 0, 1)
case BillingCycleWeekly:
return from.AddDate(0, 0, 7)
case BillingCycleMonthly:
return from.AddDate(0, 1, 0)
case BillingCycleQuarterly:
return from.AddDate(0, 3, 0)
case BillingCycleYearly:
return from.AddDate(1, 0, 0)
default:
return from.AddDate(0, 1, 0) // Default to monthly
}
}
func (s *Service) calculateCycleEnd(cycle BillingCycle, cycleStart time.Time) time.Time {
switch cycle {
case BillingCycleDaily:
return cycleStart.AddDate(0, 0, 1).Add(-time.Nanosecond)
case BillingCycleWeekly:
return cycleStart.AddDate(0, 0, 7).Add(-time.Nanosecond)
case BillingCycleMonthly:
return cycleStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
case BillingCycleQuarterly:
return cycleStart.AddDate(0, 3, 0).Add(-time.Nanosecond)
case BillingCycleYearly:
return cycleStart.AddDate(1, 0, 0).Add(-time.Nanosecond)
default:
return cycleStart.AddDate(0, 1, 0).Add(-time.Nanosecond) // Default to monthly
}
}
@@ -0,0 +1,196 @@
package rateplan
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
func (s *Service) SubscribeToPlan(ctx context.Context, req *SubscribeRequest) (*RatePlanSubscription, error) {
if err := s.validateSubscribeRequest(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
plan, err := s.repo.GetRatePlan(ctx, req.RatePlanID)
if err != nil {
return nil, err
}
if !plan.IsActive || plan.Status != PlanStatusActive {
return nil, fmt.Errorf("rate plan is not available for subscription")
}
activeSub, err := s.repo.GetActiveSubscription(ctx, req.ProfileID)
if err != nil {
return nil, err
}
if activeSub != nil {
return nil, fmt.Errorf("profile already has an active subscription")
}
// Create subscription
subscription := &RatePlanSubscription{
ID: uuid.New().String(),
ProfileID: req.ProfileID,
RatePlanID: req.RatePlanID,
Status: SubscriptionStatusActive,
StartedAt: time.Now(),
BillingCycle: plan.BillingCycle,
NextBillingDate: s.calculateNextBillingDate(plan.BillingCycle, time.Now()),
AutoRenew: req.AutoRenew,
CurrentCycle: time.Now(),
AppliedDiscounts: req.AppliedDiscounts,
Metadata: req.Metadata,
}
if err := s.repo.CreateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to create subscription")
return nil, err
}
s.logger.WithFields(logrus.Fields{
"subscription_id": subscription.ID,
"profile_id": req.ProfileID,
"rate_plan_id": req.RatePlanID,
}).Info("Subscription created successfully")
return subscription, nil
}
// GetSubscription retrieves a subscription by ID
func (s *Service) GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error) {
subscription, err := s.repo.GetSubscription(ctx, id)
if err != nil {
s.logger.WithError(err).WithField("subscription_id", id).Error("Failed to get subscription")
return nil, err
}
return subscription, nil
}
// UpdateSubscription updates an existing subscription
func (s *Service) UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) (*RatePlanSubscription, error) {
if err := s.repo.UpdateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to update subscription")
return nil, err
}
s.logger.WithField("subscription_id", subscription.ID).Info("Subscription updated successfully")
return subscription, nil
}
// CancelSubscription cancels a subscription
func (s *Service) CancelSubscription(ctx context.Context, subscriptionID string, reason string) error {
subscription, err := s.repo.GetSubscription(ctx, subscriptionID)
if err != nil {
return err
}
if subscription.Status != SubscriptionStatusActive {
return fmt.Errorf("subscription is not active")
}
now := time.Now()
subscription.Status = SubscriptionStatusCancelled
subscription.EndedAt = &now
subscription.UpdatedAt = now
if subscription.Metadata == nil {
subscription.Metadata = make(map[string]interface{})
}
subscription.Metadata["cancellation_reason"] = reason
if err := s.repo.UpdateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to cancel subscription")
return err
}
s.logger.WithField("subscription_id", subscriptionID).Info("Subscription cancelled successfully")
return nil
}
// GetActiveSubscription retrieves the active subscription for a profile
func (s *Service) GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error) {
subscription, err := s.repo.GetActiveSubscription(ctx, profileID)
if err != nil {
s.logger.WithError(err).WithField("profile_id", profileID).Error("Failed to get active subscription")
return nil, err
}
return subscription, nil
}
// ListSubscriptions retrieves subscriptions for a profile
func (s *Service) ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error) {
subscriptions, err := s.repo.ListSubscriptions(ctx, profileID, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to list subscriptions")
return nil, err
}
return subscriptions, nil
}
func (s *Service) RecordUsage(ctx context.Context, req *RecordUsageRequest) (*RatePlanUsage, error) {
// Get active subscription
subscription, err := s.repo.GetActiveSubscription(ctx, req.ProfileID)
if err != nil {
return nil, err
}
if subscription == nil {
return nil, fmt.Errorf("no active subscription found")
}
// Get current usage
currentUsage, err := s.repo.GetCurrentUsage(ctx, req.ProfileID)
if err != nil {
return nil, err
}
// Create or update usage record
var usage *RatePlanUsage
if currentUsage == nil {
// Create new usage record
usage = &RatePlanUsage{
ID: uuid.New().String(),
RatePlanID: subscription.RatePlanID,
ProfileID: req.ProfileID,
CycleStart: subscription.CurrentCycle,
CycleEnd: s.calculateCycleEnd(subscription.BillingCycle, subscription.CurrentCycle),
DataUsed: req.DataUsed,
VoiceUsed: req.VoiceUsed,
SMSUsed: req.SMSUsed,
}
if err := s.repo.CreateUsage(ctx, usage); err != nil {
s.logger.WithError(err).Error("Failed to create usage record")
return nil, err
}
} else {
// Update existing usage record
currentUsage.DataUsed += req.DataUsed
currentUsage.VoiceUsed += req.VoiceUsed
currentUsage.SMSUsed += req.SMSUsed
if err := s.repo.UpdateUsage(ctx, currentUsage); err != nil {
s.logger.WithError(err).Error("Failed to update usage record")
return nil, err
}
usage = currentUsage
}
s.logger.WithFields(logrus.Fields{
"profile_id": req.ProfileID,
"data_used": req.DataUsed,
"voice_used": req.VoiceUsed,
"sms_used": req.SMSUsed,
}).Info("Usage recorded successfully")
return usage, nil
}