| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- querystring = require 'querystring'
- crypto = require 'crypto'
- request = require 'request'
- Settings = require "settings-sharelatex"
- xml2js = require "xml2js"
- logger = require("logger-sharelatex")
- Async = require('async')
- module.exports = RecurlyWrapper =
- apiUrl : "https://api.recurly.com/v2"
- _addressToXml: (address) ->
- allowedKeys = ['address1', 'address2', 'city', 'country', 'state', 'zip', 'postal_code']
- resultString = "<billing_info>\n"
- for k, v of address
- if k == 'postal_code'
- k = 'zip'
- if v and (k in allowedKeys)
- resultString += "<#{k}#{if k == 'address2' then ' nil="nil"' else ''}>#{v || ''}</#{k}>\n"
- resultString += "</billing_info>\n"
- return resultString
- _paypal:
- checkAccountExists: (cache, next) ->
- user = cache.user
- recurly_token_id = cache.recurly_token_id
- subscriptionDetails = cache.subscriptionDetails
- logger.log {user_id: user._id, recurly_token_id}, "checking if recurly account exists for user"
- RecurlyWrapper.apiRequest({
- url: "accounts/#{user._id}"
- method: "GET"
- expect404: true
- }, (error, response, responseBody) ->
- if error
- logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
- return next(error)
- if response.statusCode == 404 # actually not an error in this case, just no existing account
- logger.log {user_id: user._id, recurly_token_id}, "user does not currently exist in recurly, proceed"
- cache.userExists = false
- return next(null, cache)
- logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
- RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
- if err
- logger.error {err, user_id: user._id, recurly_token_id}, "error parsing account"
- return next(err)
- cache.userExists = true
- cache.account = account
- return next(null, cache)
- )
- createAccount: (cache, next) ->
- user = cache.user
- recurly_token_id = cache.recurly_token_id
- subscriptionDetails = cache.subscriptionDetails
- address = subscriptionDetails.address
- if !address
- return next(new Error('no address in subscriptionDetails at createAccount stage'))
- if cache.userExists
- logger.log {user_id: user._id, recurly_token_id}, "user already exists in recurly"
- return next(null, cache)
- logger.log {user_id: user._id, recurly_token_id}, "creating user in recurly"
- requestBody = """
- <account>
- <account_code>#{user._id}</account_code>
- <email>#{user.email}</email>
- <first_name>#{user.first_name}</first_name>
- <last_name>#{user.last_name}</last_name>
- <address>
- <address1>#{address.address1}</address1>
- <address2>#{address.address2}</address2>
- <city>#{address.city || ''}</city>
- <state>#{address.state || ''}</state>
- <zip>#{address.zip || ''}</zip>
- <country>#{address.country}</country>
- </address>
- </account>
- """
- RecurlyWrapper.apiRequest({
- url : "accounts"
- method : "POST"
- body : requestBody
- }, (error, response, responseBody) =>
- if error
- logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating account"
- return next(error)
- RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
- if err
- logger.error {err, user_id: user._id, recurly_token_id}, "error creating account"
- return next(err)
- cache.account = account
- return next(null, cache)
- )
- createBillingInfo: (cache, next) ->
- user = cache.user
- recurly_token_id = cache.recurly_token_id
- subscriptionDetails = cache.subscriptionDetails
- logger.log {user_id: user._id, recurly_token_id}, "creating billing info in recurly"
- accountCode = cache?.account?.account_code
- if !accountCode
- return next(new Error('no account code at createBillingInfo stage'))
- requestBody = """
- <billing_info>
- <token_id>#{recurly_token_id}</token_id>
- </billing_info>
- """
- RecurlyWrapper.apiRequest({
- url: "accounts/#{accountCode}/billing_info"
- method: "POST"
- body: requestBody
- }, (error, response, responseBody) =>
- if error
- logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating billing info"
- return next(error)
- RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
- if err
- logger.error {err, user_id: user._id, accountCode, recurly_token_id}, "error creating billing info"
- return next(err)
- cache.billingInfo = billingInfo
- return next(null, cache)
- )
- setAddress: (cache, next) ->
- user = cache.user
- recurly_token_id = cache.recurly_token_id
- subscriptionDetails = cache.subscriptionDetails
- logger.log {user_id: user._id, recurly_token_id}, "setting billing address in recurly"
- accountCode = cache?.account?.account_code
- if !accountCode
- return next(new Error('no account code at setAddress stage'))
- address = subscriptionDetails.address
- if !address
- return next(new Error('no address in subscriptionDetails at setAddress stage'))
- requestBody = RecurlyWrapper._addressToXml(address)
- RecurlyWrapper.apiRequest({
- url: "accounts/#{accountCode}/billing_info"
- method: "PUT"
- body: requestBody
- }, (error, response, responseBody) =>
- if error
- logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while setting address"
- return next(error)
- RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
- if err
- logger.error {err, user_id: user._id, recurly_token_id}, "error updating billing info"
- return next(err)
- cache.billingInfo = billingInfo
- return next(null, cache)
- )
- createSubscription: (cache, next) ->
- user = cache.user
- recurly_token_id = cache.recurly_token_id
- subscriptionDetails = cache.subscriptionDetails
- logger.log {user_id: user._id, recurly_token_id}, "creating subscription in recurly"
- requestBody = """
- <subscription>
- <plan_code>#{subscriptionDetails.plan_code}</plan_code>
- <currency>#{subscriptionDetails.currencyCode}</currency>
- <coupon_code>#{subscriptionDetails.coupon_code}</coupon_code>
- <account>
- <account_code>#{user._id}</account_code>
- </account>
- </subscription>
- """ # TODO: check account details and billing
- RecurlyWrapper.apiRequest({
- url : "subscriptions"
- method : "POST"
- body : requestBody
- }, (error, response, responseBody) =>
- if error
- logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating subscription"
- return next(error)
- RecurlyWrapper._parseSubscriptionXml responseBody, (err, subscription) ->
- if err
- logger.error {err, user_id: user._id, recurly_token_id}, "error creating subscription"
- return next(err)
- cache.subscription = subscription
- return next(null, cache)
- )
- _createPaypalSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
- logger.log {user_id: user._id, recurly_token_id}, "starting process of creating paypal subscription"
- # We use `async.waterfall` to run each of these actions in sequence
- # passing a `cache` object along the way. The cache is initialized
- # with required data, and `async.apply` to pass the cache to the first function
- cache = {user, recurly_token_id, subscriptionDetails}
- Async.waterfall([
- Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache),
- RecurlyWrapper._paypal.createAccount,
- RecurlyWrapper._paypal.createBillingInfo,
- RecurlyWrapper._paypal.setAddress,
- RecurlyWrapper._paypal.createSubscription,
- ], (err, result) ->
- if err
- logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
- return callback(err)
- if !result.subscription
- err = new Error('no subscription object in result')
- logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
- return callback(err)
- logger.log {user_id: user._id, recurly_token_id}, "done creating paypal subscription for user"
- callback(null, result.subscription)
- )
- _createCreditCardSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
- requestBody = """
- <subscription>
- <plan_code>#{subscriptionDetails.plan_code}</plan_code>
- <currency>#{subscriptionDetails.currencyCode}</currency>
- <coupon_code>#{subscriptionDetails.coupon_code}</coupon_code>
- <account>
- <account_code>#{user._id}</account_code>
- <email>#{user.email}</email>
- <first_name>#{user.first_name}</first_name>
- <last_name>#{user.last_name}</last_name>
- <billing_info>
- <token_id>#{recurly_token_id}</token_id>
- </billing_info>
- </account>
- </subscription>
- """
- RecurlyWrapper.apiRequest({
- url : "subscriptions"
- method : "POST"
- body : requestBody
- }, (error, response, responseBody) =>
- return callback(error) if error?
- RecurlyWrapper._parseSubscriptionXml responseBody, callback
- )
- createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
- isPaypal = subscriptionDetails.isPaypal
- logger.log {user_id: user._id, isPaypal, recurly_token_id}, "setting up subscription in recurly"
- fn = if isPaypal then RecurlyWrapper._createPaypalSubscription else RecurlyWrapper._createCreditCardSubscription
- fn user, subscriptionDetails, recurly_token_id, callback
- apiRequest : (options, callback) ->
- options.url = RecurlyWrapper.apiUrl + "/" + options.url
- options.headers =
- "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
- "Accept" : "application/xml"
- "Content-Type" : "application/xml; charset=utf-8"
- expect404 = options.expect404
- delete options.expect404
- request options, (error, response, body) ->
- unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 or (response.statusCode == 404 and expect404)
- logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly"
- error = "Recurly API returned with status code: #{response.statusCode}"
- if response.statusCode == 404 and expect404
- logger.log {url: options.url, method: options.method}, "got 404 response from recurly, expected as valid response"
- callback(error, response, body)
- sign : (parameters, callback) ->
- nestAttributesForQueryString = (attributes, base) ->
- newAttributes = {}
- for key, value of attributes
- if base?
- newKey = "#{base}[#{key}]"
- else
- newKey = key
- if typeof value == "object"
- for key, value of nestAttributesForQueryString(value, newKey)
- newAttributes[key] = value
- else
- newAttributes[newKey] = value
- return newAttributes
- crypto.randomBytes 32, (error, buffer) ->
- return callback error if error?
- parameters.nonce = buffer.toString "base64"
- parameters.timestamp = Math.round((new Date()).getTime() / 1000)
- unsignedQuery = querystring.stringify nestAttributesForQueryString(parameters)
- signed = crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(unsignedQuery).digest("hex")
- signature = "#{signed}|#{unsignedQuery}"
- callback null, signature
- getSubscriptions: (accountId, callback)->
- RecurlyWrapper.apiRequest({
- url: "accounts/#{accountId}/subscriptions"
- }, (error, response, body) =>
- return callback(error) if error?
- RecurlyWrapper._parseXml body, callback
- )
- getSubscription: (subscriptionId, options, callback) ->
- callback = options unless callback?
- options ||= {}
- if options.recurlyJsResult
- url = "recurly_js/result/#{subscriptionId}"
- else
- url = "subscriptions/#{subscriptionId}"
- RecurlyWrapper.apiRequest({
- url: url
- }, (error, response, body) =>
- return callback(error) if error?
- RecurlyWrapper._parseSubscriptionXml body, (error, recurlySubscription) =>
- return callback(error) if error?
- if options.includeAccount
- if recurlySubscription.account? and recurlySubscription.account.url?
- accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
- else
- return callback "I don't understand the response from Recurly"
- RecurlyWrapper.getAccount accountId, (error, account) ->
- return callback(error) if error?
- recurlySubscription.account = account
- callback null, recurlySubscription
- else
- callback null, recurlySubscription
- )
- getAccounts: (callback)->
- allAccounts = []
- getPageOfAccounts = (cursor = null)=>
- opts =
- url: "accounts"
- qs:
- per_page:200
- if cursor?
- opts.qs.cursor = cursor
- RecurlyWrapper.apiRequest opts, (error, response, body) =>
- return callback(error) if error?
- RecurlyWrapper._parseXml body, (err, data)->
- if err?
- logger.err err:err, "could not get accoutns"
- callback(err)
- allAccounts = allAccounts.concat(data.accounts)
- logger.log "got another #{data.accounts.length}, total now #{allAccounts.length}"
- cursor = response.headers.link?.match(/cursor=([0-9]+)&/)?[1]
- if cursor?
- getPageOfAccounts(cursor)
- else
- callback(err, allAccounts)
- getPageOfAccounts()
- getAccount: (accountId, callback) ->
- RecurlyWrapper.apiRequest({
- url: "accounts/#{accountId}"
- }, (error, response, body) =>
- return callback(error) if error?
- RecurlyWrapper._parseAccountXml body, callback
- )
- getBillingInfo: (accountId, callback)->
- RecurlyWrapper.apiRequest({
- url: "accounts/#{accountId}/billing_info"
- }, (error, response, body) =>
- return callback(error) if error?
- RecurlyWrapper._parseXml body, callback
- )
- updateSubscription: (subscriptionId, options, callback) ->
- logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription"
- requestBody = """
- <subscription>
- <plan_code>#{options.plan_code}</plan_code>
- <timeframe>#{options.timeframe}</timeframe>
- </subscription>
- """
- RecurlyWrapper.apiRequest({
- url : "subscriptions/#{subscriptionId}"
- method : "put"
- body : requestBody
- }, (error, response, responseBody) =>
- return callback(error) if error?
- RecurlyWrapper._parseSubscriptionXml responseBody, callback
- )
- createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)->
- requestBody = """
- <coupon>
- <coupon_code>#{coupon_code}</coupon_code>
- <name>#{name}</name>
- <discount_type>dollars</discount_type>
- <discount_in_cents>
- <#{currencyCode}>#{discount_in_cents}</#{currencyCode}>
- </discount_in_cents>
- <plan_codes>
- <plan_code>#{plan_code}</plan_code>
- </plan_codes>
- <applies_to_all_plans>false</applies_to_all_plans>
- </coupon>
- """
- logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
- RecurlyWrapper.apiRequest({
- url : "coupons"
- method : "post"
- body : requestBody
- }, (error, response, responseBody) =>
- if error?
- logger.err err:error, coupon_code:coupon_code, "error creating coupon"
- callback(error)
- )
- lookupCoupon: (coupon_code, callback)->
- RecurlyWrapper.apiRequest({
- url: "coupons/#{coupon_code}"
- }, (error, response, body) =>
- return callback(error) if error?
- RecurlyWrapper._parseXml body, callback
- )
- cancelSubscription: (subscriptionId, callback) ->
- logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
- RecurlyWrapper.apiRequest({
- url: "subscriptions/#{subscriptionId}/cancel",
- method: "put"
- }, (error, response, body) ->
- if error?
- RecurlyWrapper._parseXml body, (_err, parsed) ->
- if parsed?.error?.description == "A canceled subscription can't transition to canceled"
- logger.log {subscriptionId, error, body}, "subscription already cancelled, not really an error, proceeding"
- callback(null)
- else
- callback(error)
- else
- callback(null)
- )
- reactivateSubscription: (subscriptionId, callback) ->
- logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
- RecurlyWrapper.apiRequest({
- url: "subscriptions/#{subscriptionId}/reactivate",
- method: "put"
- }, (error, response, body) ->
- callback(error)
- )
- redeemCoupon: (account_code, coupon_code, callback)->
- requestBody = """
- <redemption>
- <account_code>#{account_code}</account_code>
- <currency>USD</currency>
- </redemption>
- """
- logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
- RecurlyWrapper.apiRequest({
- url : "coupons/#{coupon_code}/redeem"
- method : "post"
- body : requestBody
- }, (error, response, responseBody) =>
- if error?
- logger.err err:error, account_code:account_code, coupon_code:coupon_code, "error redeeming coupon"
- callback(error)
- )
- extendTrial: (subscriptionId, daysUntilExpire = 7, callback)->
- next_renewal_date = new Date()
- next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
- logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user"
- RecurlyWrapper.apiRequest({
- url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
- method : "put"
- }, (error, response, responseBody) =>
- if error?
- logger.err err:error, subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "error exending trial"
- callback(error)
- )
-
- listAccountActiveSubscriptions: (account_id, callback = (error, subscriptions) ->) ->
- RecurlyWrapper.apiRequest {
- url: "accounts/#{account_id}/subscriptions"
- qs:
- state: "active"
- expect404: true
- }, (error, response, body) ->
- return callback(error) if error?
- if response.statusCode == 404
- return callback null, []
- else
- RecurlyWrapper._parseSubscriptionsXml body, callback
- _parseSubscriptionsXml: (xml, callback) ->
- RecurlyWrapper._parseXmlAndGetAttribute xml, "subscriptions", callback
- _parseSubscriptionXml: (xml, callback) ->
- RecurlyWrapper._parseXmlAndGetAttribute xml, "subscription", callback
- _parseAccountXml: (xml, callback) ->
- RecurlyWrapper._parseXmlAndGetAttribute xml, "account", callback
- _parseBillingInfoXml: (xml, callback) ->
- RecurlyWrapper._parseXmlAndGetAttribute xml, "billing_info", callback
- _parseXmlAndGetAttribute: (xml, attribute, callback) ->
- RecurlyWrapper._parseXml xml, (error, data) ->
- return callback(error) if error?
- if data? and data[attribute]?
- return callback null, data[attribute]
- else
- return callback(new Error("I don't understand the response from Recurly"))
- _parseXml: (xml, callback) ->
- convertDataTypes = (data) ->
- if data? and data["$"]?
- if data["$"]["nil"] == "nil"
- data = null
- else if data["$"].href?
- data.url = data["$"].href
- delete data["$"]
- else if data["$"]["type"] == "integer"
- data = parseInt(data["_"], 10)
- else if data["$"]["type"] == "datetime"
- data = new Date(data["_"])
- else if data["$"]["type"] == "array"
- delete data["$"]
- array = []
- for key, value of data
- if value instanceof Array
- array = array.concat(convertDataTypes(value))
- else
- array.push(convertDataTypes(value))
- data = array
- if data instanceof Array
- data = (convertDataTypes(entry) for entry in data)
- else if typeof data == "object"
- for key, value of data
- data[key] = convertDataTypes(value)
- return data
- parser = new xml2js.Parser(
- explicitRoot : true
- explicitArray : false
- )
- parser.parseString xml, (error, data) ->
- return callback(error) if error?
- result = convertDataTypes(data)
- callback null, result
|