mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-31 21:21:03 -04:00
2d8e832be7
GitOrigin-RevId: 43adff9af7ca347808980823ac641db05129a79b
1112 lines
29 KiB
JavaScript
1112 lines
29 KiB
JavaScript
const OError = require('@overleaf/o-error')
|
|
const request = require('request')
|
|
const Settings = require('@overleaf/settings')
|
|
const xml2js = require('xml2js')
|
|
const logger = require('@overleaf/logger')
|
|
const Async = require('async')
|
|
const Errors = require('../Errors/Errors')
|
|
const SubscriptionErrors = require('./Errors')
|
|
const { promisify } = require('util')
|
|
|
|
function updateAccountEmailAddress(accountId, newEmail, callback) {
|
|
const data = {
|
|
email: newEmail,
|
|
}
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('account', data)
|
|
} catch (error) {
|
|
return callback(
|
|
OError.tag(error, 'error building xml', { accountId, newEmail })
|
|
)
|
|
}
|
|
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountId}`,
|
|
method: 'PUT',
|
|
body: requestBody,
|
|
},
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseAccountXml(body, callback)
|
|
}
|
|
)
|
|
}
|
|
|
|
const RecurlyWrapper = {
|
|
apiUrl: Settings.apis.recurly.url || 'https://api.recurly.com/v2',
|
|
|
|
_paypal: {
|
|
checkAccountExists(cache, next) {
|
|
const { user } = cache
|
|
logger.debug(
|
|
{ user_id: user._id },
|
|
'checking if recurly account exists for user'
|
|
)
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${user._id}`,
|
|
method: 'GET',
|
|
expect404: true,
|
|
},
|
|
function (error, response, responseBody) {
|
|
if (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while checking account',
|
|
{
|
|
user_id: user._id,
|
|
}
|
|
)
|
|
return next(error)
|
|
}
|
|
if (response.statusCode === 404) {
|
|
// actually not an error in this case, just no existing account
|
|
logger.debug(
|
|
{ user_id: user._id },
|
|
'user does not currently exist in recurly, proceed'
|
|
)
|
|
cache.userExists = false
|
|
return next(null, cache)
|
|
}
|
|
logger.debug(
|
|
{ user_id: user._id },
|
|
'user appears to exist in recurly'
|
|
)
|
|
RecurlyWrapper._parseAccountXml(
|
|
responseBody,
|
|
function (err, account) {
|
|
if (err) {
|
|
OError.tag(err, 'error parsing account', {
|
|
user_id: user._id,
|
|
})
|
|
return next(err)
|
|
}
|
|
cache.userExists = true
|
|
cache.account = account
|
|
next(null, cache)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
},
|
|
createAccount(cache, next) {
|
|
const { user } = cache
|
|
const { subscriptionDetails } = cache
|
|
if (cache.userExists) {
|
|
return next(null, cache)
|
|
}
|
|
|
|
let address
|
|
try {
|
|
address = getAddressFromSubscriptionDetails(subscriptionDetails, false)
|
|
} catch (error) {
|
|
return next(error)
|
|
}
|
|
const data = {
|
|
account_code: user._id,
|
|
email: user.email,
|
|
first_name: user.first_name,
|
|
last_name: user.last_name,
|
|
address,
|
|
}
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('account', data)
|
|
} catch (error) {
|
|
return next(
|
|
OError.tag(error, 'error building xml', { user_id: user._id })
|
|
)
|
|
}
|
|
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: 'accounts',
|
|
method: 'POST',
|
|
body: requestBody,
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while creating account',
|
|
{
|
|
user_id: user._id,
|
|
}
|
|
)
|
|
return next(error)
|
|
}
|
|
RecurlyWrapper._parseAccountXml(
|
|
responseBody,
|
|
function (err, account) {
|
|
if (err) {
|
|
OError.tag(err, 'error creating account', {
|
|
user_id: user._id,
|
|
})
|
|
return next(err)
|
|
}
|
|
cache.account = account
|
|
next(null, cache)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
},
|
|
createBillingInfo(cache, next) {
|
|
const { user } = cache
|
|
const { recurlyTokenIds } = cache
|
|
logger.debug({ user_id: user._id }, 'creating billing info in recurly')
|
|
const accountCode = cache?.account?.account_code
|
|
if (!accountCode) {
|
|
return next(new Error('no account code at createBillingInfo stage'))
|
|
}
|
|
const data = { token_id: recurlyTokenIds.billing }
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('billing_info', data)
|
|
} catch (error) {
|
|
return next(
|
|
OError.tag(error, 'error building xml', { user_id: user._id })
|
|
)
|
|
}
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountCode}/billing_info`,
|
|
method: 'POST',
|
|
body: requestBody,
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while creating billing info',
|
|
{
|
|
user_id: user._id,
|
|
}
|
|
)
|
|
return next(error)
|
|
}
|
|
RecurlyWrapper._parseBillingInfoXml(
|
|
responseBody,
|
|
function (err, billingInfo) {
|
|
if (err) {
|
|
OError.tag(err, 'error creating billing info', {
|
|
user_id: user._id,
|
|
accountCode,
|
|
})
|
|
return next(err)
|
|
}
|
|
cache.billingInfo = billingInfo
|
|
next(null, cache)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
},
|
|
|
|
setAddressAndCompanyBillingInfo(cache, next) {
|
|
const { user } = cache
|
|
const { subscriptionDetails } = cache
|
|
logger.debug(
|
|
{ user_id: user._id },
|
|
'setting billing address and company info in recurly'
|
|
)
|
|
const accountCode = cache?.account?.account_code
|
|
if (!accountCode) {
|
|
return next(
|
|
new Error('no account code at setAddressAndCompanyBillingInfo stage')
|
|
)
|
|
}
|
|
|
|
let addressAndCompanyBillingInfo
|
|
try {
|
|
addressAndCompanyBillingInfo = getAddressFromSubscriptionDetails(
|
|
subscriptionDetails,
|
|
true
|
|
)
|
|
} catch (error) {
|
|
return next(error)
|
|
}
|
|
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml(
|
|
'billing_info',
|
|
addressAndCompanyBillingInfo
|
|
)
|
|
} catch (error) {
|
|
return next(
|
|
OError.tag(error, 'error building xml', { user_id: user._id })
|
|
)
|
|
}
|
|
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountCode}/billing_info`,
|
|
method: 'PUT',
|
|
body: requestBody,
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while setting address',
|
|
{
|
|
user_id: user._id,
|
|
}
|
|
)
|
|
return next(error)
|
|
}
|
|
RecurlyWrapper._parseBillingInfoXml(
|
|
responseBody,
|
|
function (err, billingInfo) {
|
|
if (err) {
|
|
OError.tag(err, 'error updating billing info', {
|
|
user_id: user._id,
|
|
})
|
|
return next(err)
|
|
}
|
|
cache.billingInfo = billingInfo
|
|
next(null, cache)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
},
|
|
createSubscription(cache, next) {
|
|
const { user } = cache
|
|
const { subscriptionDetails } = cache
|
|
logger.debug({ user_id: user._id }, 'creating subscription in recurly')
|
|
const data = {
|
|
plan_code: subscriptionDetails.plan_code,
|
|
currency: subscriptionDetails.currencyCode,
|
|
coupon_code: subscriptionDetails.coupon_code,
|
|
account: {
|
|
account_code: user._id,
|
|
},
|
|
}
|
|
const customFields =
|
|
getCustomFieldsFromSubscriptionDetails(subscriptionDetails)
|
|
if (customFields) {
|
|
data.custom_fields = customFields
|
|
}
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('subscription', data)
|
|
} catch (error) {
|
|
return next(
|
|
OError.tag(error, 'error building xml', { user_id: user._id })
|
|
)
|
|
}
|
|
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: 'subscriptions',
|
|
method: 'POST',
|
|
body: requestBody,
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while creating subscription',
|
|
{
|
|
user_id: user._id,
|
|
}
|
|
)
|
|
return next(error)
|
|
}
|
|
RecurlyWrapper._parseSubscriptionXml(
|
|
responseBody,
|
|
function (err, subscription) {
|
|
if (err) {
|
|
OError.tag(err, 'error creating subscription', {
|
|
user_id: user._id,
|
|
})
|
|
return next(err)
|
|
}
|
|
cache.subscription = subscription
|
|
next(null, cache)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
},
|
|
},
|
|
|
|
_createPaypalSubscription(
|
|
user,
|
|
subscriptionDetails,
|
|
recurlyTokenIds,
|
|
callback
|
|
) {
|
|
logger.debug(
|
|
{ user_id: user._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
|
|
const cache = { user, recurlyTokenIds, subscriptionDetails }
|
|
Async.waterfall(
|
|
[
|
|
Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache),
|
|
RecurlyWrapper._paypal.createAccount,
|
|
RecurlyWrapper._paypal.createBillingInfo,
|
|
RecurlyWrapper._paypal.setAddressAndCompanyBillingInfo,
|
|
RecurlyWrapper._paypal.createSubscription,
|
|
],
|
|
function (err, result) {
|
|
if (err) {
|
|
OError.tag(err, 'error in paypal subscription creation process', {
|
|
user_id: user._id,
|
|
})
|
|
return callback(err)
|
|
}
|
|
if (!result.subscription) {
|
|
err = new Error('no subscription object in result')
|
|
OError.tag(err, 'error in paypal subscription creation process', {
|
|
user_id: user._id,
|
|
})
|
|
return callback(err)
|
|
}
|
|
logger.debug(
|
|
{ user_id: user._id },
|
|
'done creating paypal subscription for user'
|
|
)
|
|
callback(null, result.subscription)
|
|
}
|
|
)
|
|
},
|
|
|
|
_createCreditCardSubscription(
|
|
user,
|
|
subscriptionDetails,
|
|
recurlyTokenIds,
|
|
callback
|
|
) {
|
|
const data = {
|
|
plan_code: subscriptionDetails.plan_code,
|
|
currency: subscriptionDetails.currencyCode,
|
|
coupon_code: subscriptionDetails.coupon_code,
|
|
account: {
|
|
account_code: user._id,
|
|
email: user.email,
|
|
first_name: subscriptionDetails.first_name || user.first_name,
|
|
last_name: subscriptionDetails.last_name || user.last_name,
|
|
billing_info: {
|
|
token_id: recurlyTokenIds.billing,
|
|
},
|
|
},
|
|
}
|
|
if (recurlyTokenIds.threeDSecureActionResult) {
|
|
data.account.billing_info.three_d_secure_action_result_token_id =
|
|
recurlyTokenIds.threeDSecureActionResult
|
|
}
|
|
const customFields =
|
|
getCustomFieldsFromSubscriptionDetails(subscriptionDetails)
|
|
if (customFields) {
|
|
data.custom_fields = customFields
|
|
}
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('subscription', data)
|
|
} catch (error) {
|
|
return callback(
|
|
OError.tag(error, 'error building xml', { user_id: user._id })
|
|
)
|
|
}
|
|
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: 'subscriptions',
|
|
method: 'POST',
|
|
body: requestBody,
|
|
expect422: true,
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
|
|
if (response.statusCode === 422) {
|
|
RecurlyWrapper._handle422Response(responseBody, callback)
|
|
} else {
|
|
RecurlyWrapper._parseSubscriptionXml(responseBody, callback)
|
|
}
|
|
}
|
|
)
|
|
},
|
|
|
|
createSubscription(user, subscriptionDetails, recurlyTokenIds, callback) {
|
|
const { isPaypal } = subscriptionDetails
|
|
logger.debug(
|
|
{ user_id: user._id, isPaypal },
|
|
'setting up subscription in recurly'
|
|
)
|
|
const fn = isPaypal
|
|
? RecurlyWrapper._createPaypalSubscription
|
|
: RecurlyWrapper._createCreditCardSubscription
|
|
return fn(user, subscriptionDetails, recurlyTokenIds, callback)
|
|
},
|
|
|
|
apiRequest(options, callback) {
|
|
options.url = RecurlyWrapper.apiUrl + '/' + options.url
|
|
options.headers = {
|
|
Authorization: `Basic ${Buffer.from(
|
|
Settings.apis.recurly.apiKey
|
|
).toString('base64')}`,
|
|
Accept: 'application/xml',
|
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
'X-Api-Version': Settings.apis.recurly.apiVersion,
|
|
}
|
|
const { expect404, expect422 } = options
|
|
delete options.expect404
|
|
delete options.expect422
|
|
request(options, function (error, response, body) {
|
|
if (
|
|
!error &&
|
|
response.statusCode !== 200 &&
|
|
response.statusCode !== 201 &&
|
|
response.statusCode !== 204 &&
|
|
(response.statusCode !== 404 || !expect404) &&
|
|
(response.statusCode !== 422 || !expect422)
|
|
) {
|
|
logger.warn(
|
|
{
|
|
err: error,
|
|
body,
|
|
options,
|
|
statusCode: response ? response.statusCode : undefined,
|
|
},
|
|
'error returned from recurly'
|
|
)
|
|
error = new OError(
|
|
`Recurly API returned with status code: ${response.statusCode}`,
|
|
{ statusCode: response.statusCode }
|
|
)
|
|
}
|
|
callback(error, response, body)
|
|
})
|
|
},
|
|
|
|
getSubscriptions(accountId, callback) {
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountId}/subscriptions`,
|
|
},
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseXml(body, callback)
|
|
}
|
|
)
|
|
},
|
|
|
|
getSubscription(subscriptionId, options, callback) {
|
|
let url
|
|
if (!callback) {
|
|
callback = options
|
|
}
|
|
if (!options) {
|
|
options = {}
|
|
}
|
|
|
|
if (options.recurlyJsResult) {
|
|
url = `recurly_js/result/${subscriptionId}`
|
|
} else {
|
|
url = `subscriptions/${subscriptionId}`
|
|
}
|
|
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url,
|
|
},
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseSubscriptionXml(
|
|
body,
|
|
(error, recurlySubscription) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
if (options.includeAccount) {
|
|
let accountId
|
|
if (
|
|
recurlySubscription.account &&
|
|
recurlySubscription.account.url
|
|
) {
|
|
accountId =
|
|
recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
|
|
} else {
|
|
return callback(
|
|
new Error("I don't understand the response from Recurly")
|
|
)
|
|
}
|
|
|
|
RecurlyWrapper.getAccount(accountId, function (error, account) {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
recurlySubscription.account = account
|
|
callback(null, recurlySubscription)
|
|
})
|
|
} else {
|
|
callback(null, recurlySubscription)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
)
|
|
},
|
|
|
|
getPaginatedEndpoint(resource, queryParams, callback) {
|
|
queryParams.per_page = queryParams.per_page || 200
|
|
let allItems = []
|
|
const getPage = (cursor = null) => {
|
|
const opts = {
|
|
url: resource,
|
|
qs: queryParams,
|
|
}
|
|
if (cursor) {
|
|
opts.qs.cursor = cursor
|
|
}
|
|
return RecurlyWrapper.apiRequest(opts, (error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
return RecurlyWrapper._parseXml(body, function (err, data) {
|
|
if (err) {
|
|
logger.warn({ err }, 'could not get accoutns')
|
|
return callback(err)
|
|
}
|
|
const items = data[resource]
|
|
allItems = allItems.concat(items)
|
|
logger.debug(
|
|
`got another ${items.length}, total now ${allItems.length}`
|
|
)
|
|
const match = response.headers.link?.match(
|
|
/cursor=([0-9.]+%3A[0-9.]+)&/
|
|
)
|
|
cursor = match && match[1]
|
|
if (cursor) {
|
|
cursor = decodeURIComponent(cursor)
|
|
return getPage(cursor)
|
|
} else {
|
|
callback(err, allItems)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
getPage()
|
|
},
|
|
|
|
getAccount(accountId, callback) {
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountId}`,
|
|
},
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseAccountXml(body, callback)
|
|
}
|
|
)
|
|
},
|
|
|
|
updateAccountEmailAddress,
|
|
|
|
getAccountActiveCoupons(accountId, callback) {
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountId}/redemptions`,
|
|
},
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseRedemptionsXml(
|
|
body,
|
|
function (error, redemptions) {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
const activeRedemptions = redemptions.filter(
|
|
redemption => redemption.state === 'active'
|
|
)
|
|
const couponCodes = activeRedemptions.map(
|
|
redemption => redemption.coupon_code
|
|
)
|
|
Async.map(
|
|
couponCodes,
|
|
RecurlyWrapper.getCoupon,
|
|
function (error, coupons) {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
return callback(null, coupons)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
},
|
|
|
|
getCoupon(couponCode, callback) {
|
|
const opts = { url: `coupons/${couponCode}` }
|
|
RecurlyWrapper.apiRequest(opts, (error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseCouponXml(body, callback)
|
|
})
|
|
},
|
|
|
|
getBillingInfo(accountId, callback) {
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountId}/billing_info`,
|
|
},
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseXml(body, callback)
|
|
}
|
|
)
|
|
},
|
|
|
|
getAccountPastDueInvoices(accountId, callback) {
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountId}/invoices?state=past_due`,
|
|
},
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseInvoicesXml(body, callback)
|
|
}
|
|
)
|
|
},
|
|
|
|
attemptInvoiceCollection(invoiceId, callback) {
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `invoices/${invoiceId}/collect`,
|
|
method: 'put',
|
|
},
|
|
callback
|
|
)
|
|
},
|
|
|
|
updateSubscription(subscriptionId, options, callback) {
|
|
logger.debug(
|
|
{ subscriptionId, options },
|
|
'telling recurly to update subscription'
|
|
)
|
|
const data = {
|
|
plan_code: options.plan_code,
|
|
timeframe: options.timeframe,
|
|
}
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('subscription', data)
|
|
} catch (error) {
|
|
return callback(
|
|
OError.tag(error, 'error building xml', { subscriptionId })
|
|
)
|
|
}
|
|
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `subscriptions/${subscriptionId}`,
|
|
method: 'put',
|
|
body: requestBody,
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseSubscriptionXml(responseBody, callback)
|
|
}
|
|
)
|
|
},
|
|
|
|
createFixedAmmountCoupon(
|
|
couponCode,
|
|
name,
|
|
currencyCode,
|
|
discountInCents,
|
|
planCode,
|
|
callback
|
|
) {
|
|
const data = {
|
|
coupon_code: couponCode,
|
|
name,
|
|
discount_type: 'dollars',
|
|
discount_in_cents: {},
|
|
plan_codes: {
|
|
plan_code: planCode,
|
|
},
|
|
applies_to_all_plans: false,
|
|
}
|
|
data.discount_in_cents[currencyCode] = discountInCents
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('coupon', data)
|
|
} catch (error) {
|
|
return callback(
|
|
OError.tag(error, 'error building xml', {
|
|
couponCode,
|
|
name,
|
|
})
|
|
)
|
|
}
|
|
|
|
logger.debug({ couponCode, requestBody }, 'creating coupon')
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: 'coupons',
|
|
method: 'post',
|
|
body: requestBody,
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
logger.warn({ err: error, couponCode }, 'error creating coupon')
|
|
}
|
|
callback(error)
|
|
}
|
|
)
|
|
},
|
|
|
|
lookupCoupon(couponCode, callback) {
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `coupons/${couponCode}`,
|
|
},
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
RecurlyWrapper._parseXml(body, callback)
|
|
}
|
|
)
|
|
},
|
|
|
|
redeemCoupon(accountCode, couponCode, callback) {
|
|
const data = {
|
|
account_code: accountCode,
|
|
currency: 'USD',
|
|
}
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('redemption', data)
|
|
} catch (error) {
|
|
return callback(
|
|
OError.tag(error, 'error building xml', {
|
|
accountCode,
|
|
couponCode,
|
|
})
|
|
)
|
|
}
|
|
|
|
logger.debug(
|
|
{ accountCode, couponCode, requestBody },
|
|
'redeeming coupon for user'
|
|
)
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `coupons/${couponCode}/redeem`,
|
|
method: 'post',
|
|
body: requestBody,
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
logger.warn(
|
|
{ err: error, accountCode, couponCode },
|
|
'error redeeming coupon'
|
|
)
|
|
}
|
|
callback(error)
|
|
}
|
|
)
|
|
},
|
|
|
|
extendTrial(subscriptionId, daysUntilExpire, callback) {
|
|
if (daysUntilExpire == null) {
|
|
daysUntilExpire = 7
|
|
}
|
|
const nextRenewalDate = new Date()
|
|
nextRenewalDate.setDate(nextRenewalDate.getDate() + daysUntilExpire)
|
|
logger.debug(
|
|
{ subscriptionId, daysUntilExpire },
|
|
'Exending Free trial for user'
|
|
)
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `/subscriptions/${subscriptionId}/postpone?next_renewal_date=${nextRenewalDate}&bulk=false`,
|
|
method: 'put',
|
|
},
|
|
(error, response, responseBody) => {
|
|
if (error) {
|
|
logger.warn(
|
|
{ err: error, subscriptionId, daysUntilExpire },
|
|
'error exending trial'
|
|
)
|
|
}
|
|
callback(error)
|
|
}
|
|
)
|
|
},
|
|
|
|
listAccountActiveSubscriptions(accountId, callback) {
|
|
RecurlyWrapper.apiRequest(
|
|
{
|
|
url: `accounts/${accountId}/subscriptions`,
|
|
qs: {
|
|
state: 'active',
|
|
},
|
|
expect404: true,
|
|
},
|
|
function (error, response, body) {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
if (response.statusCode === 404) {
|
|
callback(null, [])
|
|
} else {
|
|
RecurlyWrapper._parseSubscriptionsXml(body, callback)
|
|
}
|
|
}
|
|
)
|
|
},
|
|
|
|
_handle422Response(body, callback) {
|
|
RecurlyWrapper._parseErrorsXml(body, (error, data) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
|
|
let errorData = {}
|
|
if (data.transaction_error) {
|
|
errorData = {
|
|
message: data.transaction_error.merchant_message,
|
|
info: {
|
|
category: data.transaction_error.error_category,
|
|
gatewayCode: data.transaction_error.gateway_error_code,
|
|
public: {
|
|
code: data.transaction_error.error_code,
|
|
message: data.transaction_error.customer_message,
|
|
},
|
|
},
|
|
}
|
|
if (data.transaction_error.three_d_secure_action_token_id) {
|
|
errorData.info.public.threeDSecureActionTokenId =
|
|
data.transaction_error.three_d_secure_action_token_id
|
|
}
|
|
} else if (data.error && data.error._) {
|
|
// fallback for errors that don't have a `transaction_error` field, but
|
|
// instead a `error` field with a message (e.g. VATMOSS errors)
|
|
errorData = {
|
|
info: {
|
|
public: {
|
|
message: data.error._,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
callback(new SubscriptionErrors.RecurlyTransactionError(errorData))
|
|
})
|
|
},
|
|
_parseSubscriptionsXml(xml, callback) {
|
|
RecurlyWrapper._parseXmlAndGetAttribute(xml, 'subscriptions', callback)
|
|
},
|
|
|
|
_parseSubscriptionXml(xml, callback) {
|
|
RecurlyWrapper._parseXmlAndGetAttribute(xml, 'subscription', callback)
|
|
},
|
|
|
|
_parseAccountXml(xml, callback) {
|
|
RecurlyWrapper._parseXmlAndGetAttribute(xml, 'account', callback)
|
|
},
|
|
|
|
_parseBillingInfoXml(xml, callback) {
|
|
RecurlyWrapper._parseXmlAndGetAttribute(xml, 'billing_info', callback)
|
|
},
|
|
|
|
_parseRedemptionsXml(xml, callback) {
|
|
RecurlyWrapper._parseXmlAndGetAttribute(xml, 'redemptions', callback)
|
|
},
|
|
|
|
_parseCouponXml(xml, callback) {
|
|
RecurlyWrapper._parseXmlAndGetAttribute(xml, 'coupon', callback)
|
|
},
|
|
|
|
_parseErrorsXml(xml, callback) {
|
|
RecurlyWrapper._parseXmlAndGetAttribute(xml, 'errors', callback)
|
|
},
|
|
|
|
_parseInvoicesXml(xml, callback) {
|
|
RecurlyWrapper._parseXmlAndGetAttribute(xml, 'invoices', callback)
|
|
},
|
|
|
|
_parseXmlAndGetAttribute(xml, attribute, callback) {
|
|
RecurlyWrapper._parseXml(xml, function (error, data) {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
if (data && data[attribute] != null) {
|
|
callback(null, data[attribute])
|
|
} else {
|
|
callback(new Error("I don't understand the response from Recurly"))
|
|
}
|
|
})
|
|
},
|
|
|
|
_parseXml(xml, callback) {
|
|
function convertDataTypes(data) {
|
|
let key, value
|
|
if (data && data.$) {
|
|
if (data.$.nil === 'nil') {
|
|
data = null
|
|
} else if (data.$.href) {
|
|
data.url = data.$.href
|
|
delete data.$
|
|
} else if (data.$.type === 'integer') {
|
|
data = parseInt(data._, 10)
|
|
} else if (data.$.type === 'datetime') {
|
|
data = new Date(data._)
|
|
} else if (data.$.type === 'array') {
|
|
delete data.$
|
|
let array = []
|
|
for (key in data) {
|
|
value = data[key]
|
|
if (value instanceof Array) {
|
|
array = array.concat(convertDataTypes(value))
|
|
} else {
|
|
array.push(convertDataTypes(value))
|
|
}
|
|
}
|
|
data = array
|
|
}
|
|
}
|
|
|
|
if (data instanceof Array) {
|
|
data = data.map(entry => convertDataTypes(entry))
|
|
} else if (typeof data === 'object') {
|
|
for (key in data) {
|
|
value = data[key]
|
|
data[key] = convertDataTypes(value)
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
const parser = new xml2js.Parser({
|
|
explicitRoot: true,
|
|
explicitArray: false,
|
|
emptyTag: '',
|
|
})
|
|
parser.parseString(xml, function (error, data) {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
const result = convertDataTypes(data)
|
|
callback(null, result)
|
|
})
|
|
},
|
|
|
|
_buildXml(rootName, data) {
|
|
const options = {
|
|
headless: true,
|
|
renderOpts: {
|
|
pretty: true,
|
|
indent: '\t',
|
|
},
|
|
rootName,
|
|
}
|
|
const builder = new xml2js.Builder(options)
|
|
return builder.buildObject(data)
|
|
},
|
|
}
|
|
|
|
RecurlyWrapper.promises = {
|
|
getSubscription: promisify(RecurlyWrapper.getSubscription),
|
|
updateAccountEmailAddress: promisify(updateAccountEmailAddress),
|
|
}
|
|
|
|
module.exports = RecurlyWrapper
|
|
|
|
function getCustomFieldsFromSubscriptionDetails(subscriptionDetails) {
|
|
if (!subscriptionDetails.ITMCampaign) {
|
|
return null
|
|
}
|
|
|
|
const customFields = [
|
|
{
|
|
name: 'itm_campaign',
|
|
value: subscriptionDetails.ITMCampaign,
|
|
},
|
|
]
|
|
if (subscriptionDetails.ITMContent) {
|
|
customFields.push({
|
|
name: 'itm_content',
|
|
value: subscriptionDetails.ITMContent,
|
|
})
|
|
}
|
|
return { custom_field: customFields }
|
|
}
|
|
|
|
function getAddressFromSubscriptionDetails(
|
|
subscriptionDetails,
|
|
includeCompanyInfo
|
|
) {
|
|
const { address } = subscriptionDetails
|
|
|
|
if (!address || !address.country) {
|
|
throw new Errors.InvalidError({
|
|
message: 'Invalid country',
|
|
info: {
|
|
public: {
|
|
message: 'Invalid country',
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
const addressObject = {
|
|
address1: address.address1,
|
|
address2: address.address2 || '',
|
|
city: address.city || '',
|
|
state: address.state || '',
|
|
zip: address.zip || '',
|
|
country: address.country,
|
|
}
|
|
|
|
if (
|
|
includeCompanyInfo &&
|
|
subscriptionDetails.billing_info &&
|
|
subscriptionDetails.billing_info.company &&
|
|
subscriptionDetails.billing_info.company !== ''
|
|
) {
|
|
addressObject.company = subscriptionDetails.billing_info.company
|
|
if (
|
|
subscriptionDetails.billing_info.vat_number &&
|
|
subscriptionDetails.billing_info.vat_number !== ''
|
|
) {
|
|
addressObject.vat_number = subscriptionDetails.billing_info.vat_number
|
|
}
|
|
}
|
|
|
|
return addressObject
|
|
}
|