Просмотр исходного кода

Merge branch 'sk-subscription-address'

Shane Kilkelly 10 лет назад
Родитель
Сommit
9e16f250f9

+ 236 - 34
services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee

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

+ 7 - 0
services/web/public/coffee/main/new-subscription.coffee

@@ -115,6 +115,13 @@ define [
 						currencyCode:pricing.items.currency
 						currencyCode:pricing.items.currency
 						plan_code:pricing.items.plan.code
 						plan_code:pricing.items.plan.code
 						coupon_code:pricing.items?.coupon?.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)
 				$http.post("/user/subscription/create", postData)
 					.success (data, status, headers)->
 					.success (data, status, headers)->
 						sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
 						sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->

+ 766 - 80
services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee

@@ -1,9 +1,10 @@
 should = require('chai').should()
 should = require('chai').should()
+expect = require('chai').expect
 sinon = require 'sinon'
 sinon = require 'sinon'
 crypto = require 'crypto'
 crypto = require 'crypto'
 querystring = require 'querystring'
 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")
 tk = require("timekeeper")
 
 
 fixtures =
 fixtures =
@@ -97,22 +98,37 @@ mockApiRequest = (options, callback) ->
 
 
 
 
 describe "RecurlyWrapper", ->
 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", ->
 	describe "sign", ->
+
 		before (done) ->
 		before (done) ->
 			tk.freeze Date.now() # freeze the time for these tests
 			tk.freeze Date.now() # freeze the time for these tests
-			RecurlyWrapper.sign({
+			@RecurlyWrapper.sign({
 				subscription :
 				subscription :
 					plan_code : "gold"
 					plan_code : "gold"
 					name      : "$$$"
 					name      : "$$$"
@@ -127,7 +143,7 @@ describe "RecurlyWrapper", ->
 		it "should be signed correctly", ->
 		it "should be signed correctly", ->
 			signed = @signature.split("|")[0]
 			signed = @signature.split("|")[0]
 			query = @signature.split("|")[1]
 			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", ->
 		it "should be url escaped", ->
 			query = @signature.split("|")[1]
 			query = @signature.split("|")[1]
@@ -149,38 +165,39 @@ describe "RecurlyWrapper", ->
 
 
 	describe "_parseXml", ->
 	describe "_parseXml", ->
 		it "should convert different data types into correct representations", (done) ->
 		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.plan_code.should.equal "gold"
 				data.subscription.plan.name.should.equal "Gold plan"
 				data.subscription.plan.name.should.equal "Gold plan"
 				data.subscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
 				data.subscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
@@ -188,32 +205,37 @@ describe "RecurlyWrapper", ->
 				data.subscription.unit_amount_in_cents.should.equal 800
 				data.subscription.unit_amount_in_cents.should.equal 800
 				data.subscription.currency.should.equal "EUR"
 				data.subscription.currency.should.equal "EUR"
 				data.subscription.quantity.should.equal 1
 				data.subscription.quantity.should.equal 1
+
 				data.subscription.activated_at.should.deep.equal new Date("2011-05-27T07:00:00Z")
 				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.canceled_at, null
 				should.equal data.subscription.expires_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_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")
 				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_started_at, null
 				should.equal data.subscription.trial_ends_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"
 					add_on_code: "ipaddresses"
 					quantity: "10"
 					quantity: "10"
 					unit_amount_in_cents: "150"
 					unit_amount_in_cents: "150"
-				}]
+				}
 				data.subscription.account.url.should.equal "https://api.recurly.com/v2/accounts/1"
 				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.url.should.equal "https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96"
 				data.subscription.plan.url.should.equal "https://api.recurly.com/v2/plans/gold"
 				data.subscription.plan.url.should.equal "https://api.recurly.com/v2/plans/gold"
 				done()
 				done()
-	
+
 	describe "getSubscription", ->
 	describe "getSubscription", ->
+
 		describe "with proper subscription id", ->
 		describe "with proper subscription id", ->
 			before ->
 			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
 					@recurlySubscription = recurlySubscription
 			after ->
 			after ->
-				RecurlyWrapper.apiRequest.restore()
-			
+				@RecurlyWrapper.apiRequest.restore()
+
 			it "should look up the subscription at the normal API end point", ->
 			it "should look up the subscription at the normal API end point", ->
 				@apiRequest.args[0][0].url.should.equal "subscriptions/44f83d7cba354d5b84812419f923ea96"
 				@apiRequest.args[0][0].url.should.equal "subscriptions/44f83d7cba354d5b84812419f923ea96"
 
 
@@ -222,12 +244,12 @@ describe "RecurlyWrapper", ->
 
 
 		describe "with ReculyJS token", ->
 		describe "with ReculyJS token", ->
 			before ->
 			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
 					@recurlySubscription = recurlySubscription
 			after ->
 			after ->
-				RecurlyWrapper.apiRequest.restore()
-				
+				@RecurlyWrapper.apiRequest.restore()
+
 			it "should return the subscription", ->
 			it "should return the subscription", ->
 				@recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
 				@recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
 
 
@@ -236,30 +258,30 @@ describe "RecurlyWrapper", ->
 
 
 		describe "with includeAccount", ->
 		describe "with includeAccount", ->
 			beforeEach ->
 			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
 					@recurlySubscription = recurlySubscription
 			afterEach ->
 			afterEach ->
-				RecurlyWrapper.apiRequest.restore()
+				@RecurlyWrapper.apiRequest.restore()
 
 
 			it "should request the account from the API", ->
 			it "should request the account from the API", ->
 				@apiRequest.args[1][0].url.should.equal "accounts/104"
 				@apiRequest.args[1][0].url.should.equal "accounts/104"
-				
+
 			it "should populate the account attribute", ->
 			it "should populate the account attribute", ->
 				@recurlySubscription.account.account_code.should.equal "104"
 				@recurlySubscription.account.account_code.should.equal "104"
-			
+
 
 
 	describe "updateSubscription", ->
 	describe "updateSubscription", ->
 		beforeEach (done) ->
 		beforeEach (done) ->
 			@recurlySubscriptionId = "subscription-id-123"
 			@recurlySubscriptionId = "subscription-id-123"
-			@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
+			@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
 				@requestOptions = options
 				@requestOptions = options
 				callback null, {}, fixtures["subscriptions/44f83d7cba354d5b84812419f923ea96"]
 				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
 				@recurlySubscription = recurlySubscription
 				done()
 				done()
 		afterEach ->
 		afterEach ->
-			RecurlyWrapper.apiRequest.restore()
+			@RecurlyWrapper.apiRequest.restore()
 
 
 		it "should send an update request to the API", ->
 		it "should send an update request to the API", ->
 			@apiRequest.called.should.equal true
 			@apiRequest.called.should.equal true
@@ -275,59 +297,723 @@ describe "RecurlyWrapper", ->
 		it "should return the updated subscription", ->
 		it "should return the updated subscription", ->
 			should.exist @recurlySubscription
 			should.exist @recurlySubscription
 			@recurlySubscription.plan.plan_code.should.equal "gold"
 			@recurlySubscription.plan.plan_code.should.equal "gold"
-			
+
 
 
 	describe "cancelSubscription", ->
 	describe "cancelSubscription", ->
 		beforeEach (done) ->
 		beforeEach (done) ->
 			@recurlySubscriptionId = "subscription-id-123"
 			@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.url.should.equal "subscriptions/#{@recurlySubscriptionId}/cancel"
 				options.method.should.equal "put"
 				options.method.should.equal "put"
 				callback()
 				callback()
-			RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
+			@RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
 
 
 		afterEach ->
 		afterEach ->
-			RecurlyWrapper.apiRequest.restore()
+			@RecurlyWrapper.apiRequest.restore()
 
 
 		it "should send a cancel request to the API", ->
 		it "should send a cancel request to the API", ->
 			@apiRequest.called.should.equal true
 			@apiRequest.called.should.equal true
-	
+
 	describe "reactivateSubscription", ->
 	describe "reactivateSubscription", ->
 		beforeEach (done) ->
 		beforeEach (done) ->
 			@recurlySubscriptionId = "subscription-id-123"
 			@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.url.should.equal "subscriptions/#{@recurlySubscriptionId}/reactivate"
 				options.method.should.equal "put"
 				options.method.should.equal "put"
 				callback()
 				callback()
-			RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
+			@RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
 
 
 		afterEach ->
 		afterEach ->
-			RecurlyWrapper.apiRequest.restore()
+			@RecurlyWrapper.apiRequest.restore()
 
 
 		it "should send a cancel request to the API", ->
 		it "should send a cancel request to the API", ->
 			@apiRequest.called.should.equal true
 			@apiRequest.called.should.equal true
-	
-		
+
+
 
 
 	describe "redeemCoupon", ->
 	describe "redeemCoupon", ->
 
 
 		beforeEach (done) ->
 		beforeEach (done) ->
 			@recurlyAccountId = "account-id-123"
 			@recurlyAccountId = "account-id-123"
 			@coupon_code = "312321312"
 			@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.url.should.equal "coupons/#{@coupon_code}/redeem"
 				options.body.indexOf("<account_code>#{@recurlyAccountId}</account_code>").should.not.equal -1
 				options.body.indexOf("<account_code>#{@recurlyAccountId}</account_code>").should.not.equal -1
 				options.body.indexOf("<currency>USD</currency>").should.not.equal -1
 				options.body.indexOf("<currency>USD</currency>").should.not.equal -1
 				options.method.should.equal "post"
 				options.method.should.equal "post"
 				callback()
 				callback()
-			RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
+			@RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
 
 
 		afterEach ->
 		afterEach ->
-			RecurlyWrapper.apiRequest.restore()
+			@RecurlyWrapper.apiRequest.restore()
 
 
 		it "should send the request to redem the coupon", ->
 		it "should send the request to redem the coupon", ->
 			@apiRequest.called.should.equal true
 			@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()