Skip to content

Commit

Permalink
fix: trialing subscriptions failed to sync (#702)
Browse files Browse the repository at this point in the history
- Triling subscriptions were filtered out from the periodic syncer
causing for it to stay in trialing state.
- Buying a trialing subscription will be canceled only after payment
is finished

Signed-off-by: Kush Sharma <[email protected]>
  • Loading branch information
kushsharma authored Jul 30, 2024
1 parent 6f71c43 commit 2ecd3e5
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 15 deletions.
46 changes: 36 additions & 10 deletions billing/checkout/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,8 +550,7 @@ func (s *Service) SyncWithProvider(ctx context.Context, customerID string) error
errs = append(errs, fmt.Errorf("ensureSubscription: %w", err))
continue
}
}
if ch.ProductID != "" {
} else if ch.ProductID != "" {
// if the checkout was created for product
if err := s.ensureCreditsForProduct(ctx, ch); err != nil {
errs = append(errs, fmt.Errorf("ensureCreditsForProduct: %w", err))
Expand Down Expand Up @@ -620,15 +619,11 @@ func (s *Service) checkIfAlreadySubscribed(ctx context.Context, ch Checkout) (st
}

for _, sub := range subs {
// cancel immediately if trialing
if ch.State == subscription.StateTrialing.String() && !sub.TrialEndsAt.IsZero() {
if _, err := s.subscriptionService.Cancel(ctx, sub.ID, true); err != nil {
return "", fmt.Errorf("failed to cancel trialing subscription: %w", err)
}
continue
}
// don't care about canceled or ended subscriptions
if sub.State == subscription.StateCanceled.String() || sub.State == subscription.StateEnded.String() {
// trialing subscriptions will be canceled later
if sub.State == subscription.StateCanceled.String() ||
sub.State == subscription.StateEnded.String() ||
sub.State == subscription.StateTrialing.String() {
continue
}
// subscription already exists
Expand All @@ -638,6 +633,28 @@ func (s *Service) checkIfAlreadySubscribed(ctx context.Context, ch Checkout) (st
return "", nil
}

func (s *Service) cancelTrialingSubscription(ctx context.Context, customerID string, planID string) error {
// check if subscription exists
subs, err := s.subscriptionService.List(ctx, subscription.Filter{
CustomerID: customerID,
PlanID: planID,
})
if err != nil {
return err
}

for _, sub := range subs {
// cancel immediately if trialing
if sub.State == subscription.StateTrialing.String() && !sub.TrialEndsAt.IsZero() {
if _, err := s.subscriptionService.Cancel(ctx, sub.ID, true); err != nil {
return fmt.Errorf("failed to cancel trialing subscription: %w", err)
}
}
}

return nil
}

func (s *Service) ensureSubscription(ctx context.Context, ch Checkout) (string, error) {
if ch.Metadata[ProviderIDSubscriptionMetadataKey] == nil {
return "", fmt.Errorf("invalid checkout session, provider_subscription_id is missing")
Expand All @@ -654,6 +671,11 @@ func (s *Service) ensureSubscription(ctx context.Context, ch Checkout) (string,
return "", nil
}

// cancel existing trials if any
if err := s.cancelTrialingSubscription(ctx, ch.CustomerID, ch.PlanID); err != nil {
return "", err
}

stripeSubscription, err := s.stripeClient.Subscriptions.Get(subProviderID,
&stripe.SubscriptionParams{
Params: stripe.Params{
Expand Down Expand Up @@ -814,6 +836,10 @@ func (s *Service) Apply(ctx context.Context, ch Checkout) (*subscription.Subscri
return nil, nil, fmt.Errorf("already subscribed to the plan")
}

if err := s.cancelTrialingSubscription(ctx, ch.CustomerID, ch.PlanID); err != nil {
return nil, nil, err
}

// create subscription items
var subsItems []*stripe.SubscriptionItemsParams
userCount, err := s.orgService.MemberCount(ctx, billingCustomer.OrgID)
Expand Down
17 changes: 13 additions & 4 deletions billing/subscription/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (s *Service) SyncWithProvider(ctx context.Context, customr customer.Custome
subErrs = append(subErrs, err)
}
} else {
subErrs = append(subErrs, err)
subErrs = append(subErrs, fmt.Errorf("%s: %w", sub.ID, err))
}
} else {
subErrs = append(subErrs, err)
Expand Down Expand Up @@ -353,8 +353,11 @@ func (s *Service) Cancel(ctx context.Context, id string, immediate bool) (Subscr
return Subscription{}, err
}
if !sub.CanceledAt.IsZero() {
// already canceled, no-op
return sub, nil
if !immediate {
// already canceled, no-op
return sub, nil
}
// already canceled, but now we need to cancel immediately, go ahead
}

// check if schedule exists
Expand Down Expand Up @@ -809,7 +812,7 @@ func (s *Service) ChangePlan(ctx context.Context, id string, changeRequest Chang
}

// update subscription with new phase
_, nextPlanID, err := s.getPlanFromSchedule(ctx, updatedSchedule)
currentPlanID, nextPlanID, err := s.getPlanFromSchedule(ctx, updatedSchedule)
if err != nil {
return change, err
}
Expand All @@ -818,6 +821,12 @@ func (s *Service) ChangePlan(ctx context.Context, id string, changeRequest Chang
}
sub.Phase.Reason = SubscriptionChange.String()
sub.Phase.PlanID = nextPlanID
if nextPlanID == "" {
// if there is no next plan, it means the change was instant
sub.Phase.PlanID = currentPlanID
sub.Phase.EffectiveAt = utils.AsTimeFromEpoch(updatedSchedule.CurrentPhase.StartDate)
}

sub, err = s.repository.UpdateByID(ctx, sub)
if err != nil {
return change, err
Expand Down
2 changes: 1 addition & 1 deletion billing/subscription/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (s Subscription) IsActive() bool {
}

func (s Subscription) IsCanceled() bool {
return State(s.State) == StateCanceled || !s.DeletedAt.IsZero() || !s.CanceledAt.IsZero()
return State(s.State) == StateCanceled || !s.DeletedAt.IsZero()
}

type Filter struct {
Expand Down

0 comments on commit 2ecd3e5

Please sign in to comment.