diff --git a/server/payments/subscription_handler.coffee b/server/payments/subscription_handler.coffee index 03af49731..5cd7e300b 100644 --- a/server/payments/subscription_handler.coffee +++ b/server/payments/subscription_handler.coffee @@ -592,7 +592,7 @@ class SubscriptionHandler extends Handler # Check recipient is currently sponsored stripeRecipient = recipient.get 'stripe' ? {} - if stripeRecipient.sponsorID isnt user.id + if stripeRecipient?.sponsorID isnt user.id @logSubscriptionError(user, "Recipient #{req.body.stripe.recipient} not found. " + err) return done({res: 'Can only unsubscribe sponsored subscriptions.', code: 403}) diff --git a/server/routes/stripe.coffee b/server/routes/stripe.coffee index aa1c7f3c1..0ef5abb96 100644 --- a/server/routes/stripe.coffee +++ b/server/routes/stripe.coffee @@ -141,6 +141,15 @@ module.exports.setup = (app) -> checkRecipientSubscription = (done) -> return done() unless subscription.plan.id is 'basic' return done() unless subscription.metadata?.id # Shouldn't be possible + + deleteUserStripeProp = (user, propName) -> + stripeInfo = _.cloneDeep(user.get('stripe') ? {}) + delete stripeInfo[propName] + if _.isEmpty stripeInfo + user.set 'stripe', undefined + else + user.set 'stripe', stripeInfo + User.findById subscription.metadata.id, (err, recipient) => if err logStripeWebhookError(err) @@ -158,33 +167,55 @@ module.exports.setup = (app) -> # Update sponsor subscription stripeInfo = _.cloneDeep(sponsor.get('stripe') ? {}) - _.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id) - options = - quantity: utils.getSponsoredSubsAmount(subscription.plan.amount, stripeInfo.recipients.length, stripeInfo.subscriptionID?) - unless stripeInfo.sponsorSubscriptionID - # TODO: fix #2786 error for a particular customer which doesn't have this - console.error "Couldn't find sponsorSubscriptionID from stripeInfo", stripeInfo, 'for customer', stripeInfo.customerID, 'with options', options, 'and subscription', subscription, 'for user', recipient.id, 'with sponsor', sponsor.id - return res.send(500, '') - stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) => - if err - logStripeWebhookError(err) - return res.send(500, '') + stripeInfo.recipients ?= [] - # Update sponsor user - sponsor.set 'stripe', stripeInfo - sponsor.save (err) => + if stripeInfo.sponsorSubscriptionID + _.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id) + options = + quantity: utils.getSponsoredSubsAmount(subscription.plan.amount, stripeInfo.recipients.length, stripeInfo.subscriptionID?) + stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) => if err logStripeWebhookError(err) return res.send(500, '') - # Update recipient user - stripeInfo = recipient.get('stripe') - delete stripeInfo.sponsorID - if _.isEmpty stripeInfo - recipient.set 'stripe', undefined - else - recipient.set 'stripe', stripeInfo - recipient.save (err) => + # Update sponsor user + sponsor.set 'stripe', stripeInfo + sponsor.save (err) => + if err + logStripeWebhookError(err) + return res.send(500, '') + + # Update recipient user + deleteUserStripeProp recipient, 'sponsorID' + recipient.save (err) => + if err + logStripeWebhookError(err) + return res.send(500, '') + return res.send(200, '') + else + # Remove sponsorships from sponsor and recipients + console.error "Couldn't find sponsorSubscriptionID from stripeInfo", stripeInfo, 'for customer', stripeInfo.customerID, 'with options', options, 'and subscription', subscription, 'for user', recipient.id, 'with sponsor', sponsor.id + + # Update recipients + createUpdateFn = (recipientID) -> + (callback) -> + User.findById recipientID, (err, recipient) => + if err + logStripeWebhookError(err) + return callback(err) + + deleteUserStripeProp recipient, 'sponsorID' + recipient.save (err) => + logStripeWebhookError(err) if err + callback(err) + async.parallel (createUpdateFn(recipient.userID) for recipient in stripeInfo.recipients), (err, results) => + if err + logStripeWebhookError(err) + return res.send(500, '') + + # Update sponsor + deleteUserStripeProp sponsor, 'recipients' + sponsor.save (err) => if err logStripeWebhookError(err) return res.send(500, '') diff --git a/test/server/functional/subscription.spec.coffee b/test/server/functional/subscription.spec.coffee index f2aa838ac..ed99b8c2a 100644 --- a/test/server/functional/subscription.spec.coffee +++ b/test/server/functional/subscription.spec.coffee @@ -291,6 +291,7 @@ describe 'Subscriptions', -> expect(numSponsored).toBeGreaterThan(0) # Verify Stripe sponsor subscription data + return done() unless sponsorCustomerID and sponsorStripe.sponsorSubscriptionID stripe.customers.retrieveSubscription sponsorCustomerID, sponsorStripe.sponsorSubscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription.plan.amount).toEqual(1) @@ -328,6 +329,7 @@ describe 'Subscriptions', -> expect(recipientInfo.couponID).toEqual('free') # Verify Stripe recipient subscription data + return done() unless sponsorCustomerID and recipientInfo.subscriptionID stripe.customers.retrieveSubscription sponsorCustomerID, recipientInfo.subscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription.plan.amount).toEqual(subPrice) @@ -445,6 +447,8 @@ describe 'Subscriptions', -> # console.log 'unsubscribeRecipient', sponsor.id, recipient.id stripeInfo = sponsor.get('stripe') customerID = stripeInfo.customerID + expect(stripeInfo.recipients).toBeDefined() + return done() unless stripeInfo.recipients for r in stripeInfo.recipients if r.userID is recipient.id subscriptionID = r.subscriptionID @@ -739,6 +743,38 @@ describe 'Subscriptions', -> expect(user1.isPremium()).toEqual(true) verifySponsorship user1.id, user2.id, done + it 'Clean up sponsorships upon sub cancel after setup sponsor sub fails', (done) -> + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> + createNewUser (user2) -> + loginNewUser (user1) -> + subscribeUser user1, token, null, (updatedUser) -> + User.findById user1.id, (err, user1) -> + expect(err).toBeNull() + subscribeRecipients user1, [user2], null, (updatedUser) -> + + # Delete user1 sponsorSubscriptionID to simulate failed sponsor sub + User.findById user1.id, (err, user1) -> + expect(err).toBeNull() + stripeInfo = _.cloneDeep(user1.get('stripe') ? {}) + delete stripeInfo.sponsorSubscriptionID + user1.set 'stripe', stripeInfo + user1.save (err, user1) -> + expect(err).toBeNull() + + User.findById user1.id, (err, user1) -> + unsubscribeRecipient user1, user2, true, -> + User.findById user1.id, (err, user1) -> + expect(err).toBeNull() + expect(user1.get('stripe').subscriptionID).toBeDefined() + expect(user1.get('stripe').recipients).toBeUndefined() + expect(user1.isPremium()).toEqual(true) + User.findById user2.id, (err, user2) -> + verifyNotSponsoring user1.id, user2.id, -> + verifyNotRecipient user2.id, done + + it 'Unsubscribed user1 unsubscribes user2 and their sub ends', (done) -> stripe.tokens.create { card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }