mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-06 23:59:01 +02:00
f2f238be99
Support new group plan in the redesigned checkout page GitOrigin-RevId: 2564477b36ee93ca4b4b6dd8210f140a3104c6ed
991 lines
27 KiB
JavaScript
991 lines
27 KiB
JavaScript
const OError = require('@overleaf/o-error')
|
|
const {
|
|
fetchStringWithResponse,
|
|
RequestFailedError,
|
|
} = require('@overleaf/fetch-utils')
|
|
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')
|
|
|
|
/**
|
|
* @param accountId
|
|
* @param newEmail
|
|
*/
|
|
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.status === 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,
|
|
},
|
|
}
|
|
if (subscriptionDetails.subscription_add_ons) {
|
|
data.subscription_add_ons = subscriptionDetails.subscription_add_ons
|
|
}
|
|
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
|
|
}
|
|
if (subscriptionDetails.subscription_add_ons) {
|
|
data.subscription_add_ons = subscriptionDetails.subscription_add_ons
|
|
}
|
|
|
|
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.status === 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}>}
|
|
*/
|
|
async apiRequest({ expect404, expect422, url, qs, ...fetchOptions }) {
|
|
const fetchUrl = new URL(RecurlyWrapper.apiUrl)
|
|
fetchUrl.pathname =
|
|
fetchUrl.pathname !== '/' ? `${fetchUrl.pathname}/${url}` : url
|
|
|
|
if (qs) {
|
|
for (const [key, value] of Object.entries(qs)) {
|
|
fetchUrl.searchParams.set(key, value)
|
|
}
|
|
}
|
|
fetchOptions.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,
|
|
}
|
|
|
|
try {
|
|
return await fetchStringWithResponse(fetchUrl, fetchOptions)
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
if (error.response.status === 404 && expect404) {
|
|
return { response: error.response, body: null }
|
|
} else if (error.response.status === 422 && expect422) {
|
|
return { response: error.response, body: error.body }
|
|
}
|
|
|
|
if (fetchOptions.headers.Authorization) {
|
|
fetchOptions.headers.Authorization = 'REDACTED'
|
|
}
|
|
logger.warn(
|
|
{
|
|
err: error,
|
|
body: error.body,
|
|
options: fetchOptions,
|
|
url: fetchUrl.href,
|
|
statusCode: error.response?.status,
|
|
},
|
|
'error returned from recurly'
|
|
)
|
|
throw new OError(
|
|
`Recurly API returned with status code: ${error.response.status}`,
|
|
{ statusCode: error.response.status }
|
|
)
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
},
|
|
|
|
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
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @typedef {{getNextPage: () => Promise<PageData>, items: any[]}} PageData
|
|
*/
|
|
|
|
async getPaginatedEndpoint(resource, queryParams) {
|
|
let allItems = []
|
|
let items
|
|
|
|
/** @type {() => Promise<PageData>} */
|
|
let getNextPage = promises.getPaginatedEndpointIterator(
|
|
resource,
|
|
queryParams
|
|
)
|
|
while (getNextPage) {
|
|
;({ items, getNextPage } = await getNextPage())
|
|
allItems = allItems.concat(items)
|
|
logger.debug(`total now ${allItems.length}`)
|
|
}
|
|
return allItems
|
|
},
|
|
|
|
/**
|
|
* @returns {() => Promise<PageData>}
|
|
*/
|
|
getPaginatedEndpointIterator(resource, queryParams) {
|
|
queryParams.per_page = queryParams.per_page || 200
|
|
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]
|
|
logger.debug(`got ${items.length} items in this page`)
|
|
const match = response.headers.link?.match(/cursor=([0-9.]+%3A[0-9.]+)&/)
|
|
const nextCursor = match && match[1]
|
|
return {
|
|
items,
|
|
getNextPage:
|
|
nextCursor && (() => getPage(decodeURIComponent(nextCursor))),
|
|
}
|
|
}
|
|
return getPage
|
|
},
|
|
|
|
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`,
|
|
qs: { 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 createFixedAmountCoupon(
|
|
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`,
|
|
qs: { bulk: false, next_bill_date: nextRenewalDate },
|
|
method: 'PUT',
|
|
})
|
|
} catch (error) {
|
|
logger.warn(
|
|
{ err: error, subscriptionId, daysUntilExpire },
|
|
'error extending trial'
|
|
)
|
|
throw error
|
|
}
|
|
},
|
|
|
|
async listAccountActiveSubscriptions(accountId) {
|
|
const { response, body } = await RecurlyWrapper.promises.apiRequest({
|
|
url: `accounts/${accountId}/subscriptions`,
|
|
qs: {
|
|
state: 'active',
|
|
},
|
|
expect404: true,
|
|
})
|
|
if (response.status === 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")
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param xml
|
|
*/
|
|
_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),
|
|
createFixedAmountCoupon: callbackify(promises.createFixedAmountCoupon),
|
|
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
|
|
}
|