mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-02 13:49:00 +02:00
a22319fe46
Fix no-floating-promises lint rule GitOrigin-RevId: 58f89e0078abf72be65552f9468cb225e5758e3e
955 lines
27 KiB
JavaScript
955 lines
27 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 Errors = require('../Errors/Errors')
|
|
const SubscriptionErrors = require('./Errors')
|
|
const { callbackify } = require('@overleaf/promise-utils')
|
|
|
|
async function updateAccountEmailAddress(accountId, newEmail) {
|
|
const data = {
|
|
email: newEmail,
|
|
}
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('account', data)
|
|
} catch (error) {
|
|
throw OError.tag(error, 'error building xml', { accountId, newEmail })
|
|
}
|
|
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountId}`,
|
|
method: 'PUT',
|
|
body: requestBody,
|
|
})
|
|
return await RecurlyWrapper.promises._parseAccountXml(body)
|
|
}
|
|
|
|
const promises = {
|
|
_paypal: {
|
|
async checkAccountExists(cache) {
|
|
const { user } = cache
|
|
logger.debug(
|
|
{ userId: user._id },
|
|
'checking if recurly account exists for user'
|
|
)
|
|
let response, body
|
|
try {
|
|
;({ response, body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${user._id}`,
|
|
method: 'GET',
|
|
expect404: true,
|
|
}))
|
|
} catch (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while checking account',
|
|
{
|
|
user_id: user._id,
|
|
}
|
|
)
|
|
throw error
|
|
}
|
|
if (response.statusCode === 404) {
|
|
// actually not an error in this case, just no existing account
|
|
logger.debug(
|
|
{ userId: user._id },
|
|
'user does not currently exist in recurly, proceed'
|
|
)
|
|
cache.userExists = false
|
|
return cache
|
|
}
|
|
logger.debug({ userId: user._id }, 'user appears to exist in recurly')
|
|
try {
|
|
const account = await RecurlyWrapper.promises._parseAccountXml(body)
|
|
cache.userExists = true
|
|
cache.account = account
|
|
return cache
|
|
} catch (err) {
|
|
OError.tag(err, 'error parsing account', {
|
|
user_id: user._id,
|
|
})
|
|
throw err
|
|
}
|
|
},
|
|
async createAccount(cache) {
|
|
const { user } = cache
|
|
const { subscriptionDetails } = cache
|
|
if (cache.userExists) {
|
|
return cache
|
|
}
|
|
|
|
const address = getAddressFromSubscriptionDetails(
|
|
subscriptionDetails,
|
|
false
|
|
)
|
|
|
|
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) {
|
|
throw OError.tag(error, 'error building xml', { user_id: user._id })
|
|
}
|
|
|
|
let body
|
|
try {
|
|
;({ body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: 'accounts',
|
|
method: 'POST',
|
|
body: requestBody,
|
|
}))
|
|
} catch (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while creating account',
|
|
{ user_id: user._id }
|
|
)
|
|
throw error
|
|
}
|
|
try {
|
|
cache.account = await RecurlyWrapper.promises._parseAccountXml(body)
|
|
return cache
|
|
} catch (err) {
|
|
OError.tag(err, 'error creating account', {
|
|
user_id: user._id,
|
|
})
|
|
throw err
|
|
}
|
|
},
|
|
async createBillingInfo(cache) {
|
|
const { user } = cache
|
|
const { recurlyTokenIds } = cache
|
|
logger.debug({ userId: user._id }, 'creating billing info in recurly')
|
|
const accountCode = cache?.account?.account_code
|
|
if (!accountCode) {
|
|
throw 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) {
|
|
throw OError.tag(error, 'error building xml', { user_id: user._id })
|
|
}
|
|
let body
|
|
try {
|
|
;({ body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountCode}/billing_info`,
|
|
method: 'POST',
|
|
body: requestBody,
|
|
}))
|
|
} catch (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while creating billing info',
|
|
{ user_id: user._id }
|
|
)
|
|
throw error
|
|
}
|
|
try {
|
|
cache.billingInfo =
|
|
await RecurlyWrapper.promises._parseBillingInfoXml(body)
|
|
return cache
|
|
} catch (err) {
|
|
OError.tag(err, 'error creating billing info', {
|
|
user_id: user._id,
|
|
accountCode,
|
|
})
|
|
throw err
|
|
}
|
|
},
|
|
|
|
async setAddressAndCompanyBillingInfo(cache) {
|
|
const { user } = cache
|
|
const { subscriptionDetails } = cache
|
|
logger.debug(
|
|
{ userId: user._id },
|
|
'setting billing address and company info in recurly'
|
|
)
|
|
const accountCode = cache?.account?.account_code
|
|
if (!accountCode) {
|
|
throw new Error(
|
|
'no account code at setAddressAndCompanyBillingInfo stage'
|
|
)
|
|
}
|
|
|
|
const addressAndCompanyBillingInfo = getAddressFromSubscriptionDetails(
|
|
subscriptionDetails,
|
|
true
|
|
)
|
|
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml(
|
|
'billing_info',
|
|
addressAndCompanyBillingInfo
|
|
)
|
|
} catch (error) {
|
|
throw OError.tag(error, 'error building xml', { user_id: user._id })
|
|
}
|
|
|
|
let body
|
|
try {
|
|
;({ body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountCode}/billing_info`,
|
|
method: 'PUT',
|
|
body: requestBody,
|
|
}))
|
|
} catch (error) {
|
|
OError.tag(error, 'error response from recurly while setting address', {
|
|
user_id: user._id,
|
|
})
|
|
throw error
|
|
}
|
|
try {
|
|
cache.billingInfo =
|
|
await RecurlyWrapper.promises._parseBillingInfoXml(body)
|
|
return cache
|
|
} catch (err) {
|
|
if (err) {
|
|
OError.tag(err, 'error updating billing info', {
|
|
user_id: user._id,
|
|
})
|
|
throw err
|
|
}
|
|
}
|
|
},
|
|
async createSubscription(cache) {
|
|
const { user } = cache
|
|
const { subscriptionDetails } = cache
|
|
logger.debug({ userId: 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) {
|
|
throw OError.tag(error, 'error building xml', { user_id: user._id })
|
|
}
|
|
|
|
let body
|
|
try {
|
|
;({ body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: 'subscriptions',
|
|
method: 'POST',
|
|
body: requestBody,
|
|
}))
|
|
} catch (error) {
|
|
OError.tag(
|
|
error,
|
|
'error response from recurly while creating subscription',
|
|
{ user_id: user._id }
|
|
)
|
|
throw error
|
|
}
|
|
try {
|
|
cache.subscription =
|
|
await RecurlyWrapper.promises._parseSubscriptionXml(body)
|
|
return cache
|
|
} catch (err) {
|
|
OError.tag(err, 'error creating subscription', {
|
|
user_id: user._id,
|
|
})
|
|
throw err
|
|
}
|
|
},
|
|
},
|
|
|
|
async _createPaypalSubscription(user, subscriptionDetails, recurlyTokenIds) {
|
|
logger.debug(
|
|
{ userId: user._id },
|
|
'starting process of creating paypal subscription'
|
|
)
|
|
// We use waterfall through 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 }
|
|
let result
|
|
try {
|
|
result = await RecurlyWrapper.promises._paypal.checkAccountExists(cache)
|
|
result = await RecurlyWrapper.promises._paypal.createAccount(result)
|
|
result = await RecurlyWrapper.promises._paypal.createBillingInfo(result)
|
|
result =
|
|
await RecurlyWrapper.promises._paypal.setAddressAndCompanyBillingInfo(
|
|
result
|
|
)
|
|
result = await RecurlyWrapper.promises._paypal.createSubscription(result)
|
|
} catch (err) {
|
|
OError.tag(err, 'error in paypal subscription creation process', {
|
|
user_id: user._id,
|
|
})
|
|
throw err
|
|
}
|
|
if (!result.subscription) {
|
|
const err = new Error('no subscription object in result')
|
|
OError.tag(err, 'error in paypal subscription creation process', {
|
|
user_id: user._id,
|
|
})
|
|
throw err
|
|
}
|
|
logger.debug(
|
|
{ userId: user._id },
|
|
'done creating paypal subscription for user'
|
|
)
|
|
return result.subscription
|
|
},
|
|
|
|
async _createCreditCardSubscription(
|
|
user,
|
|
subscriptionDetails,
|
|
recurlyTokenIds
|
|
) {
|
|
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) {
|
|
throw OError.tag(error, 'error building xml', { user_id: user._id })
|
|
}
|
|
|
|
const { response, body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: 'subscriptions',
|
|
method: 'POST',
|
|
body: requestBody,
|
|
expect422: true,
|
|
})
|
|
|
|
if (response.statusCode === 422) {
|
|
return await RecurlyWrapper.promises._handle422Response(body)
|
|
} else {
|
|
return await RecurlyWrapper.promises._parseSubscriptionXml(body)
|
|
}
|
|
},
|
|
|
|
async createSubscription(user, subscriptionDetails, recurlyTokenIds) {
|
|
const { isPaypal } = subscriptionDetails
|
|
logger.debug(
|
|
{ userId: user._id, isPaypal },
|
|
'setting up subscription in recurly'
|
|
)
|
|
const fn = isPaypal
|
|
? RecurlyWrapper.promises._createPaypalSubscription
|
|
: RecurlyWrapper.promises._createCreditCardSubscription
|
|
return fn(user, subscriptionDetails, recurlyTokenIds)
|
|
},
|
|
|
|
/**
|
|
* @param options - the options to pass to the request library
|
|
* @returns {Promise<{ response: unknown, body: string}>}
|
|
*/
|
|
apiRequest(options) {
|
|
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
|
|
return new Promise((resolve, reject) => {
|
|
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)
|
|
) {
|
|
if (options.headers.Authorization) {
|
|
options.headers.Authorization = 'REDACTED'
|
|
}
|
|
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 }
|
|
)
|
|
reject(error)
|
|
}
|
|
resolve({ response, body })
|
|
})
|
|
})
|
|
},
|
|
|
|
async getSubscriptions(accountId) {
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountId}/subscriptions`,
|
|
})
|
|
return await RecurlyWrapper.promises._parseXml(body)
|
|
},
|
|
|
|
async getSubscription(subscriptionId, options) {
|
|
let url
|
|
if (!options) {
|
|
options = {}
|
|
}
|
|
|
|
if (options.recurlyJsResult) {
|
|
url = `recurly_js/result/${subscriptionId}`
|
|
} else {
|
|
url = `subscriptions/${subscriptionId}`
|
|
}
|
|
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url,
|
|
})
|
|
|
|
const recurlySubscription =
|
|
await RecurlyWrapper.promises._parseSubscriptionXml(body)
|
|
|
|
if (options.includeAccount) {
|
|
let accountId
|
|
if (recurlySubscription.account && recurlySubscription.account.url) {
|
|
accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
|
|
} else {
|
|
throw new Error("I don't understand the response from Recurly")
|
|
}
|
|
|
|
recurlySubscription.account =
|
|
await RecurlyWrapper.promises.getAccount(accountId)
|
|
|
|
return recurlySubscription
|
|
} else {
|
|
return recurlySubscription
|
|
}
|
|
},
|
|
|
|
async getPaginatedEndpoint(resource, queryParams) {
|
|
queryParams.per_page = queryParams.per_page || 200
|
|
let allItems = []
|
|
const getPage = async (cursor = null) => {
|
|
const opts = {
|
|
url: resource,
|
|
qs: queryParams,
|
|
}
|
|
if (cursor) {
|
|
opts.qs.cursor = cursor
|
|
}
|
|
const { response, body } = await RecurlyWrapper.promises.apiRequest(opts)
|
|
|
|
const data = await RecurlyWrapper.promises._parseXml(body)
|
|
|
|
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 {
|
|
return allItems
|
|
}
|
|
}
|
|
|
|
await getPage()
|
|
|
|
return allItems
|
|
},
|
|
|
|
async getAccount(accountId) {
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountId}`,
|
|
})
|
|
return await RecurlyWrapper.promises._parseAccountXml(body)
|
|
},
|
|
|
|
updateAccountEmailAddress,
|
|
|
|
async getAccountActiveCoupons(accountId) {
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountId}/redemptions`,
|
|
})
|
|
|
|
const redemptions = await RecurlyWrapper.promises._parseRedemptionsXml(body)
|
|
|
|
const activeRedemptions = redemptions.filter(
|
|
redemption => redemption.state === 'active'
|
|
)
|
|
const couponCodes = activeRedemptions.map(
|
|
redemption => redemption.coupon_code
|
|
)
|
|
|
|
return await Promise.all(
|
|
couponCodes.map(couponCode =>
|
|
RecurlyWrapper.promises.getCoupon(couponCode)
|
|
)
|
|
)
|
|
},
|
|
|
|
async getCoupon(couponCode) {
|
|
const opts = { url: `coupons/${couponCode}` }
|
|
const { body } = await RecurlyWrapper.promises.apiRequest(opts)
|
|
return await RecurlyWrapper.promises._parseCouponXml(body)
|
|
},
|
|
|
|
async getBillingInfo(accountId) {
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountId}/billing_info`,
|
|
})
|
|
return await RecurlyWrapper.promises._parseXml(body)
|
|
},
|
|
|
|
async getAccountPastDueInvoices(accountId) {
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountId}/invoices?state=past_due`,
|
|
})
|
|
return await RecurlyWrapper.promises._parseInvoicesXml(body)
|
|
},
|
|
|
|
async attemptInvoiceCollection(invoiceId) {
|
|
return await RecurlyWrapper.promises.apiRequest({
|
|
url: `invoices/${invoiceId}/collect`,
|
|
method: 'put',
|
|
})
|
|
},
|
|
|
|
async updateSubscription(subscriptionId, options) {
|
|
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) {
|
|
throw OError.tag(error, 'error building xml', { subscriptionId })
|
|
}
|
|
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `subscriptions/${subscriptionId}`,
|
|
method: 'put',
|
|
body: requestBody,
|
|
})
|
|
return await RecurlyWrapper.promises._parseSubscriptionXml(body)
|
|
},
|
|
|
|
async createFixedAmmountCoupon(
|
|
couponCode,
|
|
name,
|
|
currencyCode,
|
|
discountInCents,
|
|
planCode
|
|
) {
|
|
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) {
|
|
throw OError.tag(error, 'error building xml', {
|
|
couponCode,
|
|
name,
|
|
})
|
|
}
|
|
|
|
logger.debug({ couponCode, requestBody }, 'creating coupon')
|
|
try {
|
|
await RecurlyWrapper.promises.apiRequest({
|
|
url: 'coupons',
|
|
method: 'post',
|
|
body: requestBody,
|
|
})
|
|
} catch (error) {
|
|
logger.warn({ err: error, couponCode }, 'error creating coupon')
|
|
throw error
|
|
}
|
|
},
|
|
|
|
async lookupCoupon(couponCode) {
|
|
const { body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `coupons/${couponCode}`,
|
|
})
|
|
return await RecurlyWrapper.promises._parseCouponXml(body)
|
|
},
|
|
|
|
async redeemCoupon(accountCode, couponCode) {
|
|
const data = {
|
|
account_code: accountCode,
|
|
currency: 'USD',
|
|
}
|
|
let requestBody
|
|
try {
|
|
requestBody = RecurlyWrapper._buildXml('redemption', data)
|
|
} catch (error) {
|
|
throw OError.tag(error, 'error building xml', {
|
|
accountCode,
|
|
couponCode,
|
|
})
|
|
}
|
|
|
|
logger.debug(
|
|
{ accountCode, couponCode, requestBody },
|
|
'redeeming coupon for user'
|
|
)
|
|
try {
|
|
await RecurlyWrapper.promises.apiRequest({
|
|
url: `coupons/${couponCode}/redeem`,
|
|
method: 'post',
|
|
body: requestBody,
|
|
})
|
|
} catch (error) {
|
|
logger.warn(
|
|
{ err: error, accountCode, couponCode },
|
|
'error redeeming coupon'
|
|
)
|
|
throw error
|
|
}
|
|
},
|
|
|
|
async extendTrial(subscriptionId, daysUntilExpire) {
|
|
if (daysUntilExpire == null) {
|
|
daysUntilExpire = 7
|
|
}
|
|
const nextRenewalDate = new Date()
|
|
nextRenewalDate.setDate(nextRenewalDate.getDate() + daysUntilExpire)
|
|
logger.debug(
|
|
{ subscriptionId, daysUntilExpire },
|
|
'Exending Free trial for user'
|
|
)
|
|
try {
|
|
await RecurlyWrapper.promises.apiRequest({
|
|
url: `/subscriptions/${subscriptionId}/postpone?next_bill_date=${nextRenewalDate}&bulk=false`,
|
|
method: 'put',
|
|
})
|
|
} catch (error) {
|
|
logger.warn(
|
|
{ err: error, subscriptionId, daysUntilExpire },
|
|
'error exending trial'
|
|
)
|
|
throw error
|
|
}
|
|
},
|
|
|
|
async listAccountActiveSubscriptions(accountId) {
|
|
const { response, body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountId}/subscriptions`,
|
|
qs: {
|
|
state: 'active',
|
|
},
|
|
expect404: true,
|
|
})
|
|
if (response.statusCode === 404) {
|
|
return []
|
|
} else {
|
|
return await RecurlyWrapper.promises._parseSubscriptionsXml(body)
|
|
}
|
|
},
|
|
|
|
async _handle422Response(body) {
|
|
const data = await RecurlyWrapper.promises._parseErrorsXml(body)
|
|
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._,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
throw new SubscriptionErrors.RecurlyTransactionError(errorData)
|
|
},
|
|
|
|
async _parseSubscriptionsXml(xml) {
|
|
return await RecurlyWrapper.promises._parseXmlAndGetAttribute(
|
|
xml,
|
|
'subscriptions'
|
|
)
|
|
},
|
|
async _parseSubscriptionXml(xml) {
|
|
return await RecurlyWrapper.promises._parseXmlAndGetAttribute(
|
|
xml,
|
|
'subscription'
|
|
)
|
|
},
|
|
async _parseAccountXml(xml) {
|
|
return await RecurlyWrapper.promises._parseXmlAndGetAttribute(
|
|
xml,
|
|
'account'
|
|
)
|
|
},
|
|
async _parseBillingInfoXml(xml) {
|
|
return await RecurlyWrapper.promises._parseXmlAndGetAttribute(
|
|
xml,
|
|
'billing_info'
|
|
)
|
|
},
|
|
async _parseRedemptionsXml(xml) {
|
|
return await RecurlyWrapper.promises._parseXmlAndGetAttribute(
|
|
xml,
|
|
'redemptions'
|
|
)
|
|
},
|
|
async _parseCouponXml(xml) {
|
|
return await RecurlyWrapper.promises._parseXmlAndGetAttribute(xml, 'coupon')
|
|
},
|
|
async _parseErrorsXml(xml) {
|
|
return await RecurlyWrapper.promises._parseXmlAndGetAttribute(xml, 'errors')
|
|
},
|
|
async _parseInvoicesXml(xml) {
|
|
return await RecurlyWrapper.promises._parseXmlAndGetAttribute(
|
|
xml,
|
|
'invoices'
|
|
)
|
|
},
|
|
|
|
async _parseXmlAndGetAttribute(xml, attribute) {
|
|
const data = await RecurlyWrapper.promises._parseXml(xml)
|
|
if (data && data[attribute] != null) {
|
|
return data[attribute]
|
|
} else {
|
|
throw new Error("I don't understand the response from Recurly")
|
|
}
|
|
},
|
|
|
|
_parseXml(xml) {
|
|
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: '',
|
|
})
|
|
return new Promise((resolve, reject) =>
|
|
parser.parseString(xml, function (error, data) {
|
|
if (error) {
|
|
return reject(error)
|
|
}
|
|
const result = convertDataTypes(data)
|
|
resolve(result)
|
|
})
|
|
)
|
|
},
|
|
}
|
|
|
|
function _buildXml(rootName, data) {
|
|
const options = {
|
|
headless: true,
|
|
renderOpts: {
|
|
pretty: true,
|
|
indent: '\t',
|
|
},
|
|
rootName,
|
|
}
|
|
const builder = new xml2js.Builder(options)
|
|
return builder.buildObject(data)
|
|
}
|
|
|
|
const RecurlyWrapper = {
|
|
apiUrl: Settings.apis.recurly.url || 'https://api.recurly.com/v2',
|
|
_buildXml,
|
|
_parseXml: callbackify(promises._parseXml),
|
|
// This one needs to be callbackified manually because we need to transform {response, body} to (err, response, body)
|
|
attemptInvoiceCollection: (invoiceId, callback) => {
|
|
promises
|
|
.attemptInvoiceCollection(invoiceId)
|
|
.then(({ response, body }) => callback(null, response, body))
|
|
.catch(err => callback(err))
|
|
},
|
|
createFixedAmmountCoupon: callbackify(promises.createFixedAmmountCoupon),
|
|
getAccountActiveCoupons: callbackify(promises.getAccountActiveCoupons),
|
|
getBillingInfo: callbackify(promises.getBillingInfo),
|
|
getPaginatedEndpoint: callbackify(promises.getPaginatedEndpoint),
|
|
getSubscription: callbackify(promises.getSubscription),
|
|
getSubscriptions: callbackify(promises.getSubscriptions),
|
|
updateAccountEmailAddress: callbackify(promises.updateAccountEmailAddress),
|
|
}
|
|
|
|
RecurlyWrapper.promises = {
|
|
...promises,
|
|
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,
|
|
})
|
|
}
|
|
if (subscriptionDetails.ITMReferrer) {
|
|
customFields.push({
|
|
name: 'itm_referrer',
|
|
value: subscriptionDetails.ITMReferrer,
|
|
})
|
|
}
|
|
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
|
|
}
|