diff --git a/services/web/app/src/Features/Subscription/RecurlyWrapper.js b/services/web/app/src/Features/Subscription/RecurlyWrapper.js
index df5ce05ea2..9fabfa42e0 100644
--- a/services/web/app/src/Features/Subscription/RecurlyWrapper.js
+++ b/services/web/app/src/Features/Subscription/RecurlyWrapper.js
@@ -32,31 +32,6 @@ module.exports = RecurlyWrapper = {
x => x.url
) || 'https://api.recurly.com/v2',
- _addressToXml(address) {
- const allowedKeys = [
- 'address1',
- 'address2',
- 'city',
- 'country',
- 'state',
- 'zip',
- 'postal_code'
- ]
- let resultString = '\n'
- for (let k in address) {
- const v = address[k]
- if (k === 'postal_code') {
- k = 'zip'
- }
- if (v && Array.from(allowedKeys).includes(k)) {
- resultString += `<${k}${k === 'address2' ? ' nil="nil"' : ''}>${v ||
- ''}${k}>\n`
- }
- }
- resultString += '\n'
- return resultString
- },
-
_paypal: {
checkAccountExists(cache, next) {
const { user } = cache
@@ -132,22 +107,22 @@ module.exports = RecurlyWrapper = {
{ user_id: user._id, recurly_token_id },
'creating user in recurly'
)
- const requestBody = `\
-
- ${user._id}
- ${user.email}
- ${user.first_name}
- ${user.last_name}
-
- ${address.address1}
- ${address.address2}
- ${address.city || ''}
- ${address.state || ''}
- ${address.zip || ''}
- ${address.country}
-
-\
-`
+ const data = {
+ account_code: user._id,
+ email: user.email,
+ first_name: user.first_name,
+ last_name: user.last_name,
+ address: {
+ address1: address.address1,
+ address2: address.address2 || '',
+ city: address.city || '',
+ state: address.state || '',
+ zip: address.zip || '',
+ country: address.country
+ }
+ }
+ const requestBody = RecurlyWrapper._buildXml('account', data)
+
return RecurlyWrapper.apiRequest(
{
url: 'accounts',
@@ -194,11 +169,8 @@ module.exports = RecurlyWrapper = {
if (!accountCode) {
return next(new Error('no account code at createBillingInfo stage'))
}
- const requestBody = `\
-
- ${recurly_token_id}
-\
-`
+ const data = { token_id: recurly_token_id }
+ const requestBody = RecurlyWrapper._buildXml('billing_info', data)
return RecurlyWrapper.apiRequest(
{
url: `accounts/${accountCode}/billing_info`,
@@ -252,7 +224,16 @@ module.exports = RecurlyWrapper = {
new Error('no address in subscriptionDetails at setAddress stage')
)
}
- const requestBody = RecurlyWrapper._addressToXml(address)
+ const data = {
+ address1: address.address1,
+ address2: address.address2 || '',
+ city: address.city || '',
+ state: address.state || '',
+ zip: address.zip || '',
+ country: address.country
+ }
+ const requestBody = RecurlyWrapper._buildXml('billing_info', data)
+
return RecurlyWrapper.apiRequest(
{
url: `accounts/${accountCode}/billing_info`,
@@ -292,16 +273,16 @@ module.exports = RecurlyWrapper = {
{ user_id: user._id, recurly_token_id },
'creating subscription in recurly'
)
- const requestBody = `\
-
- ${subscriptionDetails.plan_code}
- ${subscriptionDetails.currencyCode}
- ${subscriptionDetails.coupon_code}
-
- ${user._id}
-
-\
-` // TODO: check account details and billing
+ const data = {
+ plan_code: subscriptionDetails.plan_code,
+ currency: subscriptionDetails.currencyCode,
+ coupon_code: subscriptionDetails.coupon_code,
+ account: {
+ account_code: user._id
+ }
+ }
+ const requestBody = RecurlyWrapper._buildXml('subscription', data)
+
return RecurlyWrapper.apiRequest(
{
url: 'subscriptions',
@@ -388,22 +369,22 @@ module.exports = RecurlyWrapper = {
recurly_token_id,
callback
) {
- const requestBody = `\
-
- ${subscriptionDetails.plan_code}
- ${subscriptionDetails.currencyCode}
- ${subscriptionDetails.coupon_code}
-
- ${user._id}
- ${user.email}
- ${user.first_name}
- ${user.last_name}
-
- ${recurly_token_id}
-
-
-\
-`
+ 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: user.first_name,
+ last_name: user.last_name,
+ billing_info: {
+ token_id: recurly_token_id
+ }
+ }
+ }
+ const requestBody = RecurlyWrapper._buildXml('subscription', data)
+
return RecurlyWrapper.apiRequest(
{
url: 'subscriptions',
@@ -667,12 +648,12 @@ module.exports = RecurlyWrapper = {
{ subscriptionId, options },
'telling recurly to update subscription'
)
- const requestBody = `\
-
- ${options.plan_code}
- ${options.timeframe}
-\
-`
+ const data = {
+ plan_code: options.plan_code,
+ timeframe: options.timeframe
+ }
+ const requestBody = RecurlyWrapper._buildXml('subscription', data)
+
return RecurlyWrapper.apiRequest(
{
url: `subscriptions/${subscriptionId}`,
@@ -696,20 +677,19 @@ module.exports = RecurlyWrapper = {
plan_code,
callback
) {
- const requestBody = `\
-
- ${coupon_code}
- ${name}
- dollars
-
- <${currencyCode}>${discount_in_cents}${currencyCode}>
-
-
- ${plan_code}
-
- false
-\
-`
+ const data = {
+ coupon_code,
+ name,
+ discount_type: 'dollars',
+ discount_in_cents: {},
+ plan_codes: {
+ plan_code
+ },
+ applies_to_all_plans: false
+ }
+ data.discount_in_cents[currencyCode] = discount_in_cents
+ const requestBody = RecurlyWrapper._buildXml('coupon', data)
+
logger.log({ coupon_code, requestBody }, 'creating coupon')
return RecurlyWrapper.apiRequest(
{
@@ -787,12 +767,12 @@ module.exports = RecurlyWrapper = {
},
redeemCoupon(account_code, coupon_code, callback) {
- const requestBody = `\
-
- ${account_code}
- USD
-\
-`
+ const data = {
+ account_code,
+ currency: 'USD'
+ }
+ const requestBody = RecurlyWrapper._buildXml('redemption', data)
+
logger.log(
{ account_code, coupon_code, requestBody },
'redeeming coupon for user'
@@ -969,6 +949,19 @@ module.exports = RecurlyWrapper = {
const result = convertDataTypes(data)
return 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)
}
}
diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json
index 912742f9e7..113139e251 100644
--- a/services/web/npm-shrinkwrap.json
+++ b/services/web/npm-shrinkwrap.json
@@ -20240,11 +20240,19 @@
}
},
"xml2js": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.0.tgz",
- "integrity": "sha1-99pSJ33rtkeYMFOtti2XLe5loaw=",
+ "version": "0.4.19",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
+ "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
- "sax": ">=0.1.1"
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~9.0.1"
+ },
+ "dependencies": {
+ "xmlbuilder": {
+ "version": "9.0.7",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+ "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
+ }
}
},
"xmlbuilder": {
diff --git a/services/web/package.json b/services/web/package.json
index 2f3721b384..dee161b35c 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -108,7 +108,7 @@
"uuid": "^3.0.1",
"v8-profiler-node8": "^6.0.1",
"valid-url": "^1.0.9",
- "xml2js": "0.2.0",
+ "xml2js": "^0.4.19",
"xregexp": "^4.2.4",
"yauzl": "^2.10.0"
},
diff --git a/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js b/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js
index 7346db8f0f..cf454475ca 100644
--- a/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js
+++ b/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js
@@ -151,7 +151,8 @@ describe('RecurlyWrapper', function() {
error: sinon.stub(),
log: sinon.stub()
},
- request: sinon.stub()
+ request: sinon.stub(),
+ xml2js: require('xml2js')
}
}
))
@@ -280,12 +281,13 @@ describe('RecurlyWrapper', function() {
return this.RecurlyWrapper.apiRequest.restore()
})
- it('should send an update request to the API', function() {
+ it('sends correct XML', function() {
this.apiRequest.called.should.equal(true)
- this.requestOptions.body.should.equal(`\
+ const { body } = this.apiRequest.lastCall.args[0]
+ expect(body).to.equal(`\
- silver
- now
+ silver
+ now
\
`)
this.requestOptions.url.should.equal(
@@ -395,10 +397,6 @@ describe('RecurlyWrapper', function() {
'apiRequest',
(options, callback) => {
options.url.should.equal(`coupons/${this.coupon_code}/redeem`)
- options.body
- .indexOf(`${this.recurlyAccountId}`)
- .should.not.equal(-1)
- options.body.indexOf('USD').should.not.equal(-1)
options.method.should.equal('post')
return callback()
}
@@ -414,37 +412,63 @@ describe('RecurlyWrapper', function() {
return this.RecurlyWrapper.apiRequest.restore()
})
- return it('should send the request to redem the coupon', function() {
- return this.apiRequest.called.should.equal(true)
+ return it('sends correct XML', function() {
+ this.apiRequest.called.should.equal(true)
+ const { body } = this.apiRequest.lastCall.args[0]
+ return expect(body).to.equal(`\
+
+ account-id-123
+ USD
+\
+`)
})
})
- describe('_addressToXml', function() {
- beforeEach(function() {
- return (this.address = {
- address1: 'addr_one',
- address2: 'addr_two',
- country: 'some_country',
- state: 'some_state',
- postal_code: 'some_zip',
- nonsenseKey: 'rubbish'
- })
+ describe('createFixedAmmountCoupon', function() {
+ beforeEach(function(done) {
+ this.couponCode = 'a-coupon-code'
+ this.couponName = 'a-coupon-name'
+ this.currencyCode = 'EUR'
+ this.discount = 1337
+ this.planCode = 'a-plan-code'
+ this.apiRequest = sinon.stub(
+ this.RecurlyWrapper,
+ 'apiRequest',
+ (options, callback) => {
+ return callback()
+ }
+ )
+ return this.RecurlyWrapper.createFixedAmmountCoupon(
+ this.couponCode,
+ this.couponName,
+ this.currencyCode,
+ this.discount,
+ this.planCode,
+ done
+ )
})
- return it('should generate the correct xml', function() {
- const result = this.RecurlyWrapper._addressToXml(this.address)
- return should.equal(
- result,
- `\
-
-addr_one
-addr_two
-some_country
-some_state
-some_zip
-\n\
-`
- )
+ afterEach(function() {
+ return this.RecurlyWrapper.apiRequest.restore()
+ })
+
+ return it('sends correct XML', function() {
+ this.apiRequest.called.should.equal(true)
+ const { body } = this.apiRequest.lastCall.args[0]
+ return expect(body).to.equal(`\
+
+ a-coupon-code
+ a-coupon-name
+ dollars
+
+ 1337
+
+
+ a-plan-code
+
+ false
+\
+`)
})
})
@@ -639,6 +663,29 @@ describe('RecurlyWrapper', function() {
return this._parseSubscriptionXml.restore()
})
+ it('sends correct XML', function(done) {
+ return this.call((err, result) => {
+ const { body } = this.apiRequest.lastCall.args[0]
+ expect(body).to.equal(`\
+
+ some_plan_code
+ EUR
+
+
+ some_id
+ user@example.com
+
+
+
+ a-token-id
+
+
+\
+`)
+ return done()
+ })
+ })
+
it('should not produce an error', function(done) {
return this.call((err, sub) => {
expect(err).to.not.be.instanceof(Error)
@@ -875,7 +922,12 @@ describe('RecurlyWrapper', function() {
'_parseSubscriptionXml'
)
return (this.cache = {
- user: (this.user = { _id: 'some_id' }),
+ user: (this.user = {
+ _id: 'some_id',
+ email: 'foo@bar.com',
+ first_name: 'Foo',
+ last_name: 'Bar'
+ }),
recurly_token_id: (this.recurly_token_id = 'some_token'),
subscriptionDetails: (this.subscriptionDetails = {
currencyCode: 'EUR',
@@ -885,6 +937,7 @@ describe('RecurlyWrapper', function() {
address: {
address1: 'addr_one',
address2: 'addr_two',
+ city: 'some_city',
country: 'some_country',
state: 'some_state',
zip: 'some_zip'
@@ -1091,6 +1144,29 @@ describe('RecurlyWrapper', function() {
)
})
+ it('sends correct XML', function(done) {
+ return this.call((err, result) => {
+ const { body } = this.apiRequest.lastCall.args[0]
+ expect(body).to.equal(`\
+
+ some_id
+ foo@bar.com
+ Foo
+ Bar
+
+ addr_one
+ addr_two
+ some_city
+ some_state
+ some_zip
+ some_country
+
+\
+`)
+ return done()
+ })
+ })
+
it('should not produce an error', function(done) {
return this.call((err, result) => {
expect(err).to.not.be.instanceof(Error)
@@ -1165,6 +1241,18 @@ describe('RecurlyWrapper', function() {
)
})
+ it('sends correct XML', function(done) {
+ return this.call((err, result) => {
+ const { body } = this.apiRequest.lastCall.args[0]
+ expect(body).to.equal(`\
+
+ some_token
+\
+`)
+ return done()
+ })
+ })
+
it('should not produce an error', function(done) {
return this.call((err, result) => {
expect(err).to.not.be.instanceof(Error)
@@ -1259,6 +1347,23 @@ describe('RecurlyWrapper', function() {
)
})
+ it('sends correct XML', function(done) {
+ return this.call((err, result) => {
+ const { body } = this.apiRequest.lastCall.args[0]
+ expect(body).to.equal(`\
+
+ addr_one
+ addr_two
+ some_city
+ some_state
+ some_zip
+ some_country
+\
+`)
+ return done()
+ })
+ })
+
it('should not produce an error', function(done) {
return this.call((err, result) => {
expect(err).to.not.be.instanceof(Error)
@@ -1330,6 +1435,23 @@ describe('RecurlyWrapper', function() {
)
})
+ it('sends correct XML', function(done) {
+ return this.call((err, result) => {
+ const { body } = this.apiRequest.lastCall.args[0]
+ expect(body).to.equal(`\
+
+ some_plan_code
+ EUR
+
+
+ some_id
+
+\
+`)
+ return done()
+ })
+ })
+
it('should not produce an error', function(done) {
return this.call((err, result) => {
expect(err).to.not.be.instanceof(Error)