Sponsored sub cleanup on unsubscribe

Sponsored subscriptions can be partially set up when the incremental
subscription charge fails.  This cleans up the sponsored subscriptions
when one of the recipient subscriptions is cancelled.

Related to #2786
This commit is contained in:
Matt Lott 2015-05-28 16:21:08 -07:00
parent 5a272f8bed
commit 9828bd5d1d
3 changed files with 90 additions and 23 deletions

View file

@ -592,7 +592,7 @@ class SubscriptionHandler extends Handler
# Check recipient is currently sponsored # Check recipient is currently sponsored
stripeRecipient = recipient.get 'stripe' ? {} 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) @logSubscriptionError(user, "Recipient #{req.body.stripe.recipient} not found. " + err)
return done({res: 'Can only unsubscribe sponsored subscriptions.', code: 403}) return done({res: 'Can only unsubscribe sponsored subscriptions.', code: 403})

View file

@ -141,6 +141,15 @@ module.exports.setup = (app) ->
checkRecipientSubscription = (done) -> checkRecipientSubscription = (done) ->
return done() unless subscription.plan.id is 'basic' return done() unless subscription.plan.id is 'basic'
return done() unless subscription.metadata?.id # Shouldn't be possible 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) => User.findById subscription.metadata.id, (err, recipient) =>
if err if err
logStripeWebhookError(err) logStripeWebhookError(err)
@ -158,33 +167,55 @@ module.exports.setup = (app) ->
# Update sponsor subscription # Update sponsor subscription
stripeInfo = _.cloneDeep(sponsor.get('stripe') ? {}) stripeInfo = _.cloneDeep(sponsor.get('stripe') ? {})
_.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id) stripeInfo.recipients ?= []
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, '')
# Update sponsor user if stripeInfo.sponsorSubscriptionID
sponsor.set 'stripe', stripeInfo _.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id)
sponsor.save (err) => options =
quantity: utils.getSponsoredSubsAmount(subscription.plan.amount, stripeInfo.recipients.length, stripeInfo.subscriptionID?)
stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
if err if err
logStripeWebhookError(err) logStripeWebhookError(err)
return res.send(500, '') return res.send(500, '')
# Update recipient user # Update sponsor user
stripeInfo = recipient.get('stripe') sponsor.set 'stripe', stripeInfo
delete stripeInfo.sponsorID sponsor.save (err) =>
if _.isEmpty stripeInfo if err
recipient.set 'stripe', undefined logStripeWebhookError(err)
else return res.send(500, '')
recipient.set 'stripe', stripeInfo
recipient.save (err) => # 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 if err
logStripeWebhookError(err) logStripeWebhookError(err)
return res.send(500, '') return res.send(500, '')

View file

@ -291,6 +291,7 @@ describe 'Subscriptions', ->
expect(numSponsored).toBeGreaterThan(0) expect(numSponsored).toBeGreaterThan(0)
# Verify Stripe sponsor subscription data # Verify Stripe sponsor subscription data
return done() unless sponsorCustomerID and sponsorStripe.sponsorSubscriptionID
stripe.customers.retrieveSubscription sponsorCustomerID, sponsorStripe.sponsorSubscriptionID, (err, subscription) -> stripe.customers.retrieveSubscription sponsorCustomerID, sponsorStripe.sponsorSubscriptionID, (err, subscription) ->
expect(err).toBeNull() expect(err).toBeNull()
expect(subscription.plan.amount).toEqual(1) expect(subscription.plan.amount).toEqual(1)
@ -328,6 +329,7 @@ describe 'Subscriptions', ->
expect(recipientInfo.couponID).toEqual('free') expect(recipientInfo.couponID).toEqual('free')
# Verify Stripe recipient subscription data # Verify Stripe recipient subscription data
return done() unless sponsorCustomerID and recipientInfo.subscriptionID
stripe.customers.retrieveSubscription sponsorCustomerID, recipientInfo.subscriptionID, (err, subscription) -> stripe.customers.retrieveSubscription sponsorCustomerID, recipientInfo.subscriptionID, (err, subscription) ->
expect(err).toBeNull() expect(err).toBeNull()
expect(subscription.plan.amount).toEqual(subPrice) expect(subscription.plan.amount).toEqual(subPrice)
@ -445,6 +447,8 @@ describe 'Subscriptions', ->
# console.log 'unsubscribeRecipient', sponsor.id, recipient.id # console.log 'unsubscribeRecipient', sponsor.id, recipient.id
stripeInfo = sponsor.get('stripe') stripeInfo = sponsor.get('stripe')
customerID = stripeInfo.customerID customerID = stripeInfo.customerID
expect(stripeInfo.recipients).toBeDefined()
return done() unless stripeInfo.recipients
for r in stripeInfo.recipients for r in stripeInfo.recipients
if r.userID is recipient.id if r.userID is recipient.id
subscriptionID = r.subscriptionID subscriptionID = r.subscriptionID
@ -739,6 +743,38 @@ describe 'Subscriptions', ->
expect(user1.isPremium()).toEqual(true) expect(user1.isPremium()).toEqual(true)
verifySponsorship user1.id, user2.id, done 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) -> it 'Unsubscribed user1 unsubscribes user2 and their sub ends', (done) ->
stripe.tokens.create { stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }