Merge branch 'sk-subscription-address'

This commit is contained in:
Shane Kilkelly 2016-06-28 14:16:34 +01:00
commit 9e16f250f9
3 changed files with 1009 additions and 114 deletions

View file

@ -4,11 +4,201 @@ 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"
createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
_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"
}, (error, response, responseBody) ->
if error
if response.statusCode == 404 # actually not an error in this case, just no existing account
cache.userExists = false
return next(null, cache)
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
return next(error)
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>
@ -25,17 +215,23 @@ module.exports = RecurlyWrapper =
</account>
</subscription>
"""
@apiRequest({
RecurlyWrapper.apiRequest({
url : "subscriptions"
method : "POST"
body : requestBody
}, (error, response, responseBody) =>
return callback(error) if error?
@_parseSubscriptionXml responseBody, callback
)
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 = @apiUrl + "/" + options.url
options.url = RecurlyWrapper.apiUrl + "/" + options.url
options.headers =
"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
"Accept" : "application/xml"
@ -60,7 +256,7 @@ module.exports = RecurlyWrapper =
newAttributes[key] = value
else
newAttributes[newKey] = value
return newAttributes
crypto.randomBytes 32, (error, buffer) ->
@ -74,14 +270,14 @@ module.exports = RecurlyWrapper =
signature = "#{signed}|#{unsignedQuery}"
callback null, signature
getSubscriptions: (accountId, callback)->
@apiRequest({
RecurlyWrapper.apiRequest({
url: "accounts/#{accountId}/subscriptions"
}, (error, response, body) =>
return callback(error) if error?
@_parseXml body, callback
RecurlyWrapper._parseXml body, callback
)
@ -94,11 +290,11 @@ module.exports = RecurlyWrapper =
else
url = "subscriptions/#{subscriptionId}"
@apiRequest({
RecurlyWrapper.apiRequest({
url: url
}, (error, response, body) =>
return callback(error) if error?
@_parseSubscriptionXml body, (error, recurlySubscription) =>
RecurlyWrapper._parseSubscriptionXml body, (error, recurlySubscription) =>
return callback(error) if error?
if options.includeAccount
if recurlySubscription.account? and recurlySubscription.account.url?
@ -106,7 +302,7 @@ module.exports = RecurlyWrapper =
else
return callback "I don't understand the response from Recurly"
@getAccount accountId, (error, account) ->
RecurlyWrapper.getAccount accountId, (error, account) ->
return callback(error) if error?
recurlySubscription.account = account
callback null, recurlySubscription
@ -124,9 +320,9 @@ module.exports = RecurlyWrapper =
per_page:200
if cursor?
opts.qs.cursor = cursor
@apiRequest opts, (error, response, body) =>
RecurlyWrapper.apiRequest opts, (error, response, body) =>
return callback(error) if error?
@_parseXml body, (err, data)->
RecurlyWrapper._parseXml body, (err, data)->
if err?
logger.err err:err, "could not get accoutns"
callback(err)
@ -142,19 +338,19 @@ module.exports = RecurlyWrapper =
getAccount: (accountId, callback) ->
@apiRequest({
RecurlyWrapper.apiRequest({
url: "accounts/#{accountId}"
}, (error, response, body) =>
return callback(error) if error?
@_parseAccountXml body, callback
RecurlyWrapper._parseAccountXml body, callback
)
getBillingInfo: (accountId, callback)->
@apiRequest({
RecurlyWrapper.apiRequest({
url: "accounts/#{accountId}/billing_info"
}, (error, response, body) =>
return callback(error) if error?
@_parseXml body, callback
RecurlyWrapper._parseXml body, callback
)
@ -166,13 +362,13 @@ module.exports = RecurlyWrapper =
<timeframe>#{options.timeframe}</timeframe>
</subscription>
"""
@apiRequest({
RecurlyWrapper.apiRequest({
url : "subscriptions/#{subscriptionId}"
method : "put"
body : requestBody
}, (error, response, responseBody) =>
return callback(error) if error?
@_parseSubscriptionXml responseBody, callback
RecurlyWrapper._parseSubscriptionXml responseBody, callback
)
createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)->
@ -191,7 +387,7 @@ module.exports = RecurlyWrapper =
</coupon>
"""
logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
@apiRequest({
RecurlyWrapper.apiRequest({
url : "coupons"
method : "post"
body : requestBody
@ -203,16 +399,16 @@ module.exports = RecurlyWrapper =
lookupCoupon: (coupon_code, callback)->
@apiRequest({
RecurlyWrapper.apiRequest({
url: "coupons/#{coupon_code}"
}, (error, response, body) =>
return callback(error) if error?
@_parseXml body, callback
RecurlyWrapper._parseXml body, callback
)
cancelSubscription: (subscriptionId, callback) ->
logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
@apiRequest({
RecurlyWrapper.apiRequest({
url: "subscriptions/#{subscriptionId}/cancel",
method: "put"
}, (error, response, body) ->
@ -221,13 +417,13 @@ module.exports = RecurlyWrapper =
reactivateSubscription: (subscriptionId, callback) ->
logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
@apiRequest({
RecurlyWrapper.apiRequest({
url: "subscriptions/#{subscriptionId}/reactivate",
method: "put"
}, (error, response, body) ->
callback(error)
)
redeemCoupon: (account_code, coupon_code, callback)->
requestBody = """
@ -237,7 +433,7 @@ module.exports = RecurlyWrapper =
</redemption>
"""
logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
@apiRequest({
RecurlyWrapper.apiRequest({
url : "coupons/#{coupon_code}/redeem"
method : "post"
body : requestBody
@ -251,7 +447,7 @@ module.exports = RecurlyWrapper =
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"
@apiRequest({
RecurlyWrapper.apiRequest({
url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
method : "put"
}, (error, response, responseBody) =>
@ -261,7 +457,7 @@ module.exports = RecurlyWrapper =
)
_parseSubscriptionXml: (xml, callback) ->
@_parseXml xml, (error, data) ->
RecurlyWrapper._parseXml xml, (error, data) ->
return callback(error) if error?
if data? and data.subscription?
recurlySubscription = data.subscription
@ -270,7 +466,7 @@ module.exports = RecurlyWrapper =
callback null, recurlySubscription
_parseAccountXml: (xml, callback) ->
@_parseXml xml, (error, data) ->
RecurlyWrapper._parseXml xml, (error, data) ->
return callback(error) if error?
if data? and data.account?
account = data.account
@ -278,6 +474,15 @@ module.exports = RecurlyWrapper =
return callback "I don't understand the response from Recurly"
callback null, account
_parseBillingInfoXml: (xml, callback) ->
RecurlyWrapper._parseXml xml, (error, data) ->
return callback(error) if error?
if data? and data.billing_info?
billingInfo = data.billing_info
else
return callback "I don't understand the response from Recurly"
callback null, billingInfo
_parseXml: (xml, callback) ->
convertDataTypes = (data) ->
if data? and data["$"]?
@ -299,7 +504,7 @@ module.exports = RecurlyWrapper =
else
array.push(convertDataTypes(value))
data = array
if data instanceof Array
data = (convertDataTypes(entry) for entry in data)
else if typeof data == "object"
@ -315,6 +520,3 @@ module.exports = RecurlyWrapper =
return callback(error) if error?
result = convertDataTypes(data)
callback null, result

View file

@ -115,6 +115,13 @@ define [
currencyCode:pricing.items.currency
plan_code:pricing.items.plan.code
coupon_code:pricing.items?.coupon?.code || ""
isPaypal: $scope.paymentMethod == 'paypal'
address:
address1: $scope.data.address1
address2: $scope.data.address2
country: $scope.data.country
state: $scope.data.state
postal_code: $scope.data.postal_code
$http.post("/user/subscription/create", postData)
.success (data, status, headers)->
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->

View file

@ -1,9 +1,10 @@
should = require('chai').should()
expect = require('chai').expect
sinon = require 'sinon'
crypto = require 'crypto'
querystring = require 'querystring'
RecurlyWrapper = require "../../../../app/js/Features/Subscription/RecurlyWrapper"
Settings = require "settings-sharelatex"
modulePath = "../../../../app/js/Features/Subscription/RecurlyWrapper"
SandboxedModule = require('sandboxed-module')
tk = require("timekeeper")
fixtures =
@ -97,22 +98,37 @@ mockApiRequest = (options, callback) ->
describe "RecurlyWrapper", ->
beforeEach ->
Settings.plans = [{
planCode: "collaborator"
name: "Collaborator"
features:
collaborators: -1
versioning: true
}]
Settings.defaultPlanCode =
collaborators: 0
versioning: false
before ->
@settings =
plans: [{
planCode: "collaborator"
name: "Collaborator"
features:
collaborators: -1
versioning: true
}]
defaultPlanCode:
collaborators: 0
versioning: false
apis:
recurly:
apiKey: 'nonsense'
privateKey: 'private_nonsense'
@RecurlyWrapper = RecurlyWrapper = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings
"logger-sharelatex":
err: sinon.stub()
error: sinon.stub()
log: sinon.stub()
"request": sinon.stub()
describe "sign", ->
before (done) ->
tk.freeze Date.now() # freeze the time for these tests
RecurlyWrapper.sign({
@RecurlyWrapper.sign({
subscription :
plan_code : "gold"
name : "$$$"
@ -127,7 +143,7 @@ describe "RecurlyWrapper", ->
it "should be signed correctly", ->
signed = @signature.split("|")[0]
query = @signature.split("|")[1]
crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(query).digest("hex").should.equal signed
crypto.createHmac("sha1", @settings.apis.recurly.privateKey).update(query).digest("hex").should.equal signed
it "should be url escaped", ->
query = @signature.split("|")[1]
@ -149,38 +165,39 @@ describe "RecurlyWrapper", ->
describe "_parseXml", ->
it "should convert different data types into correct representations", (done) ->
xml =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<subscription href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96\">" +
" <account href=\"https://api.recurly.com/v2/accounts/1\"/>" +
" <plan href=\"https://api.recurly.com/v2/plans/gold\">" +
" <plan_code>gold</plan_code>" +
" <name>Gold plan</name>" +
" </plan>" +
" <uuid>44f83d7cba354d5b84812419f923ea96</uuid>" +
" <state>active</state>" +
" <unit_amount_in_cents type=\"integer\">800</unit_amount_in_cents>" +
" <currency>EUR</currency>" +
" <quantity type=\"integer\">1</quantity>" +
" <activated_at type=\"datetime\">2011-05-27T07:00:00Z</activated_at>" +
" <canceled_at nil=\"nil\"></canceled_at>" +
" <expires_at nil=\"nil\"></expires_at>" +
" <current_period_started_at type=\"datetime\">2011-06-27T07:00:00Z</current_period_started_at>" +
" <current_period_ends_at type=\"datetime\">2011-07-27T07:00:00Z</current_period_ends_at>" +
" <trial_started_at nil=\"nil\"></trial_started_at>" +
" <trial_ends_at nil=\"nil\"></trial_ends_at>" +
" <subscription_add_ons type=\"array\">" +
" <subscription_add_on>" +
" <add_on_code>ipaddresses</add_on_code>" +
" <quantity>10</quantity>" +
" <unit_amount_in_cents>150</unit_amount_in_cents>" +
" </subscription_add_on>" +
" </subscription_add_ons>" +
" <a name=\"cancel\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel\" method=\"put\"/>" +
" <a name=\"terminate\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate\" method=\"put\"/>" +
" <a name=\"postpone\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone\" method=\"put\"/>" +
"</subscription>"
RecurlyWrapper._parseXml xml, (error, data) ->
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">
<account href="https://api.recurly.com/v2/accounts/1"/>
<plan href="https://api.recurly.com/v2/plans/gold">
<plan_code>gold</plan_code>
<name>Gold plan</name>
</plan>
<uuid>44f83d7cba354d5b84812419f923ea96</uuid>
<state>active</state>
<unit_amount_in_cents type="integer">800</unit_amount_in_cents>
<currency>EUR</currency>
<quantity type="integer">1</quantity>
<activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>
<canceled_at nil="nil"></canceled_at>
<expires_at nil="nil"></expires_at>
<current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>
<current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>
<trial_started_at nil="nil"></trial_started_at>
<trial_ends_at nil="nil"></trial_ends_at>
<subscription_add_ons type="array">
<subscription_add_on>
<add_on_code>ipaddresses</add_on_code>
<quantity>10</quantity>
<unit_amount_in_cents>150</unit_amount_in_cents>
</subscription_add_on>
</subscription_add_ons>
<a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>
<a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>
<a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>
</subscription>
"""
@RecurlyWrapper._parseXml xml, (error, data) ->
data.subscription.plan.plan_code.should.equal "gold"
data.subscription.plan.name.should.equal "Gold plan"
data.subscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
@ -188,32 +205,37 @@ describe "RecurlyWrapper", ->
data.subscription.unit_amount_in_cents.should.equal 800
data.subscription.currency.should.equal "EUR"
data.subscription.quantity.should.equal 1
data.subscription.activated_at.should.deep.equal new Date("2011-05-27T07:00:00Z")
should.equal data.subscription.canceled_at, null
should.equal data.subscription.expires_at, null
data.subscription.current_period_started_at.should.deep.equal new Date("2011-06-27T07:00:00Z")
data.subscription.current_period_ends_at.should.deep.equal new Date("2011-07-27T07:00:00Z")
should.equal data.subscription.trial_started_at, null
should.equal data.subscription.trial_ends_at, null
data.subscription.subscription_add_ons.should.deep.equal [{
data.subscription.subscription_add_ons[0].should.deep.equal {
add_on_code: "ipaddresses"
quantity: "10"
unit_amount_in_cents: "150"
}]
}
data.subscription.account.url.should.equal "https://api.recurly.com/v2/accounts/1"
data.subscription.url.should.equal "https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96"
data.subscription.plan.url.should.equal "https://api.recurly.com/v2/plans/gold"
done()
describe "getSubscription", ->
describe "with proper subscription id", ->
before ->
@apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", (error, recurlySubscription) =>
@apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
@RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", (error, recurlySubscription) =>
@recurlySubscription = recurlySubscription
after ->
RecurlyWrapper.apiRequest.restore()
@RecurlyWrapper.apiRequest.restore()
it "should look up the subscription at the normal API end point", ->
@apiRequest.args[0][0].url.should.equal "subscriptions/44f83d7cba354d5b84812419f923ea96"
@ -222,12 +244,12 @@ describe "RecurlyWrapper", ->
describe "with ReculyJS token", ->
before ->
@apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
RecurlyWrapper.getSubscription "70db44b10f5f4b238669480c9903f6f5", {recurlyJsResult: true}, (error, recurlySubscription) =>
@apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
@RecurlyWrapper.getSubscription "70db44b10f5f4b238669480c9903f6f5", {recurlyJsResult: true}, (error, recurlySubscription) =>
@recurlySubscription = recurlySubscription
after ->
RecurlyWrapper.apiRequest.restore()
@RecurlyWrapper.apiRequest.restore()
it "should return the subscription", ->
@recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
@ -236,30 +258,30 @@ describe "RecurlyWrapper", ->
describe "with includeAccount", ->
beforeEach ->
@apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", {includeAccount: true}, (error, recurlySubscription) =>
@apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
@RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", {includeAccount: true}, (error, recurlySubscription) =>
@recurlySubscription = recurlySubscription
afterEach ->
RecurlyWrapper.apiRequest.restore()
@RecurlyWrapper.apiRequest.restore()
it "should request the account from the API", ->
@apiRequest.args[1][0].url.should.equal "accounts/104"
it "should populate the account attribute", ->
@recurlySubscription.account.account_code.should.equal "104"
describe "updateSubscription", ->
beforeEach (done) ->
@recurlySubscriptionId = "subscription-id-123"
@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
@requestOptions = options
callback null, {}, fixtures["subscriptions/44f83d7cba354d5b84812419f923ea96"]
RecurlyWrapper.updateSubscription @recurlySubscriptionId, { plan_code : "silver", timeframe: "now" }, (error, recurlySubscription) =>
@RecurlyWrapper.updateSubscription @recurlySubscriptionId, { plan_code : "silver", timeframe: "now" }, (error, recurlySubscription) =>
@recurlySubscription = recurlySubscription
done()
afterEach ->
RecurlyWrapper.apiRequest.restore()
@RecurlyWrapper.apiRequest.restore()
it "should send an update request to the API", ->
@apiRequest.called.should.equal true
@ -275,59 +297,723 @@ describe "RecurlyWrapper", ->
it "should return the updated subscription", ->
should.exist @recurlySubscription
@recurlySubscription.plan.plan_code.should.equal "gold"
describe "cancelSubscription", ->
beforeEach (done) ->
@recurlySubscriptionId = "subscription-id-123"
@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/cancel"
options.method.should.equal "put"
callback()
RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
@RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
afterEach ->
RecurlyWrapper.apiRequest.restore()
@RecurlyWrapper.apiRequest.restore()
it "should send a cancel request to the API", ->
@apiRequest.called.should.equal true
describe "reactivateSubscription", ->
beforeEach (done) ->
@recurlySubscriptionId = "subscription-id-123"
@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/reactivate"
options.method.should.equal "put"
callback()
RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
@RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
afterEach ->
RecurlyWrapper.apiRequest.restore()
@RecurlyWrapper.apiRequest.restore()
it "should send a cancel request to the API", ->
@apiRequest.called.should.equal true
describe "redeemCoupon", ->
beforeEach (done) ->
@recurlyAccountId = "account-id-123"
@coupon_code = "312321312"
@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
options.url.should.equal "coupons/#{@coupon_code}/redeem"
options.body.indexOf("<account_code>#{@recurlyAccountId}</account_code>").should.not.equal -1
options.body.indexOf("<currency>USD</currency>").should.not.equal -1
options.method.should.equal "post"
callback()
RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
@RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
afterEach ->
RecurlyWrapper.apiRequest.restore()
@RecurlyWrapper.apiRequest.restore()
it "should send the request to redem the coupon", ->
@apiRequest.called.should.equal true
describe "_addressToXml", ->
beforeEach ->
@address =
address1: "addr_one"
address2: "addr_two"
country: "some_country"
state: "some_state"
postal_code: "some_zip"
nonsenseKey: "rubbish"
it 'should generate the correct xml', () ->
result = @RecurlyWrapper._addressToXml @address
should.equal(
result,
"""
<billing_info>
<address1>addr_one</address1>
<address2 nil="nil">addr_two</address2>
<country>some_country</country>
<state>some_state</state>
<zip>some_zip</zip>
</billing_info>\n
"""
)
describe 'createSubscription', ->
beforeEach ->
@user =
_id: 'some_id'
email: 'user@example.com'
@subscriptionDetails =
currencyCode: "EUR"
plan_code: "some_plan_code"
coupon_code: ""
isPaypal: true
address:
address1: "addr_one"
address2: "addr_two"
country: "some_country"
state: "some_state"
zip: "some_zip"
@subscription = {}
@recurly_token_id = "a-token-id"
@call = (callback) =>
@RecurlyWrapper.createSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
describe 'when paypal', ->
beforeEach ->
@subscriptionDetails.isPaypal = true
@_createPaypalSubscription = sinon.stub(@RecurlyWrapper, '_createPaypalSubscription')
@_createPaypalSubscription.callsArgWith(3, null, @subscription)
afterEach ->
@_createPaypalSubscription.restore()
it 'should not produce an error', (done) ->
@call (err, sub) =>
expect(err).to.equal null
expect(err).to.not.be.instanceof Error
done()
it 'should produce a subscription object', (done) ->
@call (err, sub) =>
expect(sub).to.deep.equal @subscription
done()
it 'should call _createPaypalSubscription', (done) ->
@call (err, sub) =>
@_createPaypalSubscription.callCount.should.equal 1
done()
describe "when _createPaypalSubscription produces an error", ->
beforeEach ->
@_createPaypalSubscription.callsArgWith(3, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, sub) =>
expect(err).to.be.instanceof Error
done()
describe 'when not paypal', ->
beforeEach ->
@subscriptionDetails.isPaypal = false
@_createCreditCardSubscription = sinon.stub(@RecurlyWrapper, '_createCreditCardSubscription')
@_createCreditCardSubscription.callsArgWith(3, null, @subscription)
afterEach ->
@_createCreditCardSubscription.restore()
it 'should not produce an error', (done) ->
@call (err, sub) =>
expect(err).to.equal null
expect(err).to.not.be.instanceof Error
done()
it 'should produce a subscription object', (done) ->
@call (err, sub) =>
expect(sub).to.deep.equal @subscription
done()
it 'should call _createCreditCardSubscription', (done) ->
@call (err, sub) =>
@_createCreditCardSubscription.callCount.should.equal 1
done()
describe "when _createCreditCardSubscription produces an error", ->
beforeEach ->
@_createCreditCardSubscription.callsArgWith(3, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, sub) =>
expect(err).to.be.instanceof Error
done()
describe '_createCreditCardSubscription', ->
beforeEach ->
@user =
_id: 'some_id'
email: 'user@example.com'
@subscriptionDetails =
currencyCode: "EUR"
plan_code: "some_plan_code"
coupon_code: ""
isPaypal: true
address:
address1: "addr_one"
address2: "addr_two"
country: "some_country"
state: "some_state"
zip: "some_zip"
@subscription = {}
@recurly_token_id = "a-token-id"
@apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
@response =
statusCode: 200
@body = "<xml>is_bad</xml>"
@apiRequest.callsArgWith(1, null, @response, @body)
@_parseSubscriptionXml = sinon.stub(@RecurlyWrapper, '_parseSubscriptionXml')
@_parseSubscriptionXml.callsArgWith(1, null, @subscription)
@call = (callback) =>
@RecurlyWrapper._createCreditCardSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
afterEach ->
@apiRequest.restore()
@_parseSubscriptionXml.restore()
it 'should not produce an error', (done) ->
@call (err, sub) =>
expect(err).to.not.be.instanceof Error
expect(err).to.equal null
done()
it 'should produce a subscription', (done) ->
@call (err, sub) =>
expect(sub).to.equal @subscription
done()
it 'should call apiRequest', (done) ->
@call (err, sub) =>
@apiRequest.callCount.should.equal 1
done()
it 'should call _parseSubscriptionXml', (done) ->
@call (err, sub) =>
@_parseSubscriptionXml.callCount.should.equal 1
done()
describe 'when api request produces an error', ->
beforeEach ->
@apiRequest.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, sub) =>
expect(err).to.be.instanceof Error
done()
it 'should call apiRequest', (done) ->
@call (err, sub) =>
@apiRequest.callCount.should.equal 1
done()
it 'should not _parseSubscriptionXml', (done) ->
@call (err, sub) =>
@_parseSubscriptionXml.callCount.should.equal 0
done()
describe 'when parse xml produces an error', ->
beforeEach ->
@_parseSubscriptionXml.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, sub) =>
expect(err).to.be.instanceof Error
done()
describe '_createPaypalSubscription', ->
beforeEach ->
@checkAccountExists = sinon.stub(@RecurlyWrapper._paypal, 'checkAccountExists')
@createAccount = sinon.stub(@RecurlyWrapper._paypal, 'createAccount')
@createBillingInfo = sinon.stub(@RecurlyWrapper._paypal, 'createBillingInfo')
@setAddress = sinon.stub(@RecurlyWrapper._paypal, 'setAddress')
@createSubscription = sinon.stub(@RecurlyWrapper._paypal, 'createSubscription')
@user =
_id: 'some_id'
email: 'user@example.com'
@subscriptionDetails =
currencyCode: "EUR"
plan_code: "some_plan_code"
coupon_code: ""
isPaypal: true
address:
address1: "addr_one"
address2: "addr_two"
country: "some_country"
state: "some_state"
zip: "some_zip"
@subscription = {}
@recurly_token_id = "a-token-id"
# set up data callbacks
user = @user
subscriptionDetails = @subscriptionDetails
recurly_token_id = @recurly_token_id
@checkAccountExists.callsArgWith(1, null,
{user, subscriptionDetails, recurly_token_id,
userExists: false, account: {accountCode: 'xx'}}
)
@createAccount.callsArgWith(1, null,
{user, subscriptionDetails, recurly_token_id,
userExists: false, account: {accountCode: 'xx'}}
)
@createBillingInfo.callsArgWith(1, null,
{user, subscriptionDetails, recurly_token_id,
userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
)
@setAddress.callsArgWith(1, null,
{user, subscriptionDetails, recurly_token_id,
userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
)
@createSubscription.callsArgWith(1, null,
{user, subscriptionDetails, recurly_token_id,
userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}, subscription: @subscription}
)
@call = (callback) =>
@RecurlyWrapper._createPaypalSubscription @user, @subscriptionDetails, @recurly_token_id, callback
afterEach ->
@checkAccountExists.restore()
@createAccount.restore()
@createBillingInfo.restore()
@setAddress.restore()
@createSubscription.restore()
it 'should not produce an error', (done) ->
@call (err, sub) =>
expect(err).to.not.be.instanceof Error
done()
it 'should produce a subscription object', (done) ->
@call (err, sub) =>
expect(sub).to.not.equal null
expect(sub).to.equal @subscription
done()
it 'should call each of the paypal stages', (done) ->
@call (err, sub) =>
@checkAccountExists.callCount.should.equal 1
@createAccount.callCount.should.equal 1
@createBillingInfo.callCount.should.equal 1
@setAddress.callCount.should.equal 1
@createSubscription.callCount.should.equal 1
done()
describe 'when one of the paypal stages produces an error', ->
beforeEach ->
@createAccount.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, sub) =>
expect(err).to.be.instanceof Error
done()
it 'should stop calling the paypal stages after the error', (done) ->
@call (err, sub) =>
@checkAccountExists.callCount.should.equal 1
@createAccount.callCount.should.equal 1
@createBillingInfo.callCount.should.equal 0
@setAddress.callCount.should.equal 0
@createSubscription.callCount.should.equal 0
done()
describe 'paypal actions', ->
beforeEach ->
@apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
@_parseAccountXml = sinon.spy(@RecurlyWrapper, '_parseAccountXml')
@_parseBillingInfoXml = sinon.spy(@RecurlyWrapper, '_parseBillingInfoXml')
@_parseSubscriptionXml = sinon.spy(@RecurlyWrapper, '_parseSubscriptionXml')
@cache =
user: @user = {_id: 'some_id'}
recurly_token_id: @recurly_token_id = "some_token"
subscriptionDetails: @subscriptionDetails =
currencyCode: "EUR"
plan_code: "some_plan_code"
coupon_code: ""
isPaypal: true
address:
address1: "addr_one"
address2: "addr_two"
country: "some_country"
state: "some_state"
zip: "some_zip"
afterEach ->
@apiRequest.restore()
@_parseAccountXml.restore()
@_parseBillingInfoXml.restore()
@_parseSubscriptionXml.restore()
describe '_paypal.checkAccountExists', ->
beforeEach ->
@call = (callback) =>
@RecurlyWrapper._paypal.checkAccountExists @cache, callback
describe 'when the account exists', ->
beforeEach ->
resultXml = '<account><account_code>abc</account_code></account>'
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.not.be.instanceof Error
done()
it 'should call apiRequest', (done) ->
@call (err, result) =>
@apiRequest.callCount.should.equal 1
done()
it 'should call _parseAccountXml', (done) ->
@call (err, result) =>
@RecurlyWrapper._parseAccountXml.callCount.should.equal 1
done()
it 'should add the account to the cumulative result', (done) ->
@call (err, result) =>
expect(result.account).to.not.equal null
expect(result.account).to.not.equal undefined
expect(result.account).to.deep.equal {
account_code: 'abc'
}
done()
it 'should set userExists to true', (done) ->
@call (err, result) =>
expect(result.userExists).to.equal true
done()
describe 'when the account does not exist', ->
beforeEach ->
@apiRequest.callsArgWith(1, new Error('not found'), {statusCode: 404}, '')
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.not.be.instanceof Error
done()
it 'should call apiRequest', (done) ->
@call (err, result) =>
@apiRequest.callCount.should.equal 1
@apiRequest.firstCall.args[0].method.should.equal 'GET'
done()
it 'should not call _parseAccountXml', (done) ->
@call (err, result) =>
@RecurlyWrapper._parseAccountXml.callCount.should.equal 0
done()
it 'should not add the account to result', (done) ->
@call (err, result) =>
expect(result.account).to.equal undefined
done()
it 'should set userExists to false', (done) ->
@call (err, result) =>
expect(result.userExists).to.equal false
done()
describe 'when apiRequest produces an error', ->
beforeEach ->
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()
describe '_paypal.createAccount', ->
beforeEach ->
@call = (callback) =>
@RecurlyWrapper._paypal.createAccount @cache, callback
describe 'when address is missing from subscriptionDetails', ->
beforeEach ->
@cache.subscriptionDetails.address = null
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()
describe 'when account already exists', ->
beforeEach ->
@cache.userExists = true
@cache.account =
account_code: 'abc'
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.not.be.instanceof Error
done()
it 'should produce cache object', (done) ->
@call (err, result) =>
expect(result).to.deep.equal @cache
expect(result.account).to.deep.equal {
account_code: 'abc'
}
done()
it 'should not call apiRequest', (done) ->
@call (err, result) =>
@apiRequest.callCount.should.equal 0
done()
it 'should not call _parseAccountXml', (done) ->
@call (err, result) =>
@RecurlyWrapper._parseAccountXml.callCount.should.equal 0
done()
describe 'when account does not exist', ->
beforeEach ->
@cache.userExists = false
resultXml = '<account><account_code>abc</account_code></account>'
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.not.be.instanceof Error
done()
it 'should call apiRequest', (done) ->
@call (err, result) =>
@apiRequest.callCount.should.equal 1
@apiRequest.firstCall.args[0].method.should.equal 'POST'
done()
it 'should call _parseAccountXml', (done) ->
@call (err, result) =>
@RecurlyWrapper._parseAccountXml.callCount.should.equal 1
done()
describe 'when apiRequest produces an error', ->
beforeEach ->
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()
describe '_paypal.createBillingInfo', ->
beforeEach ->
@cache.account =
account_code: 'abc'
@call = (callback) =>
@RecurlyWrapper._paypal.createBillingInfo @cache, callback
describe 'when account_code is missing from cache', ->
beforeEach ->
@cache.account.account_code = null
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()
describe 'when all goes well', ->
beforeEach ->
resultXml = '<billing_info><a>1</a></billing_info>'
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.not.be.instanceof Error
done()
it 'should call apiRequest', (done) ->
@call (err, result) =>
@apiRequest.callCount.should.equal 1
@apiRequest.firstCall.args[0].method.should.equal 'POST'
done()
it 'should call _parseBillingInfoXml', (done) ->
@call (err, result) =>
@RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
done()
it 'should set billingInfo on cache', (done) ->
@call (err, result) =>
expect(result.billingInfo).to.deep.equal {
a: "1"
}
done()
describe 'when apiRequest produces an error', ->
beforeEach ->
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()
describe '_paypal.setAddress', ->
beforeEach ->
@cache.account =
account_code: 'abc'
@cache.billingInfo = {}
@call = (callback) =>
@RecurlyWrapper._paypal.setAddress @cache, callback
describe 'when account_code is missing from cache', ->
beforeEach ->
@cache.account.account_code = null
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()
describe 'when address is missing from subscriptionDetails', ->
beforeEach ->
@cache.subscriptionDetails.address = null
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()
describe 'when all goes well', ->
beforeEach ->
resultXml = '<billing_info><city>London</city></billing_info>'
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.not.be.instanceof Error
done()
it 'should call apiRequest', (done) ->
@call (err, result) =>
@apiRequest.callCount.should.equal 1
@apiRequest.firstCall.args[0].method.should.equal 'PUT'
done()
it 'should call _parseBillingInfoXml', (done) ->
@call (err, result) =>
@RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
done()
it 'should set billingInfo on cache', (done) ->
@call (err, result) =>
expect(result.billingInfo).to.deep.equal {
city: 'London'
}
done()
describe 'when apiRequest produces an error', ->
beforeEach ->
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()
describe '_paypal.createSubscription', ->
beforeEach ->
@cache.account =
account_code: 'abc'
@cache.billingInfo = {}
@call = (callback) =>
@RecurlyWrapper._paypal.createSubscription @cache, callback
describe 'when all goes well', ->
beforeEach ->
resultXml = '<subscription><a>1</a></subscription>'
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.not.be.instanceof Error
done()
it 'should call apiRequest', (done) ->
@call (err, result) =>
@apiRequest.callCount.should.equal 1
@apiRequest.firstCall.args[0].method.should.equal 'POST'
done()
it 'should call _parseSubscriptionXml', (done) ->
@call (err, result) =>
@RecurlyWrapper._parseSubscriptionXml.callCount.should.equal 1
done()
it 'should set subscription on cache', (done) ->
@call (err, result) =>
expect(result.subscription).to.deep.equal {
a: "1"
}
done()
describe 'when apiRequest produces an error', ->
beforeEach ->
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.be.instanceof Error
done()