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
|
||||
return {'true': true, 'false': false}[pair[1]] ? decodeURIComponent(pair[1])
|
||||
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."
|
||||
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 Your Hero"
|
||||
programming_language: "Programming Language"
|
||||
|
|
|
@ -139,6 +139,7 @@ module.exports = class User extends CocoModel
|
|||
return true if me.isInGodMode()
|
||||
return true if me.isAdmin()
|
||||
return false unless stripe = @get('stripe')
|
||||
return true if stripe.sponsorID
|
||||
return true if stripe.subscriptionID
|
||||
return true if stripe.free is true
|
||||
return true if _.isString(stripe.free) and new Date() < new Date(stripe.free)
|
||||
|
|
|
@ -283,12 +283,22 @@ _.extend UserSchema.properties,
|
|||
|
||||
stripe: c.object {}, {
|
||||
customerID: { type: 'string' }
|
||||
planID: { enum: ['basic'] }
|
||||
subscriptionID: { type: 'string' }
|
||||
planID: { enum: ['basic'], description: 'Determines if a user has or wants to subscribe' }
|
||||
subscriptionID: { type: 'string', description: 'Determines if a user is subscribed' }
|
||||
token: { type: 'string' }
|
||||
couponID: { type: 'string' }
|
||||
discountID: { type: 'string' }
|
||||
free: { type: ['boolean', 'string'], format: 'date-time' }
|
||||
free: { type: ['boolean', 'string'], format: 'date-time', description: 'Type string is subscription end date' }
|
||||
|
||||
# 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' }
|
||||
|
|
|
@ -16,3 +16,25 @@
|
|||
button.btn
|
||||
width: 100%
|
||||
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")
|
||||
li.active(data-i18n="account.subscription")
|
||||
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
if subscribed
|
||||
button.end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.unsubscribe") Unsubscribe
|
||||
.unsubscribe-feedback.row.secret
|
||||
.col-lg-7
|
||||
h3
|
||||
if monthsSubscribed > 1
|
||||
span(data-i18n="subscribe.thank_you_months_prefix") Thank you for supporting us these last
|
||||
span.spl.spr= 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
|
||||
else if !me.isAnonymous()
|
||||
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
||||
|
||||
.panel-body
|
||||
table.table.table-striped
|
||||
tr
|
||||
th(data-i18n="user.status") Status
|
||||
td
|
||||
if subscribed
|
||||
strong(data-i18n="account.subscribed")
|
||||
if me.get('anonymous')
|
||||
p(data-i18n="account_settings.not_logged_in")
|
||||
else
|
||||
|
||||
//- Personal Subscriptions
|
||||
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3(data-i18n="subscribe.personal_sub")
|
||||
.panel-body
|
||||
if personalSub.state === 'loading'
|
||||
.alert.alert-info(data-i18n="subscribe.loading_info")
|
||||
else if personalSub.sponsor
|
||||
div
|
||||
span(data-i18n="subscribe.managed_by")
|
||||
span.spl.spr #{personalSub.sponsorName} (#{personalSub.sponsorEmail})
|
||||
if personalSub.endDate
|
||||
div
|
||||
span(data-i18n="subscribe.will_be_cancelled")
|
||||
span.spl.spr= moment(personalSub.endDate).format('l')
|
||||
else if personalSub.self
|
||||
if personalSub.subscribed
|
||||
button.end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.unsubscribe") Unsubscribe
|
||||
else
|
||||
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
||||
.unsubscribe-feedback.row.secret
|
||||
.col-lg-7
|
||||
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
|
||||
if 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 activeUntil
|
||||
span(data-i18n="subscribe.was_free_until")
|
||||
span.spl.spr= moment(new Date(personalSub.free)).format('l')
|
||||
|
||||
//- Sponsored Subscriptions
|
||||
|
||||
.panel.panel-default
|
||||
.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
|
||||
th(data-i18n="account.active_until")
|
||||
td= moment(activeUntil).format('l')
|
||||
if nextPaymentDate
|
||||
td(data-i18n="subscribe.group_discounts_2nd")
|
||||
td(data-i18n="subscribe.group_discounts_20")
|
||||
tr
|
||||
th(data-i18n="account.next_payment")
|
||||
td= moment(nextPaymentDate).format('l')
|
||||
if cost
|
||||
td(data-i18n="subscribe.group_discounts_12th")
|
||||
td(data-i18n="subscribe.group_discounts_40")
|
||||
.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
|
||||
th(data-i18n="account.cost")
|
||||
td= cost
|
||||
if card
|
||||
tr
|
||||
th(data-i18n="account.card")
|
||||
td= card
|
||||
th(data-i18n="general.email")
|
||||
th(data-i18n="general.name")
|
||||
th
|
||||
for recipient in recipientSubs.recipients
|
||||
tr
|
||||
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'
|
||||
SubscribeModal = require 'views/core/SubscribeModal'
|
||||
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
|
||||
id: "subscription-view"
|
||||
|
@ -13,41 +31,29 @@ module.exports = class SubscriptionView extends RootView
|
|||
'click .end-subscription-button': 'onClickEndSubscription'
|
||||
'click .cancel-end-subscription-button': 'onClickCancelEndSubscription'
|
||||
'click .confirm-end-subscription-button': 'onClickConfirmEndSubscription'
|
||||
'click .recipients-subscribe-button': 'onClickRecipientsSubscribe'
|
||||
'click .confirm-recipient-unsubscribe-button': 'onClickRecipientConfirmUnsubscribe'
|
||||
'click .recipient-unsubscribe-button': 'onClickRecipientUnsubscribe'
|
||||
|
||||
subscriptions:
|
||||
'subscribe-modal:subscribed': 'onSubscribed'
|
||||
'stripe:received-token': 'onStripeReceivedToken'
|
||||
|
||||
constructor: (options) ->
|
||||
super(options)
|
||||
if me.get('stripe')
|
||||
options = { cache: false, url: "/db/user/#{me.id}/stripe" }
|
||||
options.success = (@stripeInfo) =>
|
||||
@supermodel.addRequestResource('payment_info', options).load()
|
||||
@payments = new CocoCollection([], { url: '/db/payment', model: Payment, comparator:'_id' })
|
||||
@supermodel.loadCollection(@payments, 'payments', {cache: false})
|
||||
@personalSub = new PersonalSub(@supermodel)
|
||||
@recipientSubs = new RecipientSubs(@supermodel)
|
||||
@personalSub.update => @render?()
|
||||
@recipientSubs.update => @render?()
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
if @stripeInfo
|
||||
if subscription = @stripeInfo.subscriptions?.data?[0]
|
||||
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.personalSub = @personalSub
|
||||
c.recipientSubs = @recipientSubs
|
||||
c
|
||||
|
||||
# Personal Subscriptions
|
||||
|
||||
onClickStartSubscription: (e) ->
|
||||
@openModalView new SubscribeModal()
|
||||
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'account subscription view'
|
||||
|
@ -67,15 +73,198 @@ module.exports = class SubscriptionView extends RootView
|
|||
|
||||
onClickConfirmEndSubscription: (e) ->
|
||||
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 = =>
|
||||
stripe = _.clone(me.get('stripe'))
|
||||
delete stripe.planID
|
||||
me.set('stripe', stripe)
|
||||
stripeInfo = _.clone(me.get('stripe'))
|
||||
delete stripeInfo.planID
|
||||
me.set('stripe', stripeInfo)
|
||||
me.once 'sync', ->
|
||||
window.tracker?.trackEvent 'Unsubscribe End', message: message
|
||||
document.location.reload()
|
||||
me.patch({headers: {'X-Change-Plan': 'true'}})
|
||||
@listenToOnce me, 'sync', -> document.location.reload()
|
||||
if message
|
||||
$.post '/contact', message: message, subject: 'Cancellation', (response) ->
|
||||
removeStripe()
|
||||
else
|
||||
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
|
||||
# Each list in conditions is a function call.
|
||||
# The first arg is the function name
|
||||
# The rest are the agrs for the function
|
||||
# The rest are the args for the function
|
||||
|
||||
conditions = [
|
||||
['limit', 20]
|
||||
|
|
|
@ -4,6 +4,7 @@ mongoose = require 'mongoose'
|
|||
|
||||
module.exports =
|
||||
isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
|
||||
|
||||
objectIdFromTimestamp: (timestamp) ->
|
||||
# mongoDB ObjectId contains creation date in first 4 bytes
|
||||
# So, it can be used instead of a redundant created field
|
||||
|
@ -15,6 +16,36 @@ module.exports =
|
|||
hexSeconds = Math.floor(timestamp/1000).toString(16)
|
||||
# Create an ObjectId with that hex timestamp
|
||||
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) ->
|
||||
unless str?
|
||||
log.error "getAnalyticsStringID given invalid str param"
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
# 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.
|
||||
|
||||
async = require 'async'
|
||||
Handler = require '../commons/Handler'
|
||||
discountHandler = require './discount_handler'
|
||||
User = require '../users/User'
|
||||
{findStripeSubscription} = require '../lib/utils'
|
||||
{getSponsoredSubsAmount} = require '../../app/core/utils'
|
||||
|
||||
recipientCouponID = 'free'
|
||||
subscriptions = {
|
||||
basic: {
|
||||
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.')
|
||||
return done({res: 'Missing stripe token or customer ID.', code: 422})
|
||||
|
||||
# Create/retrieve Stripe customer
|
||||
if token
|
||||
if customerID
|
||||
stripe.customers.update customerID, { card: token }, (err, customer) =>
|
||||
if err or not customer
|
||||
# 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})
|
||||
@checkForExistingSubscription(req, user, customer, done)
|
||||
|
||||
else
|
||||
newCustomer = {
|
||||
options =
|
||||
card: token
|
||||
email: user.get('email')
|
||||
metadata: { id: user._id + '', slug: user.get('slug') }
|
||||
}
|
||||
|
||||
stripe.customers.create newCustomer, (err, customer) =>
|
||||
stripe.customers.create options, (err, customer) =>
|
||||
if err
|
||||
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
|
||||
return done({res: 'Card error', code: 402})
|
||||
else
|
||||
@logSubscriptionError(user, 'Stripe customer creation error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe customer creation error. ' + err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
|
||||
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||
|
@ -53,68 +58,78 @@ class SubscriptionHandler extends Handler
|
|||
user.set('stripe', stripeInfo)
|
||||
user.save (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})
|
||||
@checkForExistingSubscription(req, user, customer, done)
|
||||
|
||||
else
|
||||
stripe.customers.retrieve(customerID, (err, customer) =>
|
||||
if err
|
||||
@logSubscriptionError(user, 'Stripe customer creation error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe customer retrieve error. ' + err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
@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
|
||||
|
||||
# SALE LOGIC
|
||||
# overwrite couponID with another for everyone-sales
|
||||
#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
|
||||
# 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})
|
||||
if subscription
|
||||
|
||||
options = { plan: 'basic', trial_end: subscription.current_period_end }
|
||||
options.coupon = couponID if couponID
|
||||
stripe.customers.update user.get('stripe').customerID, options, (err, customer) =>
|
||||
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 customer plan setting error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
|
||||
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
|
||||
# can skip creating the subscription
|
||||
return @updateUser(req, user, customer, false, done)
|
||||
options = { plan: 'basic', metadata: {id: user.id}}
|
||||
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
|
||||
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, subscription, true, done)
|
||||
|
||||
@updateUser(req, user, customer, true, done)
|
||||
|
||||
updateUser: (req, user, customer, increment, done) ->
|
||||
subscription = customer.subscriptions.data[0]
|
||||
updateUser: (req, user, customer, subscription, increment, done) ->
|
||||
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||
stripeInfo.planID = 'basic'
|
||||
stripeInfo.subscriptionID = subscription.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)
|
||||
|
||||
if increment
|
||||
|
@ -126,25 +141,232 @@ class SubscriptionHandler extends Handler
|
|||
|
||||
user.save (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})
|
||||
user?.saveActiveUser 'subscribe'
|
||||
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) ->
|
||||
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) =>
|
||||
if err
|
||||
@logSubscriptionError(user, 'Stripe cancel subscription error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
delete stripeInfo.planID
|
||||
user.set('stripe', stripeInfo)
|
||||
req.body.stripe = stripeInfo
|
||||
user.save (err) =>
|
||||
if err
|
||||
@logSubscriptionError(user, 'User save unsubscribe error. '+err)
|
||||
@logSubscriptionError(user, 'User save unsubscribe error. ' + err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
user?.saveActiveUser 'unsubscribe'
|
||||
return done()
|
||||
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()
|
||||
|
|
|
@ -76,7 +76,7 @@ PurchaseHandler = class PurchaseHandler extends Handler
|
|||
user.set('purchased', purchased)
|
||||
|
||||
#- deduct the gems from the user
|
||||
spent = hadSpent = user.get('spent') ? 0
|
||||
spent = user.get('spent') ? 0
|
||||
spent += item.get('gems')
|
||||
user.set('spent', spent)
|
||||
|
||||
|
|
|
@ -1,59 +1,112 @@
|
|||
async = require 'async'
|
||||
config = require '../../server_config'
|
||||
stripe = require('stripe')(config.stripe.secretKey)
|
||||
User = require '../users/User'
|
||||
Payment = require '../payments/Payment'
|
||||
errors = require '../commons/errors'
|
||||
mongoose = require 'mongoose'
|
||||
utils = require '../../app/core/utils'
|
||||
|
||||
module.exports.setup = (app) ->
|
||||
# Cache customer -> user ID map (increases test perf considerably)
|
||||
customerUserMap = {}
|
||||
|
||||
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
|
||||
stripe.invoices.retrieve invoiceID, (err, invoice) =>
|
||||
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, '')
|
||||
# Subscription renewal events:
|
||||
# https://support.stripe.com/questions/what-events-can-i-see-when-a-subscription-is-renewed
|
||||
|
||||
if req.body.type is 'invoice.payment_succeeded'
|
||||
return handlePaymentSucceeded req, res
|
||||
else if req.body.type is 'customer.subscription.deleted'
|
||||
User.findOne {'stripe.subscriptionID': req.body.data.object.id}, (err, user) ->
|
||||
return res.send(200, '') unless user
|
||||
return handleSubscriptionDeleted req, res
|
||||
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.subscriptionID
|
||||
user.set('stripe', stripeInfo)
|
||||
|
@ -61,14 +114,71 @@ module.exports.setup = (app) ->
|
|||
return res.send(500, '') if err
|
||||
return res.send(200, '')
|
||||
|
||||
else # ignore all other notifications
|
||||
return res.send(200, '')
|
||||
|
||||
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()
|
||||
|
||||
|
||||
checkRecipientSubscription = (done) ->
|
||||
return done() unless subscription.plan.id is 'basic'
|
||||
User.findById subscription.metadata.id, (err, recipient) =>
|
||||
return res.send(500, '') if err
|
||||
return res.send(500, '') unless recipient
|
||||
User.findById recipient.get('stripe').sponsorID, (err, sponsor) =>
|
||||
return res.send(500, '') if err
|
||||
return res.send(500, '') unless sponsor
|
||||
|
||||
# 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 @isAdmin()
|
||||
return false unless stripeObject = @get('stripe')
|
||||
return true if stripeObject.sponsorID
|
||||
return true if stripeObject.subscriptionID
|
||||
return true if stripeObject.free is true
|
||||
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'
|
||||
EarnedAchievement = require '../achievements/EarnedAchievement'
|
||||
UserRemark = require './remarks/UserRemark'
|
||||
{findStripeSubscription} = require '../lib/utils'
|
||||
{isID} = require '../lib/utils'
|
||||
hipchat = require '../hipchat'
|
||||
sendwithus = require '../sendwithus'
|
||||
|
@ -115,22 +116,35 @@ UserHandler = class UserHandler extends Handler
|
|||
|
||||
# Subscription setting
|
||||
(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.body.stripe
|
||||
hasPlan = user.get('stripe')?.planID?
|
||||
wantsPlan = req.body.stripe.planID?
|
||||
|
||||
return callback(null, req, user) if hasPlan is wantsPlan
|
||||
if wantsPlan and not hasPlan
|
||||
finishSubscription = (hasPlan, wantsPlan) ->
|
||||
return callback(null, req, user) if hasPlan is wantsPlan
|
||||
if wantsPlan and not hasPlan
|
||||
SubscriptionHandler.subscribeUser(req, user, (err) ->
|
||||
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) ->
|
||||
return callback(err) if err
|
||||
return callback(null, req, user)
|
||||
)
|
||||
else if hasPlan and not wantsPlan
|
||||
else if req.body.stripe.unsubscribeEmail?
|
||||
SubscriptionHandler.unsubscribeUser(req, user, (err) ->
|
||||
return callback(err) if err
|
||||
return callback(null, req, user)
|
||||
)
|
||||
else
|
||||
wantsPlan = req.body.stripe.planID?
|
||||
hasPlan = user.get('stripe')?.planID?
|
||||
finishSubscription hasPlan, wantsPlan
|
||||
|
||||
# Discount setting
|
||||
(req, user, callback) ->
|
||||
|
@ -257,6 +271,8 @@ UserHandler = class UserHandler extends Handler
|
|||
return @getRemark(req, res, args[0]) if args[1] is 'remark'
|
||||
return @searchForUser(req, res) if args[1] is 'admin_search'
|
||||
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 @sendNotFoundError(res)
|
||||
super(arguments...)
|
||||
|
@ -268,7 +284,67 @@ UserHandler = class UserHandler extends Handler
|
|||
return @sendNotFoundError(res) if not customerID = user.get('stripe')?.customerID
|
||||
stripe.customers.retrieve customerID, (err, customer) =>
|
||||
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) ->
|
||||
# TODO: Should this API be somewhere else?
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
console.log 'IT BEGINS'
|
||||
|
||||
require 'jasmine-spec-reporter'
|
||||
jasmine.getEnv().defaultTimeoutInterval = 300000
|
||||
jasmine.getEnv().reporter.subReporters_ = []
|
||||
jasmine.getEnv().addReporter(new jasmine.SpecReporter({
|
||||
displaySuccessfulSpec: true,
|
||||
|
@ -21,7 +22,6 @@ mongoose.connect('mongodb://localhost/coco_unittest')
|
|||
path = require 'path'
|
||||
GLOBAL.testing = true
|
||||
GLOBAL.tv4 = require 'tv4' # required for TreemaUtils to work
|
||||
# _.str = require 'underscore.string'
|
||||
|
||||
models_path = [
|
||||
'../../server/analytics/AnalyticsUsersActive'
|
||||
|
@ -113,6 +113,25 @@ wrapUpGetUser = (email, user, done) ->
|
|||
GLOBAL.getURL = (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) ->
|
||||
request.post getURL('/auth/logout'), ->
|
||||
unittest.getNormalJoe (user) ->
|
||||
|
@ -147,6 +166,16 @@ GLOBAL.loginAdmin = (done) ->
|
|||
form.append('password', '80yqxpb38j')
|
||||
# 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) ->
|
||||
if mongoose.connection.readyState is 2
|
||||
mongoose.connection.once 'open', ->
|
||||
|
|
|
@ -66,7 +66,7 @@ describe '/db/user, editing stripe.couponID property', ->
|
|||
expect(body.stripe.couponID).toBe('500off')
|
||||
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 {
|
||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||
}, (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) ->
|
||||
joeData = body
|
||||
expect(res.statusCode).toBe(200)
|
||||
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
|
||||
expect(customer.discount).toBeDefined()
|
||||
expect(customer.discount?.coupon.id).toBe('500off')
|
||||
stripe.customers.retrieveSubscription joeData.stripe.customerID, joeData.stripe.subscriptionID, (err, subscription) ->
|
||||
expect(subscription.discount).toBeDefined()
|
||||
expect(subscription.discount?.coupon.id).toBe('500off')
|
||||
done()
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
|
||||
async = require 'async'
|
||||
config = require '../../../server_config'
|
||||
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
|
||||
invoiceChargeSampleEvent = {
|
||||
invoiceChargeSampleEvent = {
|
||||
id: 'evt_155TBeKaReE7xLUdrKM72O5R',
|
||||
created: 1417574898,
|
||||
livemode: false,
|
||||
|
@ -38,13 +40,13 @@ invoiceChargeSampleEvent = {
|
|||
metadata: {},
|
||||
statement_description: null,
|
||||
description: null,
|
||||
receipt_number: null
|
||||
}
|
||||
receipt_number: null
|
||||
}
|
||||
},
|
||||
object: 'event',
|
||||
pending_webhooks: 1,
|
||||
request: 'iar_5Fz9c4BZJyNNsM',
|
||||
api_version: '2014-11-05'
|
||||
api_version: '2015-02-18'
|
||||
}
|
||||
|
||||
customerSubscriptionDeletedSampleEvent = {
|
||||
|
@ -70,13 +72,13 @@ customerSubscriptionDeletedSampleEvent = {
|
|||
quantity: 1,
|
||||
application_fee_percent: null,
|
||||
discount: null,
|
||||
metadata: {}
|
||||
}
|
||||
metadata: {}
|
||||
}
|
||||
},
|
||||
object: 'event',
|
||||
pending_webhooks: 1,
|
||||
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
|
||||
joeData = null
|
||||
firstSubscriptionID = null
|
||||
|
||||
|
||||
it 'returns client error when a token fails to charge', (done) ->
|
||||
stripe.tokens.create {
|
||||
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) ->
|
||||
expect(res.statusCode).toBe(402)
|
||||
done()
|
||||
|
||||
|
||||
it 'creates a subscription when you put a token and plan', (done) ->
|
||||
stripe.tokens.create {
|
||||
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.token).toBeUndefined()
|
||||
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
|
||||
stripe.invoices.list {customer: joeData.stripe.customerID}, (err, invoices) ->
|
||||
expect(invoices.data.length).toBe(1)
|
||||
event = _.cloneDeep(invoiceChargeSampleEvent)
|
||||
event.data.object = invoices.data[0]
|
||||
|
||||
|
||||
request.post {uri: webhookURL, json: event}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(201)
|
||||
Payment.find {}, (err, payments) ->
|
||||
|
@ -154,7 +156,7 @@ describe '/db/user, editing stripe property', ->
|
|||
User.findById joeData._id, (err, user) ->
|
||||
expect(user.get('purchased').gems).toBe(3500)
|
||||
done()
|
||||
|
||||
|
||||
it 'schedules the stripe subscription to be cancelled when stripe.planID is removed from the user', (done) ->
|
||||
delete joeData.stripe.planID
|
||||
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)
|
||||
done()
|
||||
|
||||
|
||||
it 'allows you to sign up again using the same customer ID as before, no token necessary', (done) ->
|
||||
joeData.stripe.planID = 'basic'
|
||||
request.put {uri: userURL, json: joeData, headers: headers }, (err, res, body) ->
|
||||
joeData = body
|
||||
|
||||
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(joeData.stripe.customerID).toBeDefined()
|
||||
expect(joeData.stripe.subscriptionID).toBeDefined()
|
||||
expect(joeData.stripe.subscriptionID).not.toBe(firstSubscriptionID)
|
||||
expect(joeData.stripe.planID).toBe('basic')
|
||||
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) ->
|
||||
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').planID).toBeUndefined()
|
||||
done()
|
||||
|
||||
|
||||
it "updates the customer's email when you change the user's email", (done) ->
|
||||
joeData.email = 'newEmail@gmail.com'
|
||||
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')
|
||||
done()
|
||||
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