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 = {} logStripeWebhookError = (msg) -> console.warn "Stripe Webhook Error: #{msg}" app.post '/stripe/webhook', (req, res) -> # 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' return handleSubscriptionDeleted req, res 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() 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 checkUserExists = (done) -> stripe.customers.retrieve subscription.customer, (err, customer) => if err logStripeWebhookError("Failed to retrieve #{subscription.customer}") return res.send(500, '') unless customer?.metadata?.id logStripeWebhookError("Customer with no metadata.id #{subscription.customer}") return res.send(500, '') User.findById customer.metadata.id, (err, user) => if err logStripeWebhookError(err) return res.send(500, '') unless user logStripeWebhookError("User not found #{customer.metadata.id}") return res.send(500, '') return res.send(200, '') if user.get('deleted') is true done() checkNormalSubscription = (done) -> User.findOne {'stripe.subscriptionID': subscription.id}, (err, user) -> return done() unless user stripeInfo = _.cloneDeep(user.get('stripe') ? {}) delete stripeInfo.planID delete stripeInfo.prepaidCode delete stripeInfo.subscriptionID user.set('stripe', stripeInfo) user.save (err) => if err logStripeWebhookError(err) return res.send(500, '') return res.send(200, '') checkRecipientSubscription = (done) -> return done() unless subscription.plan.id is 'basic' return done() unless subscription.metadata?.id # Shouldn't be possible User.findById subscription.metadata.id, (err, recipient) => if err logStripeWebhookError(err) return res.send(500, '') unless recipient logStripeWebhookError("Recipient not found #{subscription.metadata.id}") return res.send(500, '') User.findById recipient.get('stripe').sponsorID, (err, sponsor) => if err logStripeWebhookError(err) return res.send(500, '') unless sponsor logStripeWebhookError("Sponsor not found #{recipient.get('stripe').sponsorID}") return res.send(500, '') # Update sponsor subscription stripeInfo = _.cloneDeep(sponsor.get('stripe') ? {}) _.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id) options = quantity: utils.getSponsoredSubsAmount(subscription.plan.amount, stripeInfo.recipients.length, stripeInfo.subscriptionID?) unless stripeInfo.sponsorSubscriptionID # TODO: fix #2786 error for a particular customer which doesn't have this console.error "Couldn't find sponsorSubscriptionID from stripeInfo", stripeInfo, 'for customer', stripeInfo.customerID, 'with options', options, 'and subscription', subscription, 'for user', recipient.id, 'with sponsor', sponsor.id return res.send(500, '') stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) => if err logStripeWebhookError(err) return res.send(500, '') # Update sponsor user sponsor.set 'stripe', stripeInfo sponsor.save (err) => if err logStripeWebhookError(err) return res.send(500, '') # Update recipient user stripeInfo = recipient.get('stripe') delete stripeInfo.sponsorID if _.isEmpty stripeInfo recipient.set 'stripe', undefined else recipient.set 'stripe', stripeInfo recipient.save (err) => if err logStripeWebhookError(err) return res.send(500, '') 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) => if err logStripeWebhookError(err) return res.send(500, '') # 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) => if err logStripeWebhookError(err) return res.send(500, '') done() # TODO: use async.series for this checkUserExists -> checkNormalSubscription -> checkRecipientSubscription -> checkSponsorSubscription -> res.send(200, '')