159 lines
4.7 KiB
Go
159 lines
4.7 KiB
Go
package subscriptions
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"Yimaru-Backend/internal/ports"
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrPlanNotFound = errors.New("subscription plan not found")
|
|
ErrSubscriptionNotFound = errors.New("subscription not found")
|
|
ErrAlreadySubscribed = errors.New("user already has an active subscription")
|
|
ErrInvalidPlan = errors.New("invalid subscription plan")
|
|
)
|
|
|
|
type Service struct {
|
|
store ports.SubscriptionStore
|
|
}
|
|
|
|
func NewService(store ports.SubscriptionStore) *Service {
|
|
return &Service{store: store}
|
|
}
|
|
|
|
// =====================
|
|
// Subscription Plans
|
|
// =====================
|
|
|
|
func (s *Service) CreatePlan(ctx context.Context, input domain.CreateSubscriptionPlanInput) (*domain.SubscriptionPlan, error) {
|
|
return s.store.CreateSubscriptionPlan(ctx, input)
|
|
}
|
|
|
|
func (s *Service) GetPlanByID(ctx context.Context, id int64) (*domain.SubscriptionPlan, error) {
|
|
return s.store.GetSubscriptionPlanByID(ctx, id)
|
|
}
|
|
|
|
func (s *Service) ListPlans(ctx context.Context, activeOnly bool) ([]domain.SubscriptionPlan, error) {
|
|
return s.store.ListSubscriptionPlans(ctx, activeOnly)
|
|
}
|
|
|
|
func (s *Service) UpdatePlan(ctx context.Context, id int64, input domain.UpdateSubscriptionPlanInput) error {
|
|
return s.store.UpdateSubscriptionPlan(ctx, id, input)
|
|
}
|
|
|
|
func (s *Service) DeletePlan(ctx context.Context, id int64) error {
|
|
return s.store.DeleteSubscriptionPlan(ctx, id)
|
|
}
|
|
|
|
// =====================
|
|
// User Subscriptions
|
|
// =====================
|
|
|
|
// Subscribe creates a new subscription for a user
|
|
func (s *Service) Subscribe(ctx context.Context, userID, planID int64, paymentRef, paymentMethod *string) (*domain.UserSubscription, error) {
|
|
// Check if user already has an active subscription
|
|
hasActive, err := s.store.HasActiveSubscription(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hasActive {
|
|
return nil, ErrAlreadySubscribed
|
|
}
|
|
|
|
// Get the plan to calculate expiry
|
|
plan, err := s.store.GetSubscriptionPlanByID(ctx, planID)
|
|
if err != nil {
|
|
return nil, ErrPlanNotFound
|
|
}
|
|
if !plan.IsActive {
|
|
return nil, ErrInvalidPlan
|
|
}
|
|
|
|
// Calculate expiry date
|
|
startsAt := time.Now()
|
|
expiresAt := domain.CalculateExpiryDate(startsAt, plan.DurationValue, plan.DurationUnit)
|
|
|
|
input := domain.CreateUserSubscriptionInput{
|
|
UserID: userID,
|
|
PlanID: planID,
|
|
StartsAt: &startsAt,
|
|
ExpiresAt: expiresAt,
|
|
Status: strPtr(string(domain.SubscriptionStatusActive)),
|
|
PaymentReference: paymentRef,
|
|
PaymentMethod: paymentMethod,
|
|
AutoRenew: boolPtr(false),
|
|
}
|
|
|
|
return s.store.CreateUserSubscription(ctx, input)
|
|
}
|
|
|
|
func (s *Service) GetSubscriptionByID(ctx context.Context, id int64) (*domain.UserSubscription, error) {
|
|
return s.store.GetUserSubscriptionByID(ctx, id)
|
|
}
|
|
|
|
func (s *Service) GetActiveSubscription(ctx context.Context, userID int64) (*domain.UserSubscription, error) {
|
|
return s.store.GetActiveSubscriptionByUserID(ctx, userID)
|
|
}
|
|
|
|
func (s *Service) GetSubscriptionHistory(ctx context.Context, userID int64, limit, offset int32) ([]domain.UserSubscription, error) {
|
|
return s.store.GetUserSubscriptionHistory(ctx, userID, limit, offset)
|
|
}
|
|
|
|
func (s *Service) HasActiveSubscription(ctx context.Context, userID int64) (bool, error) {
|
|
return s.store.HasActiveSubscription(ctx, userID)
|
|
}
|
|
|
|
func (s *Service) CancelSubscription(ctx context.Context, subscriptionID int64) error {
|
|
return s.store.CancelUserSubscription(ctx, subscriptionID)
|
|
}
|
|
|
|
func (s *Service) SetAutoRenew(ctx context.Context, subscriptionID int64, autoRenew bool) error {
|
|
return s.store.UpdateAutoRenew(ctx, subscriptionID, autoRenew)
|
|
}
|
|
|
|
// RenewSubscription extends an existing subscription
|
|
func (s *Service) RenewSubscription(ctx context.Context, subscriptionID int64) (*domain.UserSubscription, error) {
|
|
sub, err := s.store.GetUserSubscriptionByID(ctx, subscriptionID)
|
|
if err != nil {
|
|
return nil, ErrSubscriptionNotFound
|
|
}
|
|
|
|
plan, err := s.store.GetSubscriptionPlanByID(ctx, sub.PlanID)
|
|
if err != nil {
|
|
return nil, ErrPlanNotFound
|
|
}
|
|
|
|
// Calculate new expiry from current expiry (or now if expired)
|
|
baseTime := sub.ExpiresAt
|
|
if baseTime.Before(time.Now()) {
|
|
baseTime = time.Now()
|
|
}
|
|
newExpiry := domain.CalculateExpiryDate(baseTime, plan.DurationValue, plan.DurationUnit)
|
|
|
|
err = s.store.ExtendSubscription(ctx, subscriptionID, newExpiry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Also reactivate if expired
|
|
if sub.Status == string(domain.SubscriptionStatusExpired) {
|
|
err = s.store.UpdateSubscriptionStatus(ctx, subscriptionID, string(domain.SubscriptionStatusActive))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return s.store.GetUserSubscriptionByID(ctx, subscriptionID)
|
|
}
|
|
|
|
// Helper functions
|
|
func strPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
func boolPtr(b bool) *bool {
|
|
return &b
|
|
}
|