mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
Merge branch 'sponsored-subs'
This commit is contained in:
parent
c32dd6d284
commit
af89452b93
17 changed files with 1807 additions and 210 deletions
|
@ -184,3 +184,17 @@ module.exports.getQueryVariable = getQueryVariable = (param, defaultValue) ->
|
||||||
for pair in pairs when pair[0] is param
|
for pair in pairs when pair[0] is param
|
||||||
return {'true': true, 'false': false}[pair[1]] ? decodeURIComponent(pair[1])
|
return {'true': true, 'false': false}[pair[1]] ? decodeURIComponent(pair[1])
|
||||||
defaultValue
|
defaultValue
|
||||||
|
|
||||||
|
module.exports.getSponsoredSubsAmount = getSponsoredSubsAmount = (price=999, subCount=0, personalSub=false) ->
|
||||||
|
# 1 100%
|
||||||
|
# 2-11 80%
|
||||||
|
# 12+ 60%
|
||||||
|
# TODO: make this less confusing
|
||||||
|
return 0 unless subCount > 0
|
||||||
|
offset = if personalSub then 1 else 0
|
||||||
|
if subCount <= 1 - offset
|
||||||
|
price
|
||||||
|
else if subCount <= 11 - offset
|
||||||
|
Math.round((1 - offset) * price + (subCount - 1 + offset) * price * 0.8)
|
||||||
|
else
|
||||||
|
Math.round((1 - offset) * price + 10 * price * 0.8 + (subCount - 11 + offset) * price * 0.6)
|
||||||
|
|
|
@ -432,6 +432,31 @@
|
||||||
subscription_required_to_play: "You'll need a subscription to play this level."
|
subscription_required_to_play: "You'll need a subscription to play this level."
|
||||||
unlock_help_videos: "Subscribe to unlock all video tutorials."
|
unlock_help_videos: "Subscribe to unlock all video tutorials."
|
||||||
|
|
||||||
|
# Accounts Subscription View
|
||||||
|
personal_sub: "Personal Subscription"
|
||||||
|
loading_info: "Loading subscription information..."
|
||||||
|
managed_by: "Managed by"
|
||||||
|
will_be_cancelled: "Will be cancelled on"
|
||||||
|
currently_free: "You currently have a free subscription"
|
||||||
|
currently_free_until: "You currently have a free subscription until"
|
||||||
|
was_free_until: "You had a free subscription until"
|
||||||
|
managed_subs: "Managed Subscriptions"
|
||||||
|
managed_subs_desc: "Add subscriptions for other players (students, children, etc.)"
|
||||||
|
group_discounts: "Group discounts"
|
||||||
|
group_discounts_1st: "1st subscription (includes yours)"
|
||||||
|
group_discounts_full: "Full price"
|
||||||
|
group_discounts_2nd: "Subscriptions 2-11"
|
||||||
|
group_discounts_20: "20% off"
|
||||||
|
group_discounts_12th: "Subscriptions 12+"
|
||||||
|
group_discounts_40: "40% off"
|
||||||
|
subscribing: "Subscribing..."
|
||||||
|
recipient_emails_placeholder: "Enter email address to subscribe, one per line."
|
||||||
|
subscribe_users: "Subscribe Users"
|
||||||
|
users_subscribed: "Users subscribed:"
|
||||||
|
no_users_subscribed: "No users subscribed, please double check your email addresses."
|
||||||
|
current_recipients: "Current Recipients"
|
||||||
|
unsubscribing: "Unsubscribing..."
|
||||||
|
|
||||||
choose_hero:
|
choose_hero:
|
||||||
choose_hero: "Choose Your Hero"
|
choose_hero: "Choose Your Hero"
|
||||||
programming_language: "Programming Language"
|
programming_language: "Programming Language"
|
||||||
|
|
|
@ -139,6 +139,7 @@ module.exports = class User extends CocoModel
|
||||||
return true if me.isInGodMode()
|
return true if me.isInGodMode()
|
||||||
return true if me.isAdmin()
|
return true if me.isAdmin()
|
||||||
return false unless stripe = @get('stripe')
|
return false unless stripe = @get('stripe')
|
||||||
|
return true if stripe.sponsorID
|
||||||
return true if stripe.subscriptionID
|
return true if stripe.subscriptionID
|
||||||
return true if stripe.free is true
|
return true if stripe.free is true
|
||||||
return true if _.isString(stripe.free) and new Date() < new Date(stripe.free)
|
return true if _.isString(stripe.free) and new Date() < new Date(stripe.free)
|
||||||
|
|
|
@ -283,12 +283,22 @@ _.extend UserSchema.properties,
|
||||||
|
|
||||||
stripe: c.object {}, {
|
stripe: c.object {}, {
|
||||||
customerID: { type: 'string' }
|
customerID: { type: 'string' }
|
||||||
planID: { enum: ['basic'] }
|
planID: { enum: ['basic'], description: 'Determines if a user has or wants to subscribe' }
|
||||||
subscriptionID: { type: 'string' }
|
subscriptionID: { type: 'string', description: 'Determines if a user is subscribed' }
|
||||||
token: { type: 'string' }
|
token: { type: 'string' }
|
||||||
couponID: { type: 'string' }
|
couponID: { type: 'string' }
|
||||||
discountID: { type: 'string' }
|
free: { type: ['boolean', 'string'], format: 'date-time', description: 'Type string is subscription end date' }
|
||||||
free: { type: ['boolean', 'string'], format: 'date-time' }
|
|
||||||
|
# Sponsored subscriptions
|
||||||
|
subscribeEmails: c.array { description: 'Input for subscribing other users' }, c.shortString()
|
||||||
|
unsubscribeEmail: { type: 'string', description: 'Input for unsubscribing a sponsored user' }
|
||||||
|
recipients: c.array { title: 'Recipient subscriptions owned by this user' },
|
||||||
|
c.object { required: ['userID', 'subscriptionID'] },
|
||||||
|
userID: c.objectId { description: 'User ID of recipient' }
|
||||||
|
subscriptionID: { type: 'string' }
|
||||||
|
couponID: { type: 'string' }
|
||||||
|
sponsorID: c.objectId { description: "User ID that owns this user's subscription" }
|
||||||
|
sponsorSubscriptionID: { type: 'string', description: 'Sponsor aggregate subscription used to pay for all recipient subs' }
|
||||||
}
|
}
|
||||||
|
|
||||||
siteref: { type: 'string' }
|
siteref: { type: 'string' }
|
||||||
|
|
|
@ -16,3 +16,25 @@
|
||||||
button.btn
|
button.btn
|
||||||
width: 100%
|
width: 100%
|
||||||
margin-top: 12px
|
margin-top: 12px
|
||||||
|
|
||||||
|
// Sponsored subscriptions
|
||||||
|
|
||||||
|
.recipient-emails
|
||||||
|
min-width: 50%
|
||||||
|
|
||||||
|
.recipients-subscribe-button
|
||||||
|
margin-top: 10px
|
||||||
|
|
||||||
|
.recipient-unsubscribe-button
|
||||||
|
width: auto
|
||||||
|
|
||||||
|
.confirm-recipient-unsubscribe-button
|
||||||
|
width: auto
|
||||||
|
|
||||||
|
.discount-table
|
||||||
|
width: 50%
|
||||||
|
|
||||||
|
.recipients-table
|
||||||
|
width: 50%
|
||||||
|
.recipient-unsubscribe
|
||||||
|
text-align: right
|
||||||
|
|
|
@ -10,54 +10,161 @@ block content
|
||||||
a(href="/account", data-i18n="nav.account")
|
a(href="/account", data-i18n="nav.account")
|
||||||
li.active(data-i18n="account.subscription")
|
li.active(data-i18n="account.subscription")
|
||||||
|
|
||||||
.panel.panel-default
|
if me.get('anonymous')
|
||||||
.panel-heading
|
p(data-i18n="account_settings.not_logged_in")
|
||||||
if subscribed
|
else
|
||||||
button.end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.unsubscribe") Unsubscribe
|
|
||||||
.unsubscribe-feedback.row.secret
|
//- Personal Subscriptions
|
||||||
.col-lg-7
|
|
||||||
h3
|
.panel.panel-default
|
||||||
if monthsSubscribed > 1
|
.panel-heading
|
||||||
span(data-i18n="subscribe.thank_you_months_prefix") Thank you for supporting us these last
|
h3(data-i18n="subscribe.personal_sub")
|
||||||
span.spl.spr= monthsSubscribed
|
.panel-body
|
||||||
span(data-i18n="subscribe.thank_you_months_suffix") months.
|
if personalSub.state === 'loading'
|
||||||
else
|
.alert.alert-info(data-i18n="subscribe.loading_info")
|
||||||
span(data-i18n="subscribe.thank_you") Thank you for supporting CodeCombat.
|
else if personalSub.sponsor
|
||||||
div(data-i18n="subscribe.sorry_to_see_you_go") Sorry to see you go! Please let us know what we could have done better.
|
div
|
||||||
textarea(rows=3, data-i18n="[placeholder]subscribe.unsubscribe_feedback_placeholder")
|
span(data-i18n="subscribe.managed_by")
|
||||||
.col-lg-5
|
span.spl.spr #{personalSub.sponsorName} (#{personalSub.sponsorEmail})
|
||||||
button.cancel-end-subscription-button.btn.btn-lg.btn-default(data-i18n="subscribe.never_mind") Never Mind, I Still Love You
|
if personalSub.endDate
|
||||||
button.confirm-end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.confirm_unsubscribe") Confirm Unsubscribe
|
div
|
||||||
else if !me.isAnonymous()
|
span(data-i18n="subscribe.will_be_cancelled")
|
||||||
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
span.spl.spr= moment(personalSub.endDate).format('l')
|
||||||
|
else if personalSub.self
|
||||||
.panel-body
|
if personalSub.subscribed
|
||||||
table.table.table-striped
|
button.end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.unsubscribe") Unsubscribe
|
||||||
tr
|
else
|
||||||
th(data-i18n="user.status") Status
|
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
||||||
td
|
.unsubscribe-feedback.row.secret
|
||||||
if subscribed
|
.col-lg-7
|
||||||
strong(data-i18n="account.subscribed")
|
h3
|
||||||
|
if personalSub.monthsSubscribed > 1
|
||||||
|
span(data-i18n="subscribe.thank_you_months_prefix") Thank you for supporting us these last
|
||||||
|
span.spl.spr= personalSub.monthsSubscribed
|
||||||
|
span(data-i18n="subscribe.thank_you_months_suffix") months.
|
||||||
|
else
|
||||||
|
span(data-i18n="subscribe.thank_you") Thank you for supporting CodeCombat.
|
||||||
|
div(data-i18n="subscribe.sorry_to_see_you_go") Sorry to see you go! Please let us know what we could have done better.
|
||||||
|
textarea(rows=3, data-i18n="[placeholder]subscribe.unsubscribe_feedback_placeholder")
|
||||||
|
.col-lg-5
|
||||||
|
button.cancel-end-subscription-button.btn.btn-lg.btn-default(data-i18n="subscribe.never_mind") Never Mind, I Still Love You
|
||||||
|
button.confirm-end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.confirm_unsubscribe") Confirm Unsubscribe
|
||||||
|
|
||||||
|
table.table.table-striped.table-condensed
|
||||||
|
tr
|
||||||
|
th(data-i18n="user.status") Status
|
||||||
|
td
|
||||||
|
if personalSub.subscribed
|
||||||
|
strong(data-i18n="account.subscribed")
|
||||||
|
else
|
||||||
|
if personalSub.active
|
||||||
|
strong(data-i18n="account.active")
|
||||||
|
.text-muted(data-i18n="account.status_unsubscribed_active")
|
||||||
|
else
|
||||||
|
strong(data-i18n="account.unsubscribed")
|
||||||
|
.text-muted(data-i18n="account.status_unsubscribed")
|
||||||
|
if personalSub.activeUntil
|
||||||
|
tr
|
||||||
|
th(data-i18n="account.active_until")
|
||||||
|
td= moment(activeUntil).format('l')
|
||||||
|
if personalSub.nextPaymentDate
|
||||||
|
tr
|
||||||
|
th(data-i18n="account.next_payment")
|
||||||
|
td= moment(nextPaymentDate).format('l')
|
||||||
|
if personalSub.cost
|
||||||
|
tr
|
||||||
|
th(data-i18n="account.cost")
|
||||||
|
td= personalSub.cost
|
||||||
|
if personalSub.card
|
||||||
|
tr
|
||||||
|
th(data-i18n="account.card")
|
||||||
|
td= personalSub.card
|
||||||
|
|
||||||
|
else
|
||||||
|
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
||||||
|
if personalSub.free === true
|
||||||
|
div(data-i18n="subscribe.currently_free")
|
||||||
|
else if typeof personalSub.free === 'string'
|
||||||
|
if new Date() < new Date(personalSub.free)
|
||||||
|
div
|
||||||
|
span(data-i18n="subscribe.currently_free_until")
|
||||||
|
span.spl.spr= moment(new Date(personalSub.free)).format('l')
|
||||||
else
|
else
|
||||||
if active
|
span(data-i18n="subscribe.was_free_until")
|
||||||
strong(data-i18n="account.active")
|
span.spl.spr= moment(new Date(personalSub.free)).format('l')
|
||||||
.text-muted(data-i18n="account.status_unsubscribed_active")
|
|
||||||
else
|
//- Sponsored Subscriptions
|
||||||
strong(data-i18n="account.unsubscribed")
|
|
||||||
.text-muted(data-i18n="account.status_unsubscribed")
|
.panel.panel-default
|
||||||
if activeUntil
|
.panel-heading
|
||||||
|
h3(data-i18n="subscribe.managed_subs")
|
||||||
|
div(data-i18n="subscribe.managed_subs_desc")
|
||||||
|
h4(data-i18n="subscribe.group_discounts")
|
||||||
|
table.table.table-striped.table-condensed.discount-table
|
||||||
|
tr
|
||||||
|
td(data-i18n="subscribe.group_discounts_1st")
|
||||||
|
td(data-i18n="subscribe.group_discounts_full")
|
||||||
tr
|
tr
|
||||||
th(data-i18n="account.active_until")
|
td(data-i18n="subscribe.group_discounts_2nd")
|
||||||
td= moment(activeUntil).format('l')
|
td(data-i18n="subscribe.group_discounts_20")
|
||||||
if nextPaymentDate
|
|
||||||
tr
|
tr
|
||||||
th(data-i18n="account.next_payment")
|
td(data-i18n="subscribe.group_discounts_12th")
|
||||||
td= moment(nextPaymentDate).format('l')
|
td(data-i18n="subscribe.group_discounts_40")
|
||||||
if cost
|
.panel-body
|
||||||
|
if recipientSubs.state === 'subscribing'
|
||||||
|
.alert.alert-info(data-i18n="subscribe.subscribing")
|
||||||
|
else
|
||||||
|
textarea.recipient-emails(rows=3, data-i18n="[placeholder]subscribe.recipient_emails_placeholder")
|
||||||
|
div
|
||||||
|
button.recipients-subscribe-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_users")
|
||||||
|
if recipientSubs.state === 'declined'
|
||||||
|
br
|
||||||
|
.alert.alert-danger.alert-dismissible
|
||||||
|
span(data-i18n="buy_gems.declined")
|
||||||
|
button.close(type="button" data-dismiss="alert")
|
||||||
|
span(aria-hidden="true") ×
|
||||||
|
else if recipientSubs.state === 'unknown_error'
|
||||||
|
br
|
||||||
|
.alert.alert-danger.alert-dismissible
|
||||||
|
button.close(type="button" data-dismiss="alert")
|
||||||
|
span(aria-hidden="true") ×
|
||||||
|
p(data-i18n="loading_error.unknown")
|
||||||
|
p= stateMessage
|
||||||
|
else if recipientSubs.justSubscribed && recipientSubs.justSubscribed.length > 0
|
||||||
|
br
|
||||||
|
.alert.alert-success.alert-dismissible
|
||||||
|
if recipientSubs.justSubscribed.length > 0
|
||||||
|
div(data-i18n="subscribe.users_subscribed")
|
||||||
|
ul
|
||||||
|
each email in recipientSubs.justSubscribed
|
||||||
|
li= email
|
||||||
|
else if recipientSubs.justSubscribed && recipientSubs.justSubscribed.length === 0
|
||||||
|
br
|
||||||
|
.alert.alert-success.alert-dismissible
|
||||||
|
div(data-i18n="subscribe.no_users_subscribed")
|
||||||
|
|
||||||
|
if recipientSubs.nextPaymentAmount > 0 && recipientSubs.sponsorSub
|
||||||
|
h4(data-i18n="account.next_payment")
|
||||||
|
p= moment(new Date(recipientSubs.sponsorSub.current_period_end * 1000)).format('l')
|
||||||
|
p $#{recipientSubs.nextPaymentAmount / 100}
|
||||||
|
p= recipientSubs.card
|
||||||
|
|
||||||
|
h4(data-i18n="subscribe.current_recipients")
|
||||||
|
table.table.table-striped.table-condensed.recipients-table
|
||||||
tr
|
tr
|
||||||
th(data-i18n="account.cost")
|
th(data-i18n="general.email")
|
||||||
td= cost
|
th(data-i18n="general.name")
|
||||||
if card
|
th
|
||||||
tr
|
for recipient in recipientSubs.recipients
|
||||||
th(data-i18n="account.card")
|
tr
|
||||||
td= card
|
td.recipient-email= recipient.emailLower
|
||||||
|
td.recipient-name= recipient.name
|
||||||
|
td.recipient-unsubscribe
|
||||||
|
if recipient.cancel_at_period_end
|
||||||
|
div Ends #{moment(recipient.cancel_at_period_end).format('l')}
|
||||||
|
else if recipientSubs.unsubscribingRecipients.indexOf(recipient.emailLower) >= 0
|
||||||
|
div(data-i18n="subscribe.unsubscribing")
|
||||||
|
else
|
||||||
|
button.recipient-unsubscribe-button.btn.btn-sm.btn-warning Unsubscribe
|
||||||
|
button.confirm-recipient-unsubscribe-button.btn.btn-sm.btn-primary.hide(data-i18n="play.confirm")
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,24 @@ template = require 'templates/account/subscription-view'
|
||||||
CocoCollection = require 'collections/CocoCollection'
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
SubscribeModal = require 'views/core/SubscribeModal'
|
SubscribeModal = require 'views/core/SubscribeModal'
|
||||||
Payment = require 'models/Payment'
|
Payment = require 'models/Payment'
|
||||||
|
stripeHandler = require 'core/services/stripe'
|
||||||
|
User = require 'models/User'
|
||||||
|
utils = require 'core/utils'
|
||||||
|
|
||||||
|
# TODO: Link to sponsor id /user/userID instead of plain text name
|
||||||
|
# TODO: Link to sponsor email instead of plain text email
|
||||||
|
# TODO: Conslidate the multiple class for personal and recipient subscription info into 2 simple server API calls
|
||||||
|
# TODO: Track purchase amount based on actual users subscribed for a recipient subscribe event
|
||||||
|
# TODO: Validate email address formatting
|
||||||
|
# TODO: i18n pluralization for Stripe dialog description
|
||||||
|
# TODO: Don't prompt for new card if we have one already, just confirm purchase
|
||||||
|
# TODO: bulk discount isn't applied to personal sub
|
||||||
|
# TODO: next payment amount incorrect if have an expiring personal sub
|
||||||
|
# TODO: consider hiding managed subscription body UI while things are updating to avoid brief legacy data
|
||||||
|
# TODO: Next payment info for personal sub displays most recent payment when resubscribing before trial end
|
||||||
|
|
||||||
|
# TODO: Get basic plan price dynamically
|
||||||
|
basicPlanPrice = 999
|
||||||
|
|
||||||
module.exports = class SubscriptionView extends RootView
|
module.exports = class SubscriptionView extends RootView
|
||||||
id: "subscription-view"
|
id: "subscription-view"
|
||||||
|
@ -13,41 +31,29 @@ module.exports = class SubscriptionView extends RootView
|
||||||
'click .end-subscription-button': 'onClickEndSubscription'
|
'click .end-subscription-button': 'onClickEndSubscription'
|
||||||
'click .cancel-end-subscription-button': 'onClickCancelEndSubscription'
|
'click .cancel-end-subscription-button': 'onClickCancelEndSubscription'
|
||||||
'click .confirm-end-subscription-button': 'onClickConfirmEndSubscription'
|
'click .confirm-end-subscription-button': 'onClickConfirmEndSubscription'
|
||||||
|
'click .recipients-subscribe-button': 'onClickRecipientsSubscribe'
|
||||||
|
'click .confirm-recipient-unsubscribe-button': 'onClickRecipientConfirmUnsubscribe'
|
||||||
|
'click .recipient-unsubscribe-button': 'onClickRecipientUnsubscribe'
|
||||||
|
|
||||||
subscriptions:
|
subscriptions:
|
||||||
'subscribe-modal:subscribed': 'onSubscribed'
|
'subscribe-modal:subscribed': 'onSubscribed'
|
||||||
|
'stripe:received-token': 'onStripeReceivedToken'
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
super(options)
|
super(options)
|
||||||
if me.get('stripe')
|
@personalSub = new PersonalSub(@supermodel)
|
||||||
options = { cache: false, url: "/db/user/#{me.id}/stripe" }
|
@recipientSubs = new RecipientSubs(@supermodel)
|
||||||
options.success = (@stripeInfo) =>
|
@personalSub.update => @render?()
|
||||||
@supermodel.addRequestResource('payment_info', options).load()
|
@recipientSubs.update => @render?()
|
||||||
@payments = new CocoCollection([], { url: '/db/payment', model: Payment, comparator:'_id' })
|
|
||||||
@supermodel.loadCollection(@payments, 'payments', {cache: false})
|
|
||||||
|
|
||||||
getRenderData: ->
|
getRenderData: ->
|
||||||
c = super()
|
c = super()
|
||||||
if @stripeInfo
|
c.personalSub = @personalSub
|
||||||
if subscription = @stripeInfo.subscriptions?.data?[0]
|
c.recipientSubs = @recipientSubs
|
||||||
periodEnd = new Date((subscription.trial_end or subscription.current_period_end) * 1000)
|
|
||||||
if subscription.cancel_at_period_end
|
|
||||||
c.activeUntil = periodEnd
|
|
||||||
else
|
|
||||||
c.nextPaymentDate = periodEnd
|
|
||||||
c.cost = "$#{(subscription.plan.amount/100).toFixed(2)}"
|
|
||||||
if card = @stripeInfo.sources?.data?[0]
|
|
||||||
c.card = "#{card.brand}: x#{card.last4}"
|
|
||||||
if @payments?.loaded
|
|
||||||
c.monthsSubscribed = (x for x in @payments.models when not x.get('productID')).length # productID is for gem purchases
|
|
||||||
else
|
|
||||||
c.monthsSubscribed = null
|
|
||||||
|
|
||||||
c.stripeInfo = @stripeInfo
|
|
||||||
c.subscribed = me.get('stripe')?.planID
|
|
||||||
c.active = me.isPremium()
|
|
||||||
c
|
c
|
||||||
|
|
||||||
|
# Personal Subscriptions
|
||||||
|
|
||||||
onClickStartSubscription: (e) ->
|
onClickStartSubscription: (e) ->
|
||||||
@openModalView new SubscribeModal()
|
@openModalView new SubscribeModal()
|
||||||
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'account subscription view'
|
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'account subscription view'
|
||||||
|
@ -67,15 +73,198 @@ module.exports = class SubscriptionView extends RootView
|
||||||
|
|
||||||
onClickConfirmEndSubscription: (e) ->
|
onClickConfirmEndSubscription: (e) ->
|
||||||
message = @$el.find('.unsubscribe-feedback textarea').val().trim()
|
message = @$el.find('.unsubscribe-feedback textarea').val().trim()
|
||||||
window.tracker?.trackEvent 'Unsubscribe End', message: message
|
@personalSub.unsubscribe(message)
|
||||||
|
|
||||||
|
# Sponsored subscriptions
|
||||||
|
|
||||||
|
onClickRecipientsSubscribe: (e) ->
|
||||||
|
emails = @$el.find('.recipient-emails').val().split('\n')
|
||||||
|
@recipientSubs.startSubscribe(emails)
|
||||||
|
|
||||||
|
onClickRecipientUnsubscribe: (e) ->
|
||||||
|
$(e.target).addClass('hide')
|
||||||
|
$(e.target).parent().find('.confirm-recipient-unsubscribe-button').removeClass('hide')
|
||||||
|
|
||||||
|
onClickRecipientConfirmUnsubscribe: (e) ->
|
||||||
|
email = $(e.target).closest('tr').find('td.recipient-email').text()
|
||||||
|
@recipientSubs.unsubscribe(email, => @render?())
|
||||||
|
|
||||||
|
onStripeReceivedToken: (e) ->
|
||||||
|
@recipientSubs.finishSubscribe(e.token.id, => @render?())
|
||||||
|
|
||||||
|
# Helper classes for managing subscription actions and updating UI state
|
||||||
|
|
||||||
|
class PersonalSub
|
||||||
|
constructor: (@supermodel) ->
|
||||||
|
|
||||||
|
unsubscribe: (message) ->
|
||||||
removeStripe = =>
|
removeStripe = =>
|
||||||
stripe = _.clone(me.get('stripe'))
|
stripeInfo = _.clone(me.get('stripe'))
|
||||||
delete stripe.planID
|
delete stripeInfo.planID
|
||||||
me.set('stripe', stripe)
|
me.set('stripe', stripeInfo)
|
||||||
|
me.once 'sync', ->
|
||||||
|
window.tracker?.trackEvent 'Unsubscribe End', message: message
|
||||||
|
document.location.reload()
|
||||||
me.patch({headers: {'X-Change-Plan': 'true'}})
|
me.patch({headers: {'X-Change-Plan': 'true'}})
|
||||||
@listenToOnce me, 'sync', -> document.location.reload()
|
|
||||||
if message
|
if message
|
||||||
$.post '/contact', message: message, subject: 'Cancellation', (response) ->
|
$.post '/contact', message: message, subject: 'Cancellation', (response) ->
|
||||||
removeStripe()
|
removeStripe()
|
||||||
else
|
else
|
||||||
removeStripe()
|
removeStripe()
|
||||||
|
|
||||||
|
update: (render) ->
|
||||||
|
return unless stripeInfo = me.get('stripe')
|
||||||
|
|
||||||
|
@state = 'loading'
|
||||||
|
|
||||||
|
if stripeInfo.sponsorID
|
||||||
|
@sponsor = true
|
||||||
|
onSubSponsorSuccess = (sponsorInfo) =>
|
||||||
|
@sponsorEmail = sponsorInfo.email
|
||||||
|
@sponsorName = sponsorInfo.name
|
||||||
|
@sponsorID = stripeInfo.sponsorID
|
||||||
|
if sponsorInfo.subscription.cancel_at_period_end
|
||||||
|
@endDate = new Date(sponsorInfo.subscription.current_period_end * 1000)
|
||||||
|
delete @state
|
||||||
|
render()
|
||||||
|
@supermodel.addRequestResource('sub_sponsor', {
|
||||||
|
url: '/db/user/-/sub_sponsor'
|
||||||
|
method: 'POST'
|
||||||
|
success: onSubSponsorSuccess
|
||||||
|
}, 0).load()
|
||||||
|
|
||||||
|
else if stripeInfo.subscriptionID
|
||||||
|
@self = true
|
||||||
|
@active = me.isPremium()
|
||||||
|
@subscribed = stripeInfo.planID?
|
||||||
|
|
||||||
|
options = { cache: false, url: "/db/user/#{me.id}/stripe" }
|
||||||
|
options.success = (info) =>
|
||||||
|
if card = info.card
|
||||||
|
@card = "#{card.brand}: x#{card.last4}"
|
||||||
|
if sub = info.subscription
|
||||||
|
periodEnd = new Date((sub.trial_end or sub.current_period_end) * 1000)
|
||||||
|
if sub.cancel_at_period_end
|
||||||
|
@activeUntil = periodEnd
|
||||||
|
else
|
||||||
|
@nextPaymentDate = periodEnd
|
||||||
|
@cost = "$#{(sub.plan.amount/100).toFixed(2)}"
|
||||||
|
else
|
||||||
|
console.error "Could not find personal subscription #{me.get('stripe')?.customerID} #{me.get('stripe')?.subscriptionID}"
|
||||||
|
delete @state
|
||||||
|
render()
|
||||||
|
@supermodel.addRequestResource('personal_payment_info', options).load()
|
||||||
|
|
||||||
|
payments = new CocoCollection([], { url: '/db/payment', model: Payment, comparator:'_id' })
|
||||||
|
payments.once 'sync', ->
|
||||||
|
@monthsSubscribed = (x for x in payments.models when not x.get('productID')).length
|
||||||
|
render()
|
||||||
|
@supermodel.loadCollection(payments, 'payments', {cache: false})
|
||||||
|
|
||||||
|
else if stripeInfo.free
|
||||||
|
@free = stripeInfo.free
|
||||||
|
delete @state
|
||||||
|
render()
|
||||||
|
else
|
||||||
|
delete @state
|
||||||
|
render()
|
||||||
|
|
||||||
|
class RecipientSubs
|
||||||
|
constructor: (@supermodel) ->
|
||||||
|
@recipients = {}
|
||||||
|
@unsubscribingRecipients = []
|
||||||
|
|
||||||
|
addSubscribing: (email) ->
|
||||||
|
@unsubscribingRecipients.push email
|
||||||
|
|
||||||
|
removeSubscribing: (email) ->
|
||||||
|
_.remove(@unsubscribingRecipients, (recipientEmail) -> recipientEmail is email)
|
||||||
|
|
||||||
|
startSubscribe: (emails) ->
|
||||||
|
@recipientEmails = (email.trim().toLowerCase() for email in emails)
|
||||||
|
_.remove(@recipientEmails, (email) -> _.isEmpty(email))
|
||||||
|
return if @recipientEmails.length < 1
|
||||||
|
|
||||||
|
window.tracker?.trackEvent 'Start sponsored subscription'
|
||||||
|
|
||||||
|
# TODO: this sometimes shows a rounded amount (e.g. $8.00)
|
||||||
|
currentSubCount = me.get('stripe')?.recipients?.length ? 0
|
||||||
|
newSubCount = @recipientEmails.length + currentSubCount
|
||||||
|
amount = utils.getSponsoredSubsAmount(basicPlanPrice, newSubCount, me.get('stripe')?.subscriptionID?) - utils.getSponsoredSubsAmount(basicPlanPrice, currentSubCount, me.get('stripe')?.subscriptionID?)
|
||||||
|
options = {
|
||||||
|
description: "#{@recipientEmails.length} " + $.i18n.t('subscribe.stripe_description', defaultValue: 'Monthly Subscriptions')
|
||||||
|
amount: amount
|
||||||
|
}
|
||||||
|
@state = 'start subscribe'
|
||||||
|
@stateMessage = ''
|
||||||
|
stripeHandler.open(options)
|
||||||
|
|
||||||
|
finishSubscribe: (tokenID, render) ->
|
||||||
|
return unless @state is 'start subscribe' # Don't intercept personal subcribe process
|
||||||
|
|
||||||
|
@state = 'subscribing'
|
||||||
|
@stateMessage = ''
|
||||||
|
@justSubscribed = []
|
||||||
|
render()
|
||||||
|
|
||||||
|
stripeInfo = _.clone(me.get('stripe') ? {})
|
||||||
|
stripeInfo.token = tokenID
|
||||||
|
stripeInfo.subscribeEmails = @recipientEmails
|
||||||
|
me.set('stripe', stripeInfo)
|
||||||
|
|
||||||
|
me.once 'sync', =>
|
||||||
|
application.tracker?.trackEvent 'Finished sponsored subscription purchase'
|
||||||
|
@update(render)
|
||||||
|
me.once 'error', (user, response, options) =>
|
||||||
|
console.error 'We got an error subscribing with Stripe from our server:', response
|
||||||
|
stripeInfo = me.get('stripe') ? {}
|
||||||
|
delete stripeInfo.token
|
||||||
|
xhr = options.xhr
|
||||||
|
if xhr.status is 402
|
||||||
|
@state = 'declined'
|
||||||
|
@stateMessage = ''
|
||||||
|
else
|
||||||
|
@state = 'unknown_error'
|
||||||
|
@stateMessage = "#{xhr.status}: #{xhr.responseText}"
|
||||||
|
render()
|
||||||
|
me.patch({headers: {'X-Change-Plan': 'true'}})
|
||||||
|
|
||||||
|
unsubscribe: (email, render) ->
|
||||||
|
@addSubscribing(email)
|
||||||
|
render()
|
||||||
|
stripeInfo = _.clone(me.get('stripe'))
|
||||||
|
stripeInfo.unsubscribeEmail = email
|
||||||
|
me.set('stripe', stripeInfo)
|
||||||
|
me.once 'sync', =>
|
||||||
|
@removeSubscribing(email)
|
||||||
|
@update(render)
|
||||||
|
me.patch({headers: {'X-Change-Plan': 'true'}})
|
||||||
|
|
||||||
|
update: (render) ->
|
||||||
|
delete @state
|
||||||
|
delete @stateMessage
|
||||||
|
return unless me.get('stripe')?.recipients
|
||||||
|
@unsubscribingRecipients = []
|
||||||
|
|
||||||
|
options = { cache: false, url: "/db/user/#{me.id}/stripe" }
|
||||||
|
options.success = (info) =>
|
||||||
|
@sponsorSub = info.subscription
|
||||||
|
if card = info.card
|
||||||
|
@card = "#{card.brand}: x#{card.last4}"
|
||||||
|
render()
|
||||||
|
@supermodel.addRequestResource('recipients_payment_info', options).load()
|
||||||
|
|
||||||
|
onSubRecipientsSuccess = (recipientsMap) =>
|
||||||
|
@recipients = recipientsMap
|
||||||
|
count = 0
|
||||||
|
for userID, recipient of @recipients
|
||||||
|
count++ unless recipient.cancel_at_period_end
|
||||||
|
if @recipientEmails? and @justSubscribed? and recipient.emailLower in @recipientEmails
|
||||||
|
@justSubscribed.push recipient.emailLower
|
||||||
|
@nextPaymentAmount = utils.getSponsoredSubsAmount(basicPlanPrice, count, me.get('stripe')?.subscriptionID?)
|
||||||
|
render()
|
||||||
|
@supermodel.addRequestResource('sub_recipients', {
|
||||||
|
url: '/db/user/-/sub_recipients'
|
||||||
|
method: 'POST'
|
||||||
|
success: onSubRecipientsSuccess
|
||||||
|
}, 0).load()
|
||||||
|
|
|
@ -17,7 +17,7 @@ module.exports = class UsersView extends RootView
|
||||||
# http://mongoosejs.com/docs/queries.html
|
# http://mongoosejs.com/docs/queries.html
|
||||||
# Each list in conditions is a function call.
|
# Each list in conditions is a function call.
|
||||||
# The first arg is the function name
|
# The first arg is the function name
|
||||||
# The rest are the agrs for the function
|
# The rest are the args for the function
|
||||||
|
|
||||||
conditions = [
|
conditions = [
|
||||||
['limit', 20]
|
['limit', 20]
|
||||||
|
|
|
@ -4,6 +4,7 @@ mongoose = require 'mongoose'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
|
isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
|
||||||
|
|
||||||
objectIdFromTimestamp: (timestamp) ->
|
objectIdFromTimestamp: (timestamp) ->
|
||||||
# mongoDB ObjectId contains creation date in first 4 bytes
|
# mongoDB ObjectId contains creation date in first 4 bytes
|
||||||
# So, it can be used instead of a redundant created field
|
# So, it can be used instead of a redundant created field
|
||||||
|
@ -15,6 +16,36 @@ module.exports =
|
||||||
hexSeconds = Math.floor(timestamp/1000).toString(16)
|
hexSeconds = Math.floor(timestamp/1000).toString(16)
|
||||||
# Create an ObjectId with that hex timestamp
|
# Create an ObjectId with that hex timestamp
|
||||||
mongoose.Types.ObjectId(hexSeconds + "0000000000000000")
|
mongoose.Types.ObjectId(hexSeconds + "0000000000000000")
|
||||||
|
|
||||||
|
findStripeSubscription: (customerID, options, done) ->
|
||||||
|
# Grabs latest subscription (e.g. in case of a resubscribe)
|
||||||
|
return done() unless customerID?
|
||||||
|
return done() unless options.subscriptionID? or options.userID?
|
||||||
|
subscriptionID = options.subscriptionID
|
||||||
|
userID = options.userID
|
||||||
|
|
||||||
|
subscription = null
|
||||||
|
nextBatch = (starting_after, done) ->
|
||||||
|
options = limit: 100
|
||||||
|
options.starting_after = starting_after if starting_after
|
||||||
|
stripe.customers.listSubscriptions customerID, options, (err, subscriptions) ->
|
||||||
|
return done(subscription) if err
|
||||||
|
return done(subscription) unless subscriptions?.data?.length > 0
|
||||||
|
for sub in subscriptions.data
|
||||||
|
if subscriptionID? and sub.id is subscriptionID
|
||||||
|
unless subscription?.cancel_at_period_end is false
|
||||||
|
subscription = sub
|
||||||
|
if userID? and sub.metadata?.id is userID
|
||||||
|
unless subscription?.cancel_at_period_end is false
|
||||||
|
subscription = sub
|
||||||
|
return done(subscription) if subscription?.cancel_at_period_end is false
|
||||||
|
|
||||||
|
if subscriptions.has_more
|
||||||
|
nextBatch(subscriptions.data[subscriptions.data.length - 1].id, done)
|
||||||
|
else
|
||||||
|
done(subscription)
|
||||||
|
nextBatch(null, done)
|
||||||
|
|
||||||
getAnalyticsStringID: (str, callback) ->
|
getAnalyticsStringID: (str, callback) ->
|
||||||
unless str?
|
unless str?
|
||||||
log.error "getAnalyticsStringID given invalid str param"
|
log.error "getAnalyticsStringID given invalid str param"
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
# Not paired with a document in the DB, just handles coordinating between
|
# Not paired with a document in the DB, just handles coordinating between
|
||||||
# the stripe property in the user with what's being stored in Stripe.
|
# the stripe property in the user with what's being stored in Stripe.
|
||||||
|
|
||||||
|
async = require 'async'
|
||||||
Handler = require '../commons/Handler'
|
Handler = require '../commons/Handler'
|
||||||
discountHandler = require './discount_handler'
|
discountHandler = require './discount_handler'
|
||||||
|
User = require '../users/User'
|
||||||
|
{findStripeSubscription} = require '../lib/utils'
|
||||||
|
{getSponsoredSubsAmount} = require '../../app/core/utils'
|
||||||
|
|
||||||
|
recipientCouponID = 'free'
|
||||||
subscriptions = {
|
subscriptions = {
|
||||||
basic: {
|
basic: {
|
||||||
gems: 3500
|
gems: 3500
|
||||||
|
amount: 999 # For calculating incremental quantity before sub creation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,28 +30,27 @@ class SubscriptionHandler extends Handler
|
||||||
@logSubscriptionError(user, 'Missing stripe token or customer ID.')
|
@logSubscriptionError(user, 'Missing stripe token or customer ID.')
|
||||||
return done({res: 'Missing stripe token or customer ID.', code: 422})
|
return done({res: 'Missing stripe token or customer ID.', code: 422})
|
||||||
|
|
||||||
|
# Create/retrieve Stripe customer
|
||||||
if token
|
if token
|
||||||
if customerID
|
if customerID
|
||||||
stripe.customers.update customerID, { card: token }, (err, customer) =>
|
stripe.customers.update customerID, { card: token }, (err, customer) =>
|
||||||
if err or not customer
|
if err or not customer
|
||||||
# should not happen outside of test and production polluting each other
|
# should not happen outside of test and production polluting each other
|
||||||
@logSubscriptionError(user, 'Cannot find customer: ', +customer.id + '\n\n' + err)
|
@logSubscriptionError(user, 'Cannot find customer: ' + customerID + '\n\n' + err)
|
||||||
return done({res: 'Cannot find customer.', code: 404})
|
return done({res: 'Cannot find customer.', code: 404})
|
||||||
@checkForExistingSubscription(req, user, customer, done)
|
@checkForExistingSubscription(req, user, customer, done)
|
||||||
|
|
||||||
else
|
else
|
||||||
newCustomer = {
|
options =
|
||||||
card: token
|
card: token
|
||||||
email: user.get('email')
|
email: user.get('email')
|
||||||
metadata: { id: user._id + '', slug: user.get('slug') }
|
metadata: { id: user._id + '', slug: user.get('slug') }
|
||||||
}
|
stripe.customers.create options, (err, customer) =>
|
||||||
|
|
||||||
stripe.customers.create newCustomer, (err, customer) =>
|
|
||||||
if err
|
if err
|
||||||
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
|
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
|
||||||
return done({res: 'Card error', code: 402})
|
return done({res: 'Card error', code: 402})
|
||||||
else
|
else
|
||||||
@logSubscriptionError(user, 'Stripe customer creation error. '+err)
|
@logSubscriptionError(user, 'Stripe customer creation error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
|
@ -53,68 +58,78 @@ class SubscriptionHandler extends Handler
|
||||||
user.set('stripe', stripeInfo)
|
user.set('stripe', stripeInfo)
|
||||||
user.save (err) =>
|
user.save (err) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe customer id save db error. '+err)
|
@logSubscriptionError(user, 'Stripe customer id save db error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
@checkForExistingSubscription(req, user, customer, done)
|
@checkForExistingSubscription(req, user, customer, done)
|
||||||
|
|
||||||
else
|
else
|
||||||
stripe.customers.retrieve(customerID, (err, customer) =>
|
stripe.customers.retrieve(customerID, (err, customer) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe customer creation error. '+err)
|
@logSubscriptionError(user, 'Stripe customer retrieve error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
@checkForExistingSubscription(req, user, customer, done)
|
@checkForExistingSubscription(req, user, customer, done)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
checkForExistingSubscription: (req, user, customer, done) ->
|
checkForExistingSubscription: (req, user, customer, done) ->
|
||||||
|
# Check if user is subscribing someone else
|
||||||
|
if req.body.stripe?.subscribeEmails?
|
||||||
|
return @updateStripeRecipientSubscriptions req, user, customer, done
|
||||||
|
|
||||||
|
if user.get('stripe')?.sponsorID
|
||||||
|
return done({res: 'You already have a sponsored subscription.', code: 403})
|
||||||
|
|
||||||
couponID = user.get('stripe')?.couponID
|
couponID = user.get('stripe')?.couponID
|
||||||
|
|
||||||
# SALE LOGIC
|
# SALE LOGIC
|
||||||
# overwrite couponID with another for everyone-sales
|
# overwrite couponID with another for everyone-sales
|
||||||
#couponID = 'hoc_399' if not couponID
|
#couponID = 'hoc_399' if not couponID
|
||||||
|
|
||||||
if subscription = customer.subscriptions?.data?[0]
|
findStripeSubscription customer.id, subscriptionID: user.get('stripe')?.subscriptionID, (subscription) =>
|
||||||
|
|
||||||
if subscription.cancel_at_period_end
|
if subscription
|
||||||
# Things are a little tricky here. Can't re-enable a cancelled subscription,
|
|
||||||
# so it needs to be deleted, but also don't want to charge for the new subscription immediately.
|
|
||||||
# So delete the cancelled subscription (no at_period_end given here) and give the new
|
|
||||||
# subscription a trial period that ends when the cancelled subscription would have ended.
|
|
||||||
stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) =>
|
|
||||||
if err
|
|
||||||
@logSubscriptionError(user, 'Stripe cancel subscription error. '+err)
|
|
||||||
return done({res: 'Database error.', code: 500})
|
|
||||||
|
|
||||||
options = { plan: 'basic', trial_end: subscription.current_period_end }
|
if subscription.cancel_at_period_end
|
||||||
options.coupon = couponID if couponID
|
# Things are a little tricky here. Can't re-enable a cancelled subscription,
|
||||||
stripe.customers.update user.get('stripe').customerID, options, (err, customer) =>
|
# so it needs to be deleted, but also don't want to charge for the new subscription immediately.
|
||||||
|
# So delete the cancelled subscription (no at_period_end given here) and give the new
|
||||||
|
# subscription a trial period that ends when the cancelled subscription would have ended.
|
||||||
|
stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe customer plan setting error. '+err)
|
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
@updateUser(req, user, customer, false, done)
|
options = { plan: 'basic', metadata: {id: user.id}, trial_end: subscription.current_period_end }
|
||||||
|
options.coupon = couponID if couponID
|
||||||
|
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe customer plan setting error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
@updateUser(req, user, customer, subscription, false, done)
|
||||||
|
|
||||||
|
else
|
||||||
|
# can skip creating the subscription
|
||||||
|
return @updateUser(req, user, customer, subscription, false, done)
|
||||||
|
|
||||||
else
|
else
|
||||||
# can skip creating the subscription
|
options = { plan: 'basic', metadata: {id: user.id}}
|
||||||
return @updateUser(req, user, customer, false, done)
|
options.coupon = couponID if couponID
|
||||||
|
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe customer plan setting error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
else
|
@updateUser(req, user, customer, subscription, true, done)
|
||||||
options = { plan: 'basic' }
|
|
||||||
options.coupon = couponID if couponID
|
|
||||||
stripe.customers.update user.get('stripe').customerID, options, (err, customer) =>
|
|
||||||
if err
|
|
||||||
@logSubscriptionError(user, 'Stripe customer plan setting error. '+err)
|
|
||||||
return done({res: 'Database error.', code: 500})
|
|
||||||
|
|
||||||
@updateUser(req, user, customer, true, done)
|
updateUser: (req, user, customer, subscription, increment, done) ->
|
||||||
|
|
||||||
updateUser: (req, user, customer, increment, done) ->
|
|
||||||
subscription = customer.subscriptions.data[0]
|
|
||||||
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
stripeInfo.planID = 'basic'
|
stripeInfo.planID = 'basic'
|
||||||
stripeInfo.subscriptionID = subscription.id
|
stripeInfo.subscriptionID = subscription.id
|
||||||
stripeInfo.customerID = customer.id
|
stripeInfo.customerID = customer.id
|
||||||
req.body.stripe = stripeInfo # to make sure things work for admins, who are mad with power
|
|
||||||
|
# To make sure things work for admins, who are mad with power
|
||||||
|
# And, so Handler.saveChangesToDocument doesn't undo all our saves here
|
||||||
|
req.body.stripe = stripeInfo
|
||||||
user.set('stripe', stripeInfo)
|
user.set('stripe', stripeInfo)
|
||||||
|
|
||||||
if increment
|
if increment
|
||||||
|
@ -126,25 +141,232 @@ class SubscriptionHandler extends Handler
|
||||||
|
|
||||||
user.save (err) =>
|
user.save (err) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe user plan saving error. '+err)
|
@logSubscriptionError(user, 'Stripe user plan saving error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
user?.saveActiveUser 'subscribe'
|
user?.saveActiveUser 'subscribe'
|
||||||
return done()
|
return done()
|
||||||
|
|
||||||
|
updateStripeRecipientSubscriptions: (req, user, customer, done) ->
|
||||||
|
return done({res: 'Database error.', code: 500}) unless req.body.stripe?.subscribeEmails?
|
||||||
|
|
||||||
|
emails = req.body.stripe.subscribeEmails.map((email) -> email.trim().toLowerCase() unless _.isEmpty(email))
|
||||||
|
_.remove(emails, (email) -> _.isEmpty(email))
|
||||||
|
|
||||||
|
User.find {emailLower: {$in: emails}}, (err, recipients) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, "User lookup error. " + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
createUpdateFn = (recipient) =>
|
||||||
|
(done) =>
|
||||||
|
# Find existing recipient subscription
|
||||||
|
findStripeSubscription customer.id, userID: recipient.id, (subscription) =>
|
||||||
|
|
||||||
|
if subscription
|
||||||
|
if subscription.cancel_at_period_end
|
||||||
|
# Things are a little tricky here. Can't re-enable a cancelled subscription,
|
||||||
|
# so it needs to be deleted, but also don't want to charge for the new subscription immediately.
|
||||||
|
# So delete the cancelled subscription (no at_period_end given here) and give the new
|
||||||
|
# subscription a trial period that ends when the cancelled subscription would have ended.
|
||||||
|
stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
options =
|
||||||
|
plan: 'basic'
|
||||||
|
coupon: recipientCouponID
|
||||||
|
metadata: {id: recipient.id}
|
||||||
|
trial_end: subscription.current_period_end
|
||||||
|
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe new subscription error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done(null, recipient: recipient, subscription: subscription, increment: false)
|
||||||
|
else
|
||||||
|
# Can skip creating the subscription
|
||||||
|
done(null, recipient: recipient, subscription: subscription, increment: false)
|
||||||
|
|
||||||
|
else
|
||||||
|
options =
|
||||||
|
plan: 'basic'
|
||||||
|
coupon: recipientCouponID
|
||||||
|
metadata: {id: recipient.id}
|
||||||
|
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe new subscription error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done(null, recipient: recipient, subscription: subscription, increment: true)
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for recipient in recipients
|
||||||
|
continue if recipient.id is user.id
|
||||||
|
continue if recipient.get('stripe')?.subscriptionID?
|
||||||
|
continue if recipient.get('stripe')?.sponsorID? and recipient.get('stripe')?.sponsorID isnt user.id
|
||||||
|
tasks.push createUpdateFn(recipient)
|
||||||
|
|
||||||
|
# NOTE: async.parellel yields this error:
|
||||||
|
# Subscription Error: user23 (54fe3c8fea98978efa469f3b): 'Stripe new subscription error. Error: Request rate limit exceeded'
|
||||||
|
async.series tasks, (err, results) =>
|
||||||
|
return done(err) if err
|
||||||
|
@updateCocoRecipientSubscriptions(req, user, customer, results, done)
|
||||||
|
|
||||||
|
updateCocoRecipientSubscriptions: (req, user, customer, stripeRecipients, done) ->
|
||||||
|
# Update recipients list
|
||||||
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
|
stripeInfo.recipients ?= []
|
||||||
|
stripeRecipientIDs = (sub.recipient.id for sub in stripeRecipients)
|
||||||
|
_.remove(stripeInfo.recipients, (s) -> s.userID in stripeRecipientIDs)
|
||||||
|
for sub in stripeRecipients
|
||||||
|
stripeInfo.recipients.push
|
||||||
|
userID: sub.recipient.id
|
||||||
|
subscriptionID: sub.subscription.id
|
||||||
|
couponID: recipientCouponID
|
||||||
|
|
||||||
|
# TODO: how does token get removed for personal subs?
|
||||||
|
delete stripeInfo.subscribeEmails
|
||||||
|
delete stripeInfo.token
|
||||||
|
req.body.stripe = stripeInfo
|
||||||
|
user.set('stripe', stripeInfo)
|
||||||
|
user.save (err) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'User saving stripe error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
createUpdateFn = (recipient, increment) =>
|
||||||
|
(done) =>
|
||||||
|
# Update recipient
|
||||||
|
stripeInfo = _.cloneDeep(recipient.get('stripe') ? {})
|
||||||
|
stripeInfo.sponsorID = user.id
|
||||||
|
recipient.set 'stripe', stripeInfo
|
||||||
|
if increment
|
||||||
|
purchased = _.clone(recipient.get('purchased'))
|
||||||
|
purchased ?= {}
|
||||||
|
purchased.gems ?= 0
|
||||||
|
purchased.gems += subscriptions.basic.gems
|
||||||
|
recipient.set('purchased', purchased)
|
||||||
|
recipient.save (err) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe user saving stripe error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done()
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for sub in stripeRecipients
|
||||||
|
tasks.push createUpdateFn(sub.recipient, sub.increment)
|
||||||
|
|
||||||
|
async.parallel tasks, (err, results) =>
|
||||||
|
return done(err) if err
|
||||||
|
@updateStripeSponsorSubscription(req, user, customer, done)
|
||||||
|
|
||||||
|
updateStripeSponsorSubscription: (req, user, customer, done) ->
|
||||||
|
stripeInfo = user.get('stripe') ? {}
|
||||||
|
numSponsored = stripeInfo.recipients.length
|
||||||
|
quantity = getSponsoredSubsAmount(subscriptions.basic.amount, numSponsored, stripeInfo.subscriptionID?)
|
||||||
|
|
||||||
|
findStripeSubscription customer.id, subscriptionID: stripeInfo.sponsorSubscriptionID, (subscription) =>
|
||||||
|
if stripeInfo.sponsorSubscriptionID? and not subscription?
|
||||||
|
@logSubscriptionError(user, "Internal sponsor subscription #{stripeInfo.sponsorSubscriptionID} not found on Stripe customer #{customer.id}")
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
if subscription
|
||||||
|
return done() if quantity is subscription.quantity # E.g. cancelled sub has been resubbed
|
||||||
|
|
||||||
|
options = quantity: quantity
|
||||||
|
stripe.customers.updateSubscription customer.id, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe updating subscription quantity error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
# Invoice proration immediately
|
||||||
|
stripe.invoices.create customer: customer.id, (err, invoice) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe proration invoice error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done()
|
||||||
|
else
|
||||||
|
options =
|
||||||
|
plan: 'incremental'
|
||||||
|
metadata: {id: user.id}
|
||||||
|
quantity: quantity
|
||||||
|
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe new subscription error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
@updateCocoSponsorSubscription(req, user, subscription, done)
|
||||||
|
|
||||||
|
updateCocoSponsorSubscription: (req, user, subscription, done) ->
|
||||||
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
|
stripeInfo.sponsorSubscriptionID = subscription.id
|
||||||
|
req.body.stripe = stripeInfo
|
||||||
|
user.set('stripe', stripeInfo)
|
||||||
|
user.save (err) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Saving user stripe error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done()
|
||||||
|
|
||||||
unsubscribeUser: (req, user, done) ->
|
unsubscribeUser: (req, user, done) ->
|
||||||
stripeInfo = _.cloneDeep(user.get('stripe'))
|
# Check if user is subscribing someone else
|
||||||
|
return @unsubscribeRecipient(req, user, done) if req.body.stripe?.unsubscribeEmail?
|
||||||
|
|
||||||
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
stripe.customers.cancelSubscription stripeInfo.customerID, stripeInfo.subscriptionID, { at_period_end: true }, (err) =>
|
stripe.customers.cancelSubscription stripeInfo.customerID, stripeInfo.subscriptionID, { at_period_end: true }, (err) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe cancel subscription error. '+err)
|
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
delete stripeInfo.planID
|
delete stripeInfo.planID
|
||||||
user.set('stripe', stripeInfo)
|
user.set('stripe', stripeInfo)
|
||||||
req.body.stripe = stripeInfo
|
req.body.stripe = stripeInfo
|
||||||
user.save (err) =>
|
user.save (err) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'User save unsubscribe error. '+err)
|
@logSubscriptionError(user, 'User save unsubscribe error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
user?.saveActiveUser 'unsubscribe'
|
done()
|
||||||
return done()
|
|
||||||
|
unsubscribeRecipient: (req, user, done) ->
|
||||||
|
return done({res: 'Database error.', code: 500}) unless req.body.stripe?.unsubscribeEmail?
|
||||||
|
|
||||||
|
email = req.body.stripe.unsubscribeEmail.trim().toLowerCase()
|
||||||
|
return done({res: 'Database error.', code: 500}) if _.isEmpty(email)
|
||||||
|
|
||||||
|
User.findOne {emailLower: email}, (err, recipient) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, "User lookup error. " + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
unless recipient
|
||||||
|
@logSubscriptionError(user, "Recipient #{req.body.stripe.recipient} not found. " + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
# Check recipient is currently sponsored
|
||||||
|
stripeRecipient = recipient.get 'stripe' ? {}
|
||||||
|
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})
|
||||||
|
|
||||||
|
# Find recipient subscription
|
||||||
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
|
for sponsored in stripeInfo.recipients
|
||||||
|
if sponsored.userID is recipient.id
|
||||||
|
sponsoredEntry = sponsored
|
||||||
|
break
|
||||||
|
unless sponsoredEntry?
|
||||||
|
@logSubscriptionError(user, 'Unable to find sponsored subscription. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
# Cancel Stripe subscription
|
||||||
|
stripe.customers.cancelSubscription stripeInfo.customerID, sponsoredEntry.subscriptionID, { at_period_end: true }, (err) =>
|
||||||
|
if err or not recipient
|
||||||
|
@logSubscriptionError(user, "Stripe cancel sponsored subscription failed. " + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
|
delete stripeInfo.unsubscribeEmail
|
||||||
|
user.set('stripe', stripeInfo)
|
||||||
|
req.body.stripe = stripeInfo
|
||||||
|
user.save (err) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'User save unsubscribe error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done()
|
||||||
|
|
||||||
module.exports = new SubscriptionHandler()
|
module.exports = new SubscriptionHandler()
|
||||||
|
|
|
@ -76,7 +76,7 @@ PurchaseHandler = class PurchaseHandler extends Handler
|
||||||
user.set('purchased', purchased)
|
user.set('purchased', purchased)
|
||||||
|
|
||||||
#- deduct the gems from the user
|
#- deduct the gems from the user
|
||||||
spent = hadSpent = user.get('spent') ? 0
|
spent = user.get('spent') ? 0
|
||||||
spent += item.get('gems')
|
spent += item.get('gems')
|
||||||
user.set('spent', spent)
|
user.set('spent', spent)
|
||||||
|
|
||||||
|
|
|
@ -1,59 +1,112 @@
|
||||||
|
async = require 'async'
|
||||||
config = require '../../server_config'
|
config = require '../../server_config'
|
||||||
stripe = require('stripe')(config.stripe.secretKey)
|
stripe = require('stripe')(config.stripe.secretKey)
|
||||||
User = require '../users/User'
|
User = require '../users/User'
|
||||||
Payment = require '../payments/Payment'
|
Payment = require '../payments/Payment'
|
||||||
errors = require '../commons/errors'
|
errors = require '../commons/errors'
|
||||||
|
mongoose = require 'mongoose'
|
||||||
|
utils = require '../../app/core/utils'
|
||||||
|
|
||||||
module.exports.setup = (app) ->
|
module.exports.setup = (app) ->
|
||||||
|
# Cache customer -> user ID map (increases test perf considerably)
|
||||||
|
customerUserMap = {}
|
||||||
|
|
||||||
app.post '/stripe/webhook', (req, res) ->
|
app.post '/stripe/webhook', (req, res) ->
|
||||||
if req.body.type is 'invoice.payment_succeeded' # if they actually paid, give em some gems
|
|
||||||
|
|
||||||
invoiceID = req.body.data.object.id
|
# Subscription renewal events:
|
||||||
stripe.invoices.retrieve invoiceID, (err, invoice) =>
|
# https://support.stripe.com/questions/what-events-can-i-see-when-a-subscription-is-renewed
|
||||||
return res.send(500, '') if err
|
|
||||||
return res.send(200, '') unless invoice.total # invoices made when trialing, probably given for people who resubscribe after unsubscribing
|
|
||||||
|
|
||||||
stripe.customers.retrieve invoice.customer, (err, customer) =>
|
|
||||||
return res.send(500, '') if err
|
|
||||||
|
|
||||||
userID = customer.metadata.id
|
|
||||||
User.findById userID, (err, user) =>
|
|
||||||
return res.send(500, '') if err
|
|
||||||
return res.send(200) if not user # just for the sake of testing...
|
|
||||||
|
|
||||||
Payment.findOne {'stripe.invoiceID': invoiceID}, (err, payment) =>
|
|
||||||
return res.send(200, '') if payment
|
|
||||||
payment = new Payment({
|
|
||||||
'purchaser': user._id
|
|
||||||
'recipient': user._id
|
|
||||||
'created': new Date().toISOString()
|
|
||||||
'service': 'stripe'
|
|
||||||
'amount': invoice.total
|
|
||||||
'gems': 3500
|
|
||||||
'stripe': {
|
|
||||||
customerID: invoice.customer
|
|
||||||
invoiceID: invoice.id
|
|
||||||
subscriptionID: 'basic'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
payment.save (err) =>
|
|
||||||
return res.send(500, '') if err
|
|
||||||
|
|
||||||
Payment.find({recipient: user._id}).select('gems').exec (err, payments) ->
|
|
||||||
gems = _.reduce payments, ((sum, p) -> sum + p.get('gems')), 0
|
|
||||||
purchased = _.clone(user.get('purchased'))
|
|
||||||
purchased ?= {}
|
|
||||||
purchased.gems = gems
|
|
||||||
user.set('purchased', purchased)
|
|
||||||
user.save (err) ->
|
|
||||||
return res.send(500, '') if err
|
|
||||||
return res.send(201, '')
|
|
||||||
|
|
||||||
|
if req.body.type is 'invoice.payment_succeeded'
|
||||||
|
return handlePaymentSucceeded req, res
|
||||||
else if req.body.type is 'customer.subscription.deleted'
|
else if req.body.type is 'customer.subscription.deleted'
|
||||||
User.findOne {'stripe.subscriptionID': req.body.data.object.id}, (err, user) ->
|
return handleSubscriptionDeleted req, res
|
||||||
return res.send(200, '') unless user
|
else # ignore all other notifications
|
||||||
|
return res.send(200, '')
|
||||||
|
|
||||||
stripeInfo = _.cloneDeep(user.get('stripe'))
|
app.get '/stripe/coupons', (req, res) ->
|
||||||
|
return errors.forbidden(res) unless req.user?.isAdmin()
|
||||||
|
stripe.coupons.list {limit: 100}, (err, coupons) ->
|
||||||
|
return errors.serverError(res) if err
|
||||||
|
res.send(200, coupons.data)
|
||||||
|
return res.end()
|
||||||
|
|
||||||
|
handlePaymentSucceeded = (req, res) ->
|
||||||
|
# if they actually paid, give em some gems
|
||||||
|
|
||||||
|
getUserID = (customerID, done) =>
|
||||||
|
# Asumming Stripe customer never has a different userID
|
||||||
|
return done(null, customerUserMap[customerID]) if customerID of customerUserMap
|
||||||
|
stripe.customers.retrieve customerID, (err, customer) =>
|
||||||
|
return done(err) if err
|
||||||
|
customerUserMap[customerID] = customer.metadata.id
|
||||||
|
return done(null, customerUserMap[customerID])
|
||||||
|
|
||||||
|
invoiceID = req.body.data.object.id
|
||||||
|
stripe.invoices.retrieve invoiceID, (err, invoice) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
unless invoice.total or invoice.discount?.coupon?.id is 'free'
|
||||||
|
# invoices made when trialing, probably given for people who resubscribe after unsubscribing
|
||||||
|
return res.send(200, '')
|
||||||
|
return res.send(200, '') unless invoice.lines?.data?.length > 0
|
||||||
|
|
||||||
|
getUserID invoice.customer, (err, userID) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
|
||||||
|
# User is recipient if no metadata.id
|
||||||
|
recipientID = invoice.lines.data[0].metadata?.id or userID
|
||||||
|
|
||||||
|
# Subscription id location depends on invoice line_item type
|
||||||
|
subscriptionID = invoice.lines.data[0].subscription or invoice.lines.data[0].id
|
||||||
|
|
||||||
|
User.findById recipientID, (err, recipient) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
return res.send(200) unless recipient # just for the sake of testing...
|
||||||
|
|
||||||
|
Payment.findOne {'stripe.invoiceID': invoiceID}, (err, payment) =>
|
||||||
|
return res.send(200, '') if payment
|
||||||
|
payment = new Payment({
|
||||||
|
'purchaser': mongoose.Types.ObjectId(userID)
|
||||||
|
'recipient': recipient._id
|
||||||
|
'created': new Date().toISOString()
|
||||||
|
'service': 'stripe'
|
||||||
|
'amount': invoice.total
|
||||||
|
'stripe': {
|
||||||
|
customerID: invoice.customer
|
||||||
|
invoiceID: invoice.id
|
||||||
|
subscriptionID: subscriptionID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
payment.set 'gems', 3500 if invoice.lines.data[0].plan?.id is 'basic'
|
||||||
|
|
||||||
|
payment.save (err) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
return res.send(201, '') if invoice.lines.data[0].plan?.id isnt 'basic'
|
||||||
|
|
||||||
|
# Update purchased gems
|
||||||
|
# TODO: is this correct for a resub?
|
||||||
|
Payment.find({recipient: recipient._id, gems: {$exists: true}}).select('gems').exec (err, payments) ->
|
||||||
|
gems = _.reduce payments, ((sum, p) -> sum + p.get('gems')), 0
|
||||||
|
purchased = _.clone(recipient.get('purchased'))
|
||||||
|
purchased ?= {}
|
||||||
|
purchased.gems = gems
|
||||||
|
recipient.set('purchased', purchased)
|
||||||
|
recipient.save (err) ->
|
||||||
|
return res.send(500, '') if err
|
||||||
|
return res.send(201, '')
|
||||||
|
|
||||||
|
handleSubscriptionDeleted = (req, res) ->
|
||||||
|
# Three variants:
|
||||||
|
# normal - Personal subscription deleted
|
||||||
|
# recipeint - Subscription sponsored by another user is being deleted.
|
||||||
|
# sponsor - Aggregate subscription used to pay for multiple recipient subscriptions. Ugh.
|
||||||
|
|
||||||
|
subscription = req.body.data.object
|
||||||
|
|
||||||
|
checkNormalSubscription = (done) ->
|
||||||
|
User.findOne {'stripe.subscriptionID': subscription.id}, (err, user) ->
|
||||||
|
return done() unless user
|
||||||
|
|
||||||
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
delete stripeInfo.planID
|
delete stripeInfo.planID
|
||||||
delete stripeInfo.subscriptionID
|
delete stripeInfo.subscriptionID
|
||||||
user.set('stripe', stripeInfo)
|
user.set('stripe', stripeInfo)
|
||||||
|
@ -61,14 +114,71 @@ module.exports.setup = (app) ->
|
||||||
return res.send(500, '') if err
|
return res.send(500, '') if err
|
||||||
return res.send(200, '')
|
return res.send(200, '')
|
||||||
|
|
||||||
else # ignore all other notifications
|
checkRecipientSubscription = (done) ->
|
||||||
return res.send(200, '')
|
return done() unless subscription.plan.id is 'basic'
|
||||||
|
User.findById subscription.metadata.id, (err, recipient) =>
|
||||||
app.get '/stripe/coupons', (req, res) ->
|
return res.send(500, '') if err
|
||||||
return errors.forbidden(res) unless req.user?.isAdmin()
|
return res.send(500, '') unless recipient
|
||||||
stripe.coupons.list {limit: 100}, (err, coupons) ->
|
User.findById recipient.get('stripe').sponsorID, (err, sponsor) =>
|
||||||
return errors.serverError(res) if err
|
return res.send(500, '') if err
|
||||||
res.send(200, coupons.data)
|
return res.send(500, '') unless sponsor
|
||||||
return res.end()
|
|
||||||
|
# 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?)
|
||||||
|
stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
|
||||||
|
# Update sponsor user
|
||||||
|
sponsor.set 'stripe', stripeInfo
|
||||||
|
sponsor.save (err) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
|
||||||
|
# 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) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
return res.send(200, '')
|
||||||
|
|
||||||
|
checkSponsorSubscription = (done) ->
|
||||||
|
return done() unless subscription.plan.id is 'incremental'
|
||||||
|
|
||||||
|
customerID = subscription.customer
|
||||||
|
|
||||||
|
createUpdateFn = (sub) ->
|
||||||
|
(callback) ->
|
||||||
|
# Cancel Stripe recipient subscription
|
||||||
|
stripe.customers.cancelSubscription customerID, sub.subscriptionID, { at_period_end: true }, (err) ->
|
||||||
|
callback err
|
||||||
|
|
||||||
|
User.findById subscription.metadata.id, (err, sponsor) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
stripeInfo = _.cloneDeep(sponsor.get('stripe') ? {})
|
||||||
|
|
||||||
|
# Cancel all recipient subscriptions
|
||||||
|
async.parallel (createUpdateFn(sub) for sub in stripeInfo.recipients), (err, results) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
|
||||||
|
# Update sponsor user
|
||||||
|
delete stripeInfo.sponsorSubscriptionID
|
||||||
|
delete stripeInfo.recipients # Loses remaining credit on a re-subscribe for previous user
|
||||||
|
if _.isEmpty stripeInfo
|
||||||
|
sponsor.set 'stripe', undefined
|
||||||
|
else
|
||||||
|
sponsor.set 'stripe', stripeInfo
|
||||||
|
sponsor.save (err) =>
|
||||||
|
return res.send(500, '') if err
|
||||||
|
done()
|
||||||
|
|
||||||
|
# TODO: use async.series for this
|
||||||
|
checkNormalSubscription ->
|
||||||
|
checkRecipientSubscription ->
|
||||||
|
checkSponsorSubscription ->
|
||||||
|
res.send(200, '')
|
||||||
|
|
|
@ -213,6 +213,7 @@ UserSchema.methods.isPremium = ->
|
||||||
return true if @isInGodMode()
|
return true if @isInGodMode()
|
||||||
return true if @isAdmin()
|
return true if @isAdmin()
|
||||||
return false unless stripeObject = @get('stripe')
|
return false unless stripeObject = @get('stripe')
|
||||||
|
return true if stripeObject.sponsorID
|
||||||
return true if stripeObject.subscriptionID
|
return true if stripeObject.subscriptionID
|
||||||
return true if stripeObject.free is true
|
return true if stripeObject.free is true
|
||||||
return true if _.isString(stripeObject.free) and new Date() < new Date(stripeObject.free)
|
return true if _.isString(stripeObject.free) and new Date() < new Date(stripeObject.free)
|
||||||
|
|
|
@ -16,6 +16,7 @@ SubscriptionHandler = require '../payments/subscription_handler'
|
||||||
DiscountHandler = require '../payments/discount_handler'
|
DiscountHandler = require '../payments/discount_handler'
|
||||||
EarnedAchievement = require '../achievements/EarnedAchievement'
|
EarnedAchievement = require '../achievements/EarnedAchievement'
|
||||||
UserRemark = require './remarks/UserRemark'
|
UserRemark = require './remarks/UserRemark'
|
||||||
|
{findStripeSubscription} = require '../lib/utils'
|
||||||
{isID} = require '../lib/utils'
|
{isID} = require '../lib/utils'
|
||||||
hipchat = require '../hipchat'
|
hipchat = require '../hipchat'
|
||||||
sendwithus = require '../sendwithus'
|
sendwithus = require '../sendwithus'
|
||||||
|
@ -115,22 +116,35 @@ UserHandler = class UserHandler extends Handler
|
||||||
|
|
||||||
# Subscription setting
|
# Subscription setting
|
||||||
(req, user, callback) ->
|
(req, user, callback) ->
|
||||||
|
# TODO: Make subscribe vs. unsubscribe explicit. This property dance is confusing.
|
||||||
return callback(null, req, user) unless req.headers['x-change-plan'] # ensure only saves that are targeted at changing the subscription actually affect the subscription
|
return callback(null, req, user) unless req.headers['x-change-plan'] # ensure only saves that are targeted at changing the subscription actually affect the subscription
|
||||||
return callback(null, req, user) unless req.body.stripe
|
return callback(null, req, user) unless req.body.stripe
|
||||||
hasPlan = user.get('stripe')?.planID?
|
finishSubscription = (hasPlan, wantsPlan) ->
|
||||||
wantsPlan = req.body.stripe.planID?
|
return callback(null, req, user) if hasPlan is wantsPlan
|
||||||
|
if wantsPlan and not hasPlan
|
||||||
return callback(null, req, user) if hasPlan is wantsPlan
|
SubscriptionHandler.subscribeUser(req, user, (err) ->
|
||||||
if wantsPlan and not hasPlan
|
return callback(err) if err
|
||||||
|
return callback(null, req, user)
|
||||||
|
)
|
||||||
|
else if hasPlan and not wantsPlan
|
||||||
|
SubscriptionHandler.unsubscribeUser(req, user, (err) ->
|
||||||
|
return callback(err) if err
|
||||||
|
return callback(null, req, user)
|
||||||
|
)
|
||||||
|
if req.body.stripe.subscribeEmails?
|
||||||
SubscriptionHandler.subscribeUser(req, user, (err) ->
|
SubscriptionHandler.subscribeUser(req, user, (err) ->
|
||||||
return callback(err) if err
|
return callback(err) if err
|
||||||
return callback(null, req, user)
|
return callback(null, req, user)
|
||||||
)
|
)
|
||||||
else if hasPlan and not wantsPlan
|
else if req.body.stripe.unsubscribeEmail?
|
||||||
SubscriptionHandler.unsubscribeUser(req, user, (err) ->
|
SubscriptionHandler.unsubscribeUser(req, user, (err) ->
|
||||||
return callback(err) if err
|
return callback(err) if err
|
||||||
return callback(null, req, user)
|
return callback(null, req, user)
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
wantsPlan = req.body.stripe.planID?
|
||||||
|
hasPlan = user.get('stripe')?.planID?
|
||||||
|
finishSubscription hasPlan, wantsPlan
|
||||||
|
|
||||||
# Discount setting
|
# Discount setting
|
||||||
(req, user, callback) ->
|
(req, user, callback) ->
|
||||||
|
@ -257,6 +271,8 @@ UserHandler = class UserHandler extends Handler
|
||||||
return @getRemark(req, res, args[0]) if args[1] is 'remark'
|
return @getRemark(req, res, args[0]) if args[1] is 'remark'
|
||||||
return @searchForUser(req, res) if args[1] is 'admin_search'
|
return @searchForUser(req, res) if args[1] is 'admin_search'
|
||||||
return @getStripeInfo(req, res, args[0]) if args[1] is 'stripe'
|
return @getStripeInfo(req, res, args[0]) if args[1] is 'stripe'
|
||||||
|
return @getSubRecipients(req, res) if args[1] is 'sub_recipients'
|
||||||
|
return @getSubSponsor(req, res) if args[1] is 'sub_sponsor'
|
||||||
return @sendOneTimeEmail(req, res, args[0]) if args[1] is 'send_one_time_email'
|
return @sendOneTimeEmail(req, res, args[0]) if args[1] is 'send_one_time_email'
|
||||||
return @sendNotFoundError(res)
|
return @sendNotFoundError(res)
|
||||||
super(arguments...)
|
super(arguments...)
|
||||||
|
@ -268,7 +284,67 @@ UserHandler = class UserHandler extends Handler
|
||||||
return @sendNotFoundError(res) if not customerID = user.get('stripe')?.customerID
|
return @sendNotFoundError(res) if not customerID = user.get('stripe')?.customerID
|
||||||
stripe.customers.retrieve customerID, (err, customer) =>
|
stripe.customers.retrieve customerID, (err, customer) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
@sendSuccess(res, JSON.stringify(customer, null, '\t'))
|
info = card: customer.sources?.data?[0]
|
||||||
|
findStripeSubscription customerID, subscriptionID: user.get('stripe').subscriptionID, (subscription) =>
|
||||||
|
info.subscription = subscription
|
||||||
|
findStripeSubscription customerID, subscriptionID: user.get('stripe').sponsorSubscriptionID, (subscription) =>
|
||||||
|
info.sponsorSubscription = subscription
|
||||||
|
@sendSuccess(res, JSON.stringify(info, null, '\t'))
|
||||||
|
|
||||||
|
getSubRecipients: (req, res) ->
|
||||||
|
# Return map of userIDs to name/email/cancel date
|
||||||
|
# TODO: Add test for this API
|
||||||
|
|
||||||
|
return @sendSuccess(res, {}) if _.isEmpty(req.user?.get('stripe')?.recipients ? [])
|
||||||
|
return @sendSuccess(res, {}) unless req.user.get('stripe')?.customerID?
|
||||||
|
|
||||||
|
# Get recipients User info
|
||||||
|
ids = (recipient.userID for recipient in req.user.get('stripe').recipients)
|
||||||
|
User.find({'_id': { $in: ids} }, 'name emailLower').exec (err, users) =>
|
||||||
|
info = {}
|
||||||
|
_.each users, (user) -> info[user.id] = user.toObject()
|
||||||
|
customerID = req.user.get('stripe').customerID
|
||||||
|
|
||||||
|
nextBatch = (starting_after, done) ->
|
||||||
|
options = limit: 100
|
||||||
|
options.starting_after = starting_after if starting_after
|
||||||
|
stripe.customers.listSubscriptions customerID, options, (err, subscriptions) ->
|
||||||
|
return done(err) if err
|
||||||
|
return done() unless subscriptions?.data?.length > 0
|
||||||
|
for sub in subscriptions.data
|
||||||
|
userID = sub.metadata?.id
|
||||||
|
continue unless userID of info
|
||||||
|
if sub.cancel_at_period_end and info[userID]['cancel_at_period_end'] isnt false
|
||||||
|
info[userID]['cancel_at_period_end'] = new Date(sub.current_period_end * 1000)
|
||||||
|
else
|
||||||
|
info[userID]['cancel_at_period_end'] = false
|
||||||
|
|
||||||
|
if subscriptions.has_more
|
||||||
|
return nextBatch(subscriptions.data[subscriptions.data.length - 1].id, done)
|
||||||
|
else
|
||||||
|
return done()
|
||||||
|
nextBatch null, (err) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res, info)
|
||||||
|
|
||||||
|
getSubSponsor: (req, res) ->
|
||||||
|
# TODO: Add test for this API
|
||||||
|
|
||||||
|
return @sendSuccess(res, {}) unless req.user?.get('stripe')?.sponsorID?
|
||||||
|
|
||||||
|
# Get sponsor User info
|
||||||
|
User.findById req.user.get('stripe').sponsorID, (err, sponsor) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendDatabaseError(res, 'No sponsor customerID') unless sponsor?.get('stripe')?.customerID?
|
||||||
|
info =
|
||||||
|
email: sponsor.get('emailLower')
|
||||||
|
name: sponsor.get('name')
|
||||||
|
|
||||||
|
# Get recipient subscription info
|
||||||
|
findStripeSubscription sponsor.get('stripe').customerID, userID: req.user.id, (subscription) =>
|
||||||
|
info.subscription = subscription
|
||||||
|
@sendDatabaseError(res, 'No sponsored subscription found') unless info.subscription?
|
||||||
|
@sendSuccess(res, info)
|
||||||
|
|
||||||
sendOneTimeEmail: (req, res) ->
|
sendOneTimeEmail: (req, res) ->
|
||||||
# TODO: Should this API be somewhere else?
|
# TODO: Should this API be somewhere else?
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
console.log 'IT BEGINS'
|
console.log 'IT BEGINS'
|
||||||
|
|
||||||
require 'jasmine-spec-reporter'
|
require 'jasmine-spec-reporter'
|
||||||
|
jasmine.getEnv().defaultTimeoutInterval = 300000
|
||||||
jasmine.getEnv().reporter.subReporters_ = []
|
jasmine.getEnv().reporter.subReporters_ = []
|
||||||
jasmine.getEnv().addReporter(new jasmine.SpecReporter({
|
jasmine.getEnv().addReporter(new jasmine.SpecReporter({
|
||||||
displaySuccessfulSpec: true,
|
displaySuccessfulSpec: true,
|
||||||
|
@ -21,7 +22,6 @@ mongoose.connect('mongodb://localhost/coco_unittest')
|
||||||
path = require 'path'
|
path = require 'path'
|
||||||
GLOBAL.testing = true
|
GLOBAL.testing = true
|
||||||
GLOBAL.tv4 = require 'tv4' # required for TreemaUtils to work
|
GLOBAL.tv4 = require 'tv4' # required for TreemaUtils to work
|
||||||
# _.str = require 'underscore.string'
|
|
||||||
|
|
||||||
models_path = [
|
models_path = [
|
||||||
'../../server/analytics/AnalyticsUsersActive'
|
'../../server/analytics/AnalyticsUsersActive'
|
||||||
|
@ -113,6 +113,25 @@ wrapUpGetUser = (email, user, done) ->
|
||||||
GLOBAL.getURL = (path) ->
|
GLOBAL.getURL = (path) ->
|
||||||
return 'http://localhost:3001' + path
|
return 'http://localhost:3001' + path
|
||||||
|
|
||||||
|
newUserCount = 0
|
||||||
|
GLOBAL.createNewUser = (done) ->
|
||||||
|
name = password = "user#{newUserCount++}"
|
||||||
|
email = "#{name}@foo.bar"
|
||||||
|
unittest.getUser name, email, password, done, true
|
||||||
|
GLOBAL.loginNewUser = (done) ->
|
||||||
|
name = password = "user#{newUserCount++}"
|
||||||
|
email = "#{name}@me.com"
|
||||||
|
request.post getURL('/auth/logout'), ->
|
||||||
|
unittest.getUser name, email, password, (user) ->
|
||||||
|
req = request.post(getURL('/auth/login'), (error, response) ->
|
||||||
|
expect(response.statusCode).toBe(200)
|
||||||
|
done(user)
|
||||||
|
)
|
||||||
|
form = req.form()
|
||||||
|
form.append('username', email)
|
||||||
|
form.append('password', password)
|
||||||
|
, true
|
||||||
|
|
||||||
GLOBAL.loginJoe = (done) ->
|
GLOBAL.loginJoe = (done) ->
|
||||||
request.post getURL('/auth/logout'), ->
|
request.post getURL('/auth/logout'), ->
|
||||||
unittest.getNormalJoe (user) ->
|
unittest.getNormalJoe (user) ->
|
||||||
|
@ -147,6 +166,16 @@ GLOBAL.loginAdmin = (done) ->
|
||||||
form.append('password', '80yqxpb38j')
|
form.append('password', '80yqxpb38j')
|
||||||
# find some other way to make the admin object an admin... maybe directly?
|
# find some other way to make the admin object an admin... maybe directly?
|
||||||
|
|
||||||
|
GLOBAL.loginUser = (user, done) ->
|
||||||
|
request.post getURL('/auth/logout'), ->
|
||||||
|
req = request.post(getURL('/auth/login'), (error, response) ->
|
||||||
|
expect(response.statusCode).toBe(200)
|
||||||
|
done(user)
|
||||||
|
)
|
||||||
|
form = req.form()
|
||||||
|
form.append('username', user.get('email'))
|
||||||
|
form.append('password', user.get('name'))
|
||||||
|
|
||||||
GLOBAL.dropGridFS = (done) ->
|
GLOBAL.dropGridFS = (done) ->
|
||||||
if mongoose.connection.readyState is 2
|
if mongoose.connection.readyState is 2
|
||||||
mongoose.connection.once 'open', ->
|
mongoose.connection.once 'open', ->
|
||||||
|
|
|
@ -66,7 +66,7 @@ describe '/db/user, editing stripe.couponID property', ->
|
||||||
expect(body.stripe.couponID).toBe('500off')
|
expect(body.stripe.couponID).toBe('500off')
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'applies a discount to the newly created customer when a plan is set', (done) ->
|
it 'applies a discount to the newly created subscription when a plan is set', (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' }
|
||||||
}, (err, token) ->
|
}, (err, token) ->
|
||||||
|
@ -77,9 +77,9 @@ describe '/db/user, editing stripe.couponID property', ->
|
||||||
request.put {uri: userURL, json: joeData, headers: {'X-Change-Plan': 'true'} }, (err, res, body) ->
|
request.put {uri: userURL, json: joeData, headers: {'X-Change-Plan': 'true'} }, (err, res, body) ->
|
||||||
joeData = body
|
joeData = body
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
|
stripe.customers.retrieveSubscription joeData.stripe.customerID, joeData.stripe.subscriptionID, (err, subscription) ->
|
||||||
expect(customer.discount).toBeDefined()
|
expect(subscription.discount).toBeDefined()
|
||||||
expect(customer.discount?.coupon.id).toBe('500off')
|
expect(subscription.discount?.coupon.id).toBe('500off')
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
async = require 'async'
|
||||||
config = require '../../../server_config'
|
config = require '../../../server_config'
|
||||||
require '../common'
|
require '../common'
|
||||||
|
utils = require '../../../app/core/utils' # Must come after require /common
|
||||||
|
mongoose = require 'mongoose'
|
||||||
|
|
||||||
# sample data that comes in through the webhook when you subscribe
|
# sample data that comes in through the webhook when you subscribe
|
||||||
invoiceChargeSampleEvent = {
|
invoiceChargeSampleEvent = {
|
||||||
id: 'evt_155TBeKaReE7xLUdrKM72O5R',
|
id: 'evt_155TBeKaReE7xLUdrKM72O5R',
|
||||||
created: 1417574898,
|
created: 1417574898,
|
||||||
livemode: false,
|
livemode: false,
|
||||||
|
@ -38,13 +40,13 @@ invoiceChargeSampleEvent = {
|
||||||
metadata: {},
|
metadata: {},
|
||||||
statement_description: null,
|
statement_description: null,
|
||||||
description: null,
|
description: null,
|
||||||
receipt_number: null
|
receipt_number: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
object: 'event',
|
object: 'event',
|
||||||
pending_webhooks: 1,
|
pending_webhooks: 1,
|
||||||
request: 'iar_5Fz9c4BZJyNNsM',
|
request: 'iar_5Fz9c4BZJyNNsM',
|
||||||
api_version: '2014-11-05'
|
api_version: '2015-02-18'
|
||||||
}
|
}
|
||||||
|
|
||||||
customerSubscriptionDeletedSampleEvent = {
|
customerSubscriptionDeletedSampleEvent = {
|
||||||
|
@ -70,13 +72,13 @@ customerSubscriptionDeletedSampleEvent = {
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
application_fee_percent: null,
|
application_fee_percent: null,
|
||||||
discount: null,
|
discount: null,
|
||||||
metadata: {}
|
metadata: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
object: 'event',
|
object: 'event',
|
||||||
pending_webhooks: 1,
|
pending_webhooks: 1,
|
||||||
request: 'iar_5FziYQJ4oQdL6w',
|
request: 'iar_5FziYQJ4oQdL6w',
|
||||||
api_version: '2014-11-05'
|
api_version: '2015-02-18'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,7 +105,7 @@ describe '/db/user, editing stripe property', ->
|
||||||
#- shared data between tests
|
#- shared data between tests
|
||||||
joeData = null
|
joeData = null
|
||||||
firstSubscriptionID = null
|
firstSubscriptionID = null
|
||||||
|
|
||||||
it 'returns client error when a token fails to charge', (done) ->
|
it 'returns client error when a token fails to charge', (done) ->
|
||||||
stripe.tokens.create {
|
stripe.tokens.create {
|
||||||
card: { number: '4000000000000002', exp_month: 12, exp_year: 2020, cvc: '123' }
|
card: { number: '4000000000000002', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
@ -118,7 +120,7 @@ describe '/db/user, editing stripe property', ->
|
||||||
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
||||||
expect(res.statusCode).toBe(402)
|
expect(res.statusCode).toBe(402)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'creates a subscription when you put a token and plan', (done) ->
|
it 'creates a subscription when you put a token and plan', (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' }
|
||||||
|
@ -139,14 +141,14 @@ describe '/db/user, editing stripe property', ->
|
||||||
expect(joeData.stripe.planID).toBe('basic')
|
expect(joeData.stripe.planID).toBe('basic')
|
||||||
expect(joeData.stripe.token).toBeUndefined()
|
expect(joeData.stripe.token).toBeUndefined()
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'records a payment through the webhook', (done) ->
|
it 'records a payment through the webhook', (done) ->
|
||||||
# Don't even want to think about hooking in tests to webhooks, so... put in some data manually
|
# Don't even want to think about hooking in tests to webhooks, so... put in some data manually
|
||||||
stripe.invoices.list {customer: joeData.stripe.customerID}, (err, invoices) ->
|
stripe.invoices.list {customer: joeData.stripe.customerID}, (err, invoices) ->
|
||||||
expect(invoices.data.length).toBe(1)
|
expect(invoices.data.length).toBe(1)
|
||||||
event = _.cloneDeep(invoiceChargeSampleEvent)
|
event = _.cloneDeep(invoiceChargeSampleEvent)
|
||||||
event.data.object = invoices.data[0]
|
event.data.object = invoices.data[0]
|
||||||
|
|
||||||
request.post {uri: webhookURL, json: event}, (err, res, body) ->
|
request.post {uri: webhookURL, json: event}, (err, res, body) ->
|
||||||
expect(res.statusCode).toBe(201)
|
expect(res.statusCode).toBe(201)
|
||||||
Payment.find {}, (err, payments) ->
|
Payment.find {}, (err, payments) ->
|
||||||
|
@ -154,7 +156,7 @@ describe '/db/user, editing stripe property', ->
|
||||||
User.findById joeData._id, (err, user) ->
|
User.findById joeData._id, (err, user) ->
|
||||||
expect(user.get('purchased').gems).toBe(3500)
|
expect(user.get('purchased').gems).toBe(3500)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'schedules the stripe subscription to be cancelled when stripe.planID is removed from the user', (done) ->
|
it 'schedules the stripe subscription to be cancelled when stripe.planID is removed from the user', (done) ->
|
||||||
delete joeData.stripe.planID
|
delete joeData.stripe.planID
|
||||||
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
||||||
|
@ -168,19 +170,18 @@ describe '/db/user, editing stripe property', ->
|
||||||
expect(customer.subscriptions.data[0].cancel_at_period_end).toBe(true)
|
expect(customer.subscriptions.data[0].cancel_at_period_end).toBe(true)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
||||||
it 'allows you to sign up again using the same customer ID as before, no token necessary', (done) ->
|
it 'allows you to sign up again using the same customer ID as before, no token necessary', (done) ->
|
||||||
joeData.stripe.planID = 'basic'
|
joeData.stripe.planID = 'basic'
|
||||||
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
||||||
joeData = body
|
joeData = body
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
expect(joeData.stripe.customerID).toBeDefined()
|
expect(joeData.stripe.customerID).toBeDefined()
|
||||||
expect(joeData.stripe.subscriptionID).toBeDefined()
|
expect(joeData.stripe.subscriptionID).toBeDefined()
|
||||||
expect(joeData.stripe.subscriptionID).not.toBe(firstSubscriptionID)
|
expect(joeData.stripe.subscriptionID).not.toBe(firstSubscriptionID)
|
||||||
expect(joeData.stripe.planID).toBe('basic')
|
expect(joeData.stripe.planID).toBe('basic')
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'will not have immediately created new payments when signing back up from a cancelled subscription', (done) ->
|
it 'will not have immediately created new payments when signing back up from a cancelled subscription', (done) ->
|
||||||
stripe.invoices.list {customer: joeData.stripe.customerID}, (err, invoices) ->
|
stripe.invoices.list {customer: joeData.stripe.customerID}, (err, invoices) ->
|
||||||
expect(invoices.data.length).toBe(2)
|
expect(invoices.data.length).toBe(2)
|
||||||
|
@ -206,7 +207,7 @@ describe '/db/user, editing stripe property', ->
|
||||||
expect(user.get('stripe').subscriptionID).toBeUndefined()
|
expect(user.get('stripe').subscriptionID).toBeUndefined()
|
||||||
expect(user.get('stripe').planID).toBeUndefined()
|
expect(user.get('stripe').planID).toBeUndefined()
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "updates the customer's email when you change the user's email", (done) ->
|
it "updates the customer's email when you change the user's email", (done) ->
|
||||||
joeData.email = 'newEmail@gmail.com'
|
joeData.email = 'newEmail@gmail.com'
|
||||||
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
||||||
|
@ -214,3 +215,762 @@ describe '/db/user, editing stripe property', ->
|
||||||
expect(customer.email).toBe('newEmail@gmail.com')
|
expect(customer.email).toBe('newEmail@gmail.com')
|
||||||
done()
|
done()
|
||||||
setTimeout(f, 500) # bit of a race condition here, response returns before stripe has been updated
|
setTimeout(f, 500) # bit of a race condition here, response returns before stripe has been updated
|
||||||
|
|
||||||
|
|
||||||
|
describe 'Sponsored subscriptions', ->
|
||||||
|
# TODO: Test recurring billing via webhooks
|
||||||
|
# TODO: Test error rollbacks, Stripe is authority
|
||||||
|
|
||||||
|
stripe = require('stripe')(config.stripe.secretKey)
|
||||||
|
userURL = getURL('/db/user')
|
||||||
|
webhookURL = getURL('/stripe/webhook')
|
||||||
|
headers = {'X-Change-Plan': 'true'}
|
||||||
|
subPrice = 999
|
||||||
|
subGems = 3500
|
||||||
|
invoicesWebHooked = {}
|
||||||
|
|
||||||
|
# Start helpers
|
||||||
|
|
||||||
|
getSubscribedQuantity = (numSponsored) ->
|
||||||
|
return 0 if numSponsored < 1
|
||||||
|
if numSponsored <= 10
|
||||||
|
Math.round(numSponsored * subPrice * 0.8)
|
||||||
|
else
|
||||||
|
Math.round(10 * subPrice * 0.8 + (numSponsored - 10) * subPrice * 0.6)
|
||||||
|
|
||||||
|
getUnsubscribedQuantity = (numSponsored) ->
|
||||||
|
return 0 if numSponsored < 1
|
||||||
|
if numSponsored <= 1
|
||||||
|
subPrice
|
||||||
|
else if numSponsored <= 11
|
||||||
|
Math.round(subPrice + (numSponsored - 1) * subPrice * 0.8)
|
||||||
|
else
|
||||||
|
Math.round(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6)
|
||||||
|
|
||||||
|
verifyNotRecipient = (userID, done) ->
|
||||||
|
User.findById userID, (err, user) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
if stripeInfo = user.get('stripe')
|
||||||
|
expect(stripeInfo.sponsorID).toBeUndefined()
|
||||||
|
done()
|
||||||
|
|
||||||
|
verifyNotSponsoring = (sponsorID, recipientID, done) ->
|
||||||
|
# console.log 'verifyNotSponsoring', sponsorID, recipientID
|
||||||
|
User.findById sponsorID, (err, sponsor) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
stripeInfo = sponsor.get('stripe')
|
||||||
|
return done() unless stripeInfo?.customerID?
|
||||||
|
checkSubscriptions = (starting_after, done) ->
|
||||||
|
options = {}
|
||||||
|
options.starting_after = starting_after if starting_after
|
||||||
|
stripe.customers.listSubscriptions stripeInfo.customerID, options, (err, subscriptions) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
for subscription in subscriptions.data
|
||||||
|
if subscription.plan.id is 'basic'
|
||||||
|
expect(subscription.metadata.id).not.toEqual(recipientID)
|
||||||
|
if subscription.plan.id is 'incremental'
|
||||||
|
expect(subscription.metadata.id).toEqual(sponsorID)
|
||||||
|
if subscriptions.has_more
|
||||||
|
checkSubscriptions subscriptions.data[subscriptions.data.length - 1].id, done
|
||||||
|
else
|
||||||
|
done()
|
||||||
|
checkSubscriptions null, done
|
||||||
|
|
||||||
|
verifySponsorship = (sponsorUserID, sponsoredUserID, done) ->
|
||||||
|
# console.log 'verifySponsorship', sponsorUserID, sponsoredUserID
|
||||||
|
User.findById sponsorUserID, (err, user) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user).not.toBeNull()
|
||||||
|
sponsorStripe = user.get('stripe')
|
||||||
|
sponsorCustomerID = sponsorStripe.customerID
|
||||||
|
numSponsored = sponsorStripe.recipients.length
|
||||||
|
expect(sponsorCustomerID).toBeDefined()
|
||||||
|
expect(sponsorStripe.sponsorSubscriptionID).toBeDefined()
|
||||||
|
expect(sponsorStripe.token).toBeUndefined()
|
||||||
|
expect(numSponsored).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
# Verify Stripe sponsor subscription data
|
||||||
|
stripe.customers.retrieveSubscription sponsorCustomerID, sponsorStripe.sponsorSubscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription.plan.amount).toEqual(1)
|
||||||
|
expect(subscription.customer).toEqual(sponsorCustomerID)
|
||||||
|
expect(subscription.quantity).toEqual(utils.getSponsoredSubsAmount(subPrice, numSponsored, sponsorStripe.subscriptionID?))
|
||||||
|
|
||||||
|
# Verify sponsor payment
|
||||||
|
# May be greater than expected amount due to multiple subscribes and unsubscribes
|
||||||
|
paymentQuery =
|
||||||
|
purchaser: mongoose.Types.ObjectId(sponsorUserID)
|
||||||
|
recipient: mongoose.Types.ObjectId(sponsorUserID)
|
||||||
|
"stripe.customerID": sponsorCustomerID
|
||||||
|
"stripe.subscriptionID": sponsorStripe.sponsorSubscriptionID
|
||||||
|
expectedAmount = utils.getSponsoredSubsAmount(subPrice, numSponsored, sponsorStripe.subscriptionID?)
|
||||||
|
Payment.find paymentQuery, (err, payments) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(payments).not.toBeNull()
|
||||||
|
amount = 0
|
||||||
|
for payment in payments
|
||||||
|
amount += payment.get('amount')
|
||||||
|
expect(payment.get('gems')).toBeUndefined()
|
||||||
|
|
||||||
|
# NOTE: this amount may be greater than the expected amount due to proration accumlation
|
||||||
|
# NOTE: during localy execution, this is usually only 1-2 cents
|
||||||
|
expect(amount).toBeGreaterThan(expectedAmount - 50)
|
||||||
|
|
||||||
|
# Find recipient info from sponsor stripe data
|
||||||
|
for r in sponsorStripe.recipients
|
||||||
|
if r.userID is sponsoredUserID
|
||||||
|
recipientInfo = r
|
||||||
|
break
|
||||||
|
expect(recipientInfo).toBeDefined()
|
||||||
|
expect(recipientInfo.subscriptionID).toBeDefined()
|
||||||
|
expect(recipientInfo.subscriptionID).toNotEqual(sponsorStripe.sponsorSubscriptionID)
|
||||||
|
expect(recipientInfo.couponID).toEqual('free')
|
||||||
|
|
||||||
|
# Verify Stripe recipient subscription data
|
||||||
|
stripe.customers.retrieveSubscription sponsorCustomerID, recipientInfo.subscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription.plan.amount).toEqual(subPrice)
|
||||||
|
expect(subscription.customer).toEqual(sponsorCustomerID)
|
||||||
|
expect(subscription.quantity).toEqual(1)
|
||||||
|
expect(subscription.metadata.id).toEqual(sponsoredUserID)
|
||||||
|
expect(subscription.discount.coupon.id).toEqual(recipientInfo.couponID)
|
||||||
|
|
||||||
|
# Verify recipient internal data
|
||||||
|
User.findById sponsoredUserID, (err, recipient) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
stripeInfo = recipient.get('stripe')
|
||||||
|
expect(stripeInfo.sponsorID).toEqual(sponsorUserID)
|
||||||
|
unless stripeInfo.sponsorSubscriptionID?
|
||||||
|
expect(stripeInfo.customerID).toBeUndefined()
|
||||||
|
expect(stripeInfo.token).toBeUndefined()
|
||||||
|
expect(recipient.get('purchased').gems).toBeGreaterThan(subGems - 1)
|
||||||
|
expect(recipient.isPremium()).toEqual(true)
|
||||||
|
|
||||||
|
# Verify recipient payment
|
||||||
|
# TODO: Not accurate enough when resubscribing a user
|
||||||
|
paymentQuery =
|
||||||
|
purchaser: mongoose.Types.ObjectId(sponsorUserID)
|
||||||
|
recipient: mongoose.Types.ObjectId(sponsoredUserID)
|
||||||
|
"stripe.customerID": sponsorCustomerID
|
||||||
|
Payment.findOne paymentQuery, (err, payment) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(payment).not.toBeNull()
|
||||||
|
expect(payment.get('amount')).toEqual(0)
|
||||||
|
expect(payment.get('gems')).toBeGreaterThan(subGems - 1)
|
||||||
|
done()
|
||||||
|
|
||||||
|
subscribeUser = (user, token, done) ->
|
||||||
|
requestBody = user.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
planID: 'basic'
|
||||||
|
requestBody.stripe.token = token.id if token?
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(body.stripe.customerID).toBeDefined()
|
||||||
|
expect(body.stripe.planID).toBe('basic')
|
||||||
|
expect(body.stripe.token).toBeUndefined()
|
||||||
|
expect(body.purchased.gems).toBeGreaterThan(subGems - 1)
|
||||||
|
done()
|
||||||
|
|
||||||
|
unsubscribeUser = (user, done) ->
|
||||||
|
requestBody = user.toObject()
|
||||||
|
delete requestBody.stripe.planID
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
User.findById user.id, (err, user) ->
|
||||||
|
expect(user.get('stripe').customerID).toBeDefined()
|
||||||
|
expect(user.get('stripe').planID).toBeUndefined()
|
||||||
|
expect(user.get('stripe').token).toBeUndefined()
|
||||||
|
done()
|
||||||
|
|
||||||
|
subscribeRecipients = (sponsor, recipients, token, done) ->
|
||||||
|
# console.log 'subscribeRecipients', sponsor.id, (recipient.id for recipient in recipients), token?
|
||||||
|
requestBody = sponsor.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
subscribeEmails: (recipient.get('email') for recipient in recipients)
|
||||||
|
requestBody.stripe.token = token.id if token?
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(body.stripe.customerID).toBeDefined()
|
||||||
|
updatedUser = body
|
||||||
|
|
||||||
|
# Call webhooks for invoices
|
||||||
|
options = customer: body.stripe.customerID, limit: 100
|
||||||
|
stripe.invoices.list options, (err, invoices) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(invoices.has_more).toEqual(false)
|
||||||
|
makeWebhookCall = (invoice) ->
|
||||||
|
(callback) ->
|
||||||
|
event = _.cloneDeep(invoiceChargeSampleEvent)
|
||||||
|
event.data.object = invoice
|
||||||
|
# console.log 'Calling webhook', event.type, invoice.id
|
||||||
|
request.post {uri: webhookURL, json: event}, (err, res, body) ->
|
||||||
|
callback err
|
||||||
|
webhookTasks = []
|
||||||
|
for invoice in invoices.data
|
||||||
|
unless invoice.id of invoicesWebHooked
|
||||||
|
invoicesWebHooked[invoice.id] = true
|
||||||
|
webhookTasks.push makeWebhookCall(invoice)
|
||||||
|
async.parallel webhookTasks, (err, results) ->
|
||||||
|
expect(err?).toEqual(false)
|
||||||
|
done(updatedUser)
|
||||||
|
|
||||||
|
unsubscribeRecipient = (sponsor, recipient, immediately, done) ->
|
||||||
|
# console.log 'unsubscribeRecipient', sponsor.id, recipient.id
|
||||||
|
stripeInfo = sponsor.get('stripe')
|
||||||
|
customerID = stripeInfo.customerID
|
||||||
|
for r in stripeInfo.recipients
|
||||||
|
if r.userID is recipient.id
|
||||||
|
subscriptionID = r.subscriptionID
|
||||||
|
break
|
||||||
|
expect(customerID).toBeDefined()
|
||||||
|
expect(subscriptionID).toBeDefined()
|
||||||
|
|
||||||
|
# Find Stripe subscription
|
||||||
|
stripe.customers.retrieveSubscription customerID, subscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
|
||||||
|
# Call unsubscribe API
|
||||||
|
requestBody = sponsor.toObject()
|
||||||
|
requestBody.stripe = unsubscribeEmail: recipient.get('email')
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
|
||||||
|
# Simulate subscription ending after cancellation
|
||||||
|
return done() unless immediately
|
||||||
|
|
||||||
|
# Simulate subscription cancelling a trial end
|
||||||
|
stripe.customers.cancelSubscription customerID, subscriptionID, (err) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
# Simulate customer.subscription.deleted webhook event
|
||||||
|
event = _.cloneDeep(customerSubscriptionDeletedSampleEvent)
|
||||||
|
event.data.object = subscription
|
||||||
|
request.post {uri: webhookURL, json: event}, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
done()
|
||||||
|
|
||||||
|
# Subscribe a bunch of recipients at once, used for bulk discount testing
|
||||||
|
class SubbedRecipients
|
||||||
|
constructor: (@count, @toVerify) ->
|
||||||
|
@index = 0
|
||||||
|
@recipients = []
|
||||||
|
|
||||||
|
length: ->
|
||||||
|
@recipients.length
|
||||||
|
|
||||||
|
get: (i) ->
|
||||||
|
@recipients[i]
|
||||||
|
|
||||||
|
createRecipients: (done) ->
|
||||||
|
return done() if @recipients.length is @count
|
||||||
|
createNewUser (user) =>
|
||||||
|
@recipients.push user
|
||||||
|
@createRecipients done
|
||||||
|
|
||||||
|
subRecipients: (user1, token=null, done) ->
|
||||||
|
# console.log 'subRecipients', user1.id, @recipients.length
|
||||||
|
User.findById user1.id, (err, user1) =>
|
||||||
|
subscribeRecipients user1, @recipients, token, (updatedUser) =>
|
||||||
|
verifyIndex = 0
|
||||||
|
verify = =>
|
||||||
|
return done(updatedUser) if verifyIndex >= @toVerify.length
|
||||||
|
verifySponsorship user1.id, @recipients[verifyIndex].id, =>
|
||||||
|
verifyIndex++
|
||||||
|
verify()
|
||||||
|
verify()
|
||||||
|
|
||||||
|
# End helpers
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Use beforeAll()
|
||||||
|
it 'Clear database users and payments', (done) ->
|
||||||
|
clearModels [User, Payment], (err) ->
|
||||||
|
throw err if err
|
||||||
|
done()
|
||||||
|
|
||||||
|
describe 'Basic', ->
|
||||||
|
it 'Unsubscribed user1 subscribes user2', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
verifySponsorship user1.id, 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' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
unsubscribeRecipient user1, user2, true, ->
|
||||||
|
verifyNotSponsoring user1.id, user2.id, ->
|
||||||
|
verifyNotRecipient user2.id, done
|
||||||
|
|
||||||
|
it 'Unsubscribed user1 immediately resubscribes user2, one token', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
unsubscribeRecipient user1, user2, false, ->
|
||||||
|
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
||||||
|
verifySponsorship user1.id, user2.id, done
|
||||||
|
|
||||||
|
it 'Sponsored user2 subscribes their sponsor user1', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
loginUser user2, (user2) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
subscribeRecipients user2, [user1], token, (updatedUser) ->
|
||||||
|
verifySponsorship user1.id, user2.id, ->
|
||||||
|
verifySponsorship user2.id, user1.id, done
|
||||||
|
|
||||||
|
it 'Unsubscribed user1 subscribes user1', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
|
||||||
|
requestBody = user1.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
subscribeEmails: [user1.get('email')]
|
||||||
|
token: token.id
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
|
||||||
|
User.findById user1.id, (err, user) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
stripeInfo = user.get('stripe')
|
||||||
|
expect(stripeInfo.customerID).toBeDefined()
|
||||||
|
expect(stripeInfo.planID).toBeUndefined()
|
||||||
|
expect(stripeInfo.subscriptionID).toBeUndefined()
|
||||||
|
expect(stripeInfo.recipients.length).toEqual(0)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Subscribed user1 subscribes user2, one token', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeUser user1, token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.get('stripe').subscriptionID).toBeDefined()
|
||||||
|
expect(user1.isPremium()).toEqual(true)
|
||||||
|
verifySponsorship user1.id, user2.id, done
|
||||||
|
|
||||||
|
it 'Subscribed user1 unsubscribes user2', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeUser user1, token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
||||||
|
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.isPremium()).toEqual(true)
|
||||||
|
User.findById user2.id, (err, user2) ->
|
||||||
|
verifyNotSponsoring user1.id, user2.id, ->
|
||||||
|
verifyNotRecipient user2.id, done
|
||||||
|
|
||||||
|
it 'Subscribed user1 subscribes user2, unsubscribes themselves', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeUser user1, token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
unsubscribeUser user1, (updatedUser) ->
|
||||||
|
verifySponsorship user1.id, user2.id, done
|
||||||
|
|
||||||
|
it 'Sponsored user2 tries to subscribe', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
loginUser user2, (user2) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
requestBody = user2.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
token: token.id
|
||||||
|
planID: 'basic'
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Sponsored user2 tries to unsubscribe', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
loginUser user2, (user2) ->
|
||||||
|
requestBody = user2.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
recipient: user2.id
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
verifySponsorship user1.id, user2.id, done
|
||||||
|
|
||||||
|
it 'Cancel sponsor subscription with 2 recipient subscriptions, then subscribe 1 old and 1 new', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user3) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2, user3], token, (updatedUser) ->
|
||||||
|
customerID = updatedUser.stripe.customerID
|
||||||
|
subscriptionID = updatedUser.stripe.sponsorSubscriptionID
|
||||||
|
|
||||||
|
# Find Stripe sponsor subscription
|
||||||
|
stripe.customers.retrieveSubscription customerID, subscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
|
||||||
|
# Cancel Stripe sponsor subscription
|
||||||
|
stripe.customers.cancelSubscription customerID, subscriptionID, (err) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
# Simulate customer.subscription.deleted webhook event for sponsor subscription
|
||||||
|
event = _.cloneDeep(customerSubscriptionDeletedSampleEvent)
|
||||||
|
event.data.object = subscription
|
||||||
|
request.post {uri: webhookURL, json: event}, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
# Should have 2 cancelled recipient subs with cancel_at_period_end = true
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.sponsorSubscriptionID).toBeUndefined()
|
||||||
|
expect(stripeInfo.recipients).toBeUndefined()
|
||||||
|
stripe.customers.listSubscriptions stripeInfo.customerID, (err, subscriptions) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscriptions.data.length).toEqual(2)
|
||||||
|
for sub in subscriptions.data
|
||||||
|
expect(sub.plan.id).toEqual('basic')
|
||||||
|
expect(sub.cancel_at_period_end).toEqual(true)
|
||||||
|
|
||||||
|
# Subscribe user3 back
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
subscribeRecipients user1, [user3], null, (updatedUser) ->
|
||||||
|
verifySponsorship user1.id, user3.id, ->
|
||||||
|
|
||||||
|
# Subscribe new user4
|
||||||
|
createNewUser (user4) ->
|
||||||
|
loginUser user1, (user1) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
subscribeRecipients user1, [user4], null, (updatedUser) ->
|
||||||
|
verifySponsorship user1.id, user4.id, done
|
||||||
|
|
||||||
|
it 'Subscribing two users separately yields proration payment', (done) ->
|
||||||
|
# TODO: Use test plan with low duration + setTimeout to test delay between 2 subscribes
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user3) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
subscribeRecipients user1, [user3], null, (updatedUser) ->
|
||||||
|
# TODO: What do we expect invoices to show here?
|
||||||
|
stripe.invoices.list {customer: updatedUser.stripe.customerID}, (err, invoices) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
# Verify for proration invoice
|
||||||
|
foundProratedInvoice = false
|
||||||
|
for invoice in invoices.data
|
||||||
|
line = invoice.lines.data[0]
|
||||||
|
if line.type is 'invoiceitem' and line.proration
|
||||||
|
totalAmount = utils.getSponsoredSubsAmount(subPrice, 2, false)
|
||||||
|
expect(invoice.total).toBeLessThan(totalAmount)
|
||||||
|
expect(invoice.total).toEqual(totalAmount - subPrice)
|
||||||
|
Payment.findOne "stripe.invoiceID": invoice.id, (err, payment) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(payment.get('amount')).toEqual(invoice.total)
|
||||||
|
done()
|
||||||
|
foundProratedInvoice = true
|
||||||
|
break
|
||||||
|
unless foundProratedInvoice
|
||||||
|
expect(foundProratedInvoice).toEqual(true)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Invalid subscribeEmails', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
requestBody = user1.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
subscribeEmails: ['invalid@user.com', 'notemailformat', '', null, undefined]
|
||||||
|
requestBody.stripe.token = token.id
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(body.stripe).toBeDefined()
|
||||||
|
User.findById user1.id, (err, sponsor) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(sponsor.get('stripe')).toBeDefined()
|
||||||
|
expect(sponsor.get('stripe').customerID).toBeDefined()
|
||||||
|
expect(sponsor.get('stripe').sponsorSubscriptionID).toBeDefined()
|
||||||
|
expect(sponsor.get('stripe').recipients?.length).toEqual(0)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'User1 subscribes user2 then themselves', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
verifySponsorship user1.id, user2.id, ->
|
||||||
|
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
subscribeUser user1, token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.get('stripe').subscriptionID).toBeDefined()
|
||||||
|
expect(user1.isPremium()).toEqual(true)
|
||||||
|
|
||||||
|
stripe.customers.listSubscriptions user1.get('stripe').customerID, (err, subscriptions) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscriptions.data.length).toEqual(3)
|
||||||
|
for sub in subscriptions.data
|
||||||
|
if sub.plan.id is 'basic'
|
||||||
|
if sub.discount?.coupon?.id is 'free'
|
||||||
|
expect(sub.metadata?.id).toEqual(user2.id)
|
||||||
|
else
|
||||||
|
expect(sub.metadata?.id).toEqual(user1.id)
|
||||||
|
else
|
||||||
|
expect(sub.plan.id).toEqual('incremental')
|
||||||
|
expect(sub.metadata?.id).toEqual(user1.id)
|
||||||
|
done()
|
||||||
|
|
||||||
|
describe 'Bulk discounts', ->
|
||||||
|
# Bulk discount algorithm (includes personal sub):
|
||||||
|
# 1 100%
|
||||||
|
# 2-11 80%
|
||||||
|
# 12+ 60%
|
||||||
|
|
||||||
|
it 'Unsubscribed user1 subscribes two users', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user3) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2, user3], token, (updatedUser) ->
|
||||||
|
verifySponsorship user1.id, user2.id, ->
|
||||||
|
verifySponsorship user1.id, user3.id, done
|
||||||
|
|
||||||
|
it 'Subscribed user1 subscribes 2 users, unsubscribes 2', (done) ->
|
||||||
|
recipientCount = 2
|
||||||
|
recipientsToVerify = [0, 1]
|
||||||
|
recipients = new SubbedRecipients recipientCount, recipientsToVerify
|
||||||
|
|
||||||
|
# Create recipients
|
||||||
|
recipients.createRecipients ->
|
||||||
|
expect(recipients.length()).toEqual(recipientCount)
|
||||||
|
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
|
||||||
|
# Create sponsor user
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeUser user1, token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
# Subscribe recipients
|
||||||
|
recipients.subRecipients user1, null, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
|
||||||
|
# Unsubscribe recipient0
|
||||||
|
unsubscribeRecipient user1, recipients.get(0), true, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.recipients.length).toEqual(1)
|
||||||
|
verifyNotSponsoring user1.id, recipients.get(0).id, ->
|
||||||
|
verifyNotRecipient recipients.get(0).id, ->
|
||||||
|
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription.quantity).toEqual(getSubscribedQuantity(1))
|
||||||
|
|
||||||
|
# Unsubscribe recipient1
|
||||||
|
unsubscribeRecipient user1, recipients.get(1), true, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.recipients.length).toEqual(0)
|
||||||
|
verifyNotSponsoring user1.id, recipients.get(1).id, ->
|
||||||
|
verifyNotRecipient recipients.get(1).id, ->
|
||||||
|
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription.quantity).toEqual(0)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Subscribed user1 subscribes 3 users, unsubscribes 2, themselves, then 1', (done) ->
|
||||||
|
recipientCount = 3
|
||||||
|
recipientsToVerify = [0, 1, 2]
|
||||||
|
recipients = new SubbedRecipients recipientCount, recipientsToVerify
|
||||||
|
|
||||||
|
# Create recipients
|
||||||
|
recipients.createRecipients ->
|
||||||
|
expect(recipients.length()).toEqual(recipientCount)
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
|
||||||
|
# Create sponsor user
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeUser user1, token, (updatedUser) ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
# Subscribe recipients
|
||||||
|
recipients.subRecipients user1, null, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
|
||||||
|
# Unsubscribe first recipient
|
||||||
|
unsubscribeRecipient user1, recipients.get(0), true, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.recipients.length).toEqual(recipientCount - 1)
|
||||||
|
verifyNotSponsoring user1.id, recipients.get(0).id, ->
|
||||||
|
verifyNotRecipient recipients.get(0).id, ->
|
||||||
|
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 1))
|
||||||
|
|
||||||
|
# Unsubscribe second recipient
|
||||||
|
unsubscribeRecipient user1, recipients.get(1), true, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.recipients.length).toEqual(recipientCount - 2)
|
||||||
|
verifyNotSponsoring user1.id, recipients.get(1).id, ->
|
||||||
|
verifyNotRecipient recipients.get(1).id, ->
|
||||||
|
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 2))
|
||||||
|
|
||||||
|
# Unsubscribe self
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
unsubscribeUser user1, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.planID).toBeUndefined()
|
||||||
|
|
||||||
|
# Unsubscribe third recipient
|
||||||
|
verifySponsorship user1.id, recipients.get(2).id, ->
|
||||||
|
unsubscribeRecipient user1, recipients.get(2), true, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.recipients.length).toEqual(recipientCount - 3)
|
||||||
|
verifyNotSponsoring user1.id, recipients.get(2).id, ->
|
||||||
|
verifyNotRecipient recipients.get(2).id, ->
|
||||||
|
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 3))
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Unsubscribed user1 subscribes 13 users, unsubcribes 2', (done) ->
|
||||||
|
# TODO: verify interim invoices?
|
||||||
|
recipientCount = 13
|
||||||
|
recipientsToVerify = [0, 1, 10, 11, 12]
|
||||||
|
recipients = new SubbedRecipients recipientCount, recipientsToVerify
|
||||||
|
|
||||||
|
# Create recipients
|
||||||
|
recipients.createRecipients ->
|
||||||
|
expect(recipients.length()).toEqual(recipientCount)
|
||||||
|
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
|
||||||
|
# Create sponsor user
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
|
||||||
|
# Subscribe recipients
|
||||||
|
recipients.subRecipients user1, token, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
|
||||||
|
# Unsubscribe first recipient
|
||||||
|
unsubscribeRecipient user1, recipients.get(0), true, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.recipients.length).toEqual(recipientCount - 1)
|
||||||
|
verifyNotSponsoring user1.id, recipients.get(0).id, ->
|
||||||
|
verifyNotRecipient recipients.get(0).id, ->
|
||||||
|
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription.quantity).toEqual(getUnsubscribedQuantity(recipientCount - 1))
|
||||||
|
|
||||||
|
# Unsubscribe last recipient
|
||||||
|
unsubscribeRecipient user1, recipients.get(recipientCount - 1), true, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.recipients.length).toEqual(recipientCount - 2)
|
||||||
|
verifyNotSponsoring user1.id, recipients.get(recipientCount - 1).id, ->
|
||||||
|
verifyNotRecipient recipients.get(recipientCount - 1).id, ->
|
||||||
|
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
numSponsored = recipientCount - 2
|
||||||
|
if numSponsored <= 1
|
||||||
|
expect(subscription.quantity).toEqual(subPrice)
|
||||||
|
else if numSponsored <= 11
|
||||||
|
expect(subscription.quantity).toEqual(subPrice + (numSponsored - 1) * subPrice * 0.8)
|
||||||
|
else
|
||||||
|
expect(subscription.quantity).toEqual(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6)
|
||||||
|
done()
|
||||||
|
|
Loading…
Reference in a new issue