UNPKG

recurring

Version:

a recurly v2 api client for node.js

936 lines (838 loc) 26.5 kB
'use strict' const demand = require('must') const Recurring = require('../lib/recurly') const uuid = require('uuid') const debug = require('debug')('recurring:test') const _ = require('lodash') // This recurly account is an empty test account connected to their // development gateway. const config = { apikey: '88ac57c6891440bda9ba28b6b9c18857', plan_code: 'recurring-test', subdomain: 'recurring-test' } const recurly = new Recurring() let plan let account let subscription let freshAccountId before(() => { recurly.setAPIKey(config.apikey) }) describe('Plan', () => { let cached // create a plan. it('can create a plan', done => { const planId = uuid.v4() const data = { plan_code: `testplan${planId}`, name: `Test Plan ${planId}`, unit_amount_in_cents: { USD: 1000 } } recurly.Plan().create(data, (err, newPlan) => { demand(err).not.exist() newPlan.must.be.an.object() newPlan.id.must.equal(data.plan_code) newPlan.name.must.equal(data.name) done() }) }) it('cannot create a plan without a plan code', done => { const planId = uuid.v4() const wrong = () => { const inadequate = { name: `Test Plan ${planId}`, unit_amount_in_cents: { USD: 1000 } } recurly.Plan().create(inadequate, _.noop) } wrong.must.throw(Error) done() }) it('cannot create a plan without a plan name', done => { const planId = uuid.v4() const wrong = () => { const inadequate = { plan_code: `testplan${planId}`, unit_amount_in_cents: { USD: 1000 } } recurly.Plan().create(inadequate, _.noop) } wrong.must.throw(Error) done() }) it('cannot create a plan without a plan unit amount in cents', done => { const planId = uuid.v4() const wrong = () => { const inadequate = { plan_code: `testplan${planId}`, name: `Test Plan ${planId}` } recurly.Plan().create(inadequate, _.noop) } wrong.must.throw(Error) done() }) it('can fetch all plans from the test Recurly account', done => { recurly.Plan().all((err, plans) => { demand(err).not.exist() plans.must.be.an.object() const planCodes = Object.keys(plans) planCodes.length.must.be.above(0) planCodes[0].must.not.equal('undefined') cached = planCodes done() }) }) it('can fetch a single plan', done => { plan = recurly.Plan() plan.id = cached[0] plan.fetch(err => { demand(err).not.exist() plan.href.length.must.be.above(0) plan.must.have.property('name') plan.must.have.property('description') plan.name.must.exist() plan.description.must.exist() done() }) }) }) describe('Account', () => { it('can create an account', done => { freshAccountId = uuid.v4() const data = { id: freshAccountId, email: 'test@example.com', first_name: 'John', last_name: 'Whorfin', company_name: 'Yoyodyne Propulsion Systems' } recurly.Account().create(data, (err, newAccount) => { demand(err).not.exist() newAccount.must.be.an.object() newAccount.id.must.equal(freshAccountId) newAccount.company_name.must.equal('Yoyodyne Propulsion Systems') done() }) }) it('can close an account', done => { account = recurly.Account() account.id = freshAccountId account.close((err, closed) => { demand(err).not.exist() closed.must.equal(true) done() }) }) it('can create or reopen a previously-closed account, transparently', done => { const data = { id: freshAccountId } recurly.Account().create(data, (err, newAccount) => { demand(err).not.exist() newAccount.must.be.an.object() newAccount.first_name.must.equal('John') // from old data newAccount.last_name.must.equal('Whorfin') // from old data done() }) }) it('can fetch a single account', done => { account = recurly.Account() account.id = freshAccountId account.fetch(err => { demand(err).not.exist() account.must.be.an.object() account.email.must.equal('test@example.com') account.company_name.must.equal('Yoyodyne Propulsion Systems') done() }) }) it('can fetch all accounts from the test Recurly account', done => { recurly.Account().all((err, accounts) => { demand(err).not.exist() accounts.must.be.an.object() const uuids = Object.keys(accounts) uuids.length.must.be.above(0) uuids[0].must.not.equal('undefined') done() }) }) it('can update an account', done => { account.company_name = 'Yoyodyne Propulsion, International' account.update((err, updated) => { demand(err).not.exist() updated.must.be.an.object() const testAcc = recurly.Account() testAcc.id = account.id testAcc.fetch(err => { demand(err).not.exist() testAcc.company_name.must.equal(account.company_name) done() }) }) }) }) describe('BillingInfo', () => { let binfo // before(done => { // freshAccountId = uuid.v4() // const data = { // id: freshAccountId, // email: 'test@example.com', // first_name: 'John', // last_name: 'Whorfin', // company_name: 'Yoyodyne Propulsion Systems' // } // recurly.Account().create(data, (err, newAccount) => { // demand(err).not.exist() // account = newAccount // done() // }) // }) it('can not retrieve billing info for an account that does not exist', done => { const data = { id: uuid.v4(), email: 'test2@example.com', first_name: 'John', last_name: 'Smallberries', company_name: 'Yoyodyne Propulsion Systems' } recurly.Account().create(data, (err, newAccount) => { demand(err).not.exist() newAccount.fetchBillingInfo((err, info) => { debug('Got error: %o', err) err.must.exist() err.error_code.must.equal('not_found') demand(info).not.exist() done() }) }) }) it('can add billing info to an account and skip card authorization', done => { binfo = recurly.BillingInfo() binfo.account_code = freshAccountId binfo.skipAuthorization = true const billingData = { first_name: account.first_name, last_name: account.last_name, number: '4000-0000-0000-0077', month: 1, year: (new Date()).getFullYear() + 3, verification_value: '111', address1: '760 Market Street', address2: 'Suite 500', city: 'San Francisco', state: 'CA', country: 'USA', zip: '94102' } binfo.update(billingData, err => { demand(err).not.exist() binfo.last_four.must.equal('0077') done() }) }) it('can add billing info to an account', done => { binfo = recurly.BillingInfo() binfo.account_code = freshAccountId const billingData = { first_name: account.first_name, last_name: account.last_name, number: '4111-1111-1111-1111', month: 1, year: (new Date()).getFullYear() + 3, verification_value: '111', address1: '760 Market Street', address2: 'Suite 500', city: 'San Francisco', state: 'CA', country: 'USA', zip: '94102' } binfo.update(billingData, err => { demand(err).not.exist() binfo.last_four.must.equal('1111') done() }) }) it('throws an error when missing a required billing data field', done => { const binfo2 = recurly.BillingInfo() binfo2.account_code = freshAccountId const wrong = () => { const inadequate = { first_name: account.first_name, last_name: account.last_name } binfo2.update(inadequate, _.noop) } wrong.must.throw(Error) done() }) it('can fetch the billing info for an account', done => { account.fetchBillingInfo((err, info) => { demand(err).not.exist() info.first_name.must.equal(account.first_name) info.last_four.must.equal('1111') info.address2.must.be.a.string() done() }) }) }) describe('Subscription', () => { // let account let cached // before(done => { // freshAccountId = uuid.v4() // const data = { // id: freshAccountId, // email: 'test@example.com', // first_name: 'John', // last_name: 'Whorfin', // company_name: 'Yoyodyne Propulsion Systems' // } // recurly.Account().create(data, (err, newAccount) => { // demand(err).not.exist() // account = newAccount // // const binfo = recurly.BillingInfo() // binfo.account_code = freshAccountId // const billingData = { // first_name: account.first_name, // last_name: account.last_name, // number: '4111-1111-1111-1111', // month: 1, // year: (new Date()).getFullYear() + 3, // verification_value: '111', // address1: '760 Market Street', // address2: 'Suite 500', // city: 'San Francisco', // state: 'CA', // country: 'USA', // zip: '94102' // } // // binfo.update(billingData, err => { // demand(err).not.exist() // binfo.last_four.must.equal('1111') // done() // }) // }) // }) it('can create a subscription for an account', done => { const data = { plan_code: config.plan_code, account: { account_code: account.id }, currency: 'USD', quantity: 10 } recurly.Subscription().create(data, (err, newsub) => { demand(err).not.exist() newsub.id.must.exist() newsub.quantity.must.equal(10) newsub.plan.must.be.an.object() newsub.plan.plan_code.must.equal(config.plan_code) subscription = newsub done() }) }) it('can fetch all subscriptions associated with an account', done => { account.fetchSubscriptions((err, subscriptions) => { demand(err).not.exist() subscriptions.must.be.an.array() cached = subscriptions done() }) }) it('can fetch a single subscription', done => { const uuid = cached[0].uuid subscription = recurly.Subscription() subscription.id = uuid subscription.fetch(err => { demand(err).not.exist() subscription.must.have.property('_resources') subscription._resources.must.be.an.object() subscription._resources.must.have.property('account') subscription.account_id.must.equal(account.id) done() }) }) it('throws an error when attempting to modify a subscription without a timeframe', done => { const wrong = () => { subscription.update({ inadequate: true }, _.noop) } wrong.must.throw(Error) done() }) it('can modify a subscription', done => { const mods = { timeframe: 'now', quantity: subscription.quantity + 3 } subscription.update(mods, err => { demand(err).not.exist() subscription.must.be.an.object() subscription.quantity.must.equal(mods.quantity) done() }) }) it('can cancel a subscription', done => { subscription.cancel(err => { demand(err).not.exist() subscription.state.must.equal('canceled') subscription.canceled_at.must.be.a.date() subscription.expires_at.must.be.a.date() // in the future, even done() }) }) it('can reactivate a subscription', done => { subscription.reactivate(err => { demand(err).not.exist() subscription.state.must.equal('active') subscription.activated_at.must.be.a.date() subscription.activated_at.getTime().must.equal(subscription.current_period_started_at.getTime()) subscription.canceled_at.must.be.a.string() subscription.expires_at.must.be.a.string() done() }) }) it('can postpone a subscription', done => { const now = new Date() const nextDate = new Date(now.getUTCFullYear(), now.getUTCMonth() + 1, 1) subscription.postpone(nextDate, err => { demand(err).not.exist() nextDate.getTime().must.equal(subscription.current_period_ends_at.getTime()) done() }) }) it('can terminate a subscription without a refund', done => { subscription.terminate('none', err => { demand(err).not.exist() subscription.state.must.equal('expired') subscription.canceled_at.must.be.a.date() done() }) }) }) describe.skip('Coupons', () => { let coupon, couponCode it('can create a coupon', done => { couponCode = uuid.v4() const data = { coupon_code: couponCode, name: 'Test Coupon', discount_type: 'percent', discount_percent: 50, single_use: true, invoice_description: 'The coupon, as invoiced', hosted_description: 'This is a description of a coupon' } recurly.Coupon().create(data, (err, coup) => { demand(err).not.exist() coup.coupon_code.must.equal(couponCode) coup.state.must.equal('redeemable') coup.single_use.must.equal(true) coup.applies_to_all_plans.must.equal(true) done() }) }) it('can fetch a coupon', done => { coupon = recurly.Coupon() coupon.id = couponCode coupon.fetch(err => { demand(err).not.exist() coupon.coupon_code.must.equal(couponCode) coupon.state.must.equal('redeemable') coupon.single_use.must.equal(true) coupon.applies_to_all_plans.must.equal(true) coupon.name.must.equal('Test Coupon') done() }) }) it('can redeem a coupon', done => { const options = { account_code: freshAccountId, currency: 'USD' } coupon.redeem(options, (err, redemption) => { demand(err).not.exist() redemption.coupon_code.must.equal(couponCode) redemption.single_use.must.equal(true) done() }) }) // examine a redemption object it('can delete a coupon', done => { coupon.destroy(err => { console.log(err) demand(err).not.exist() done() }) }) }) describe('Transactions', () => { let trans1 it('can fetch all transactions from the test Recurly account', done => { recurly.Transaction().all((err, transactions) => { demand(err).not.exist() transactions.must.be.an.object() const transactionsIds = Object.keys(transactions) transactionsIds.length.must.be.above(0) transactionsIds[0].must.not.equal('undefined') done() }) }) it('requires an account parameter with account code', () => { const wrong = () => { const inadequate = { amount_in_cents: 10, currency: 'USD' } recurly.Transaction().create(inadequate, _.noop) } wrong.must.throw(Error) }) it('requires an amount_in_cents parameter when creating a transaction', () => { const wrong = () => { const inadequate = { account: { account_code: freshAccountId }, currency: 'USD' } recurly.Transaction().create(inadequate, _.noop) } wrong.must.throw(Error) }) it('requires an currency parameter when creating a transaction', () => { const wrong = () => { const inadequate = { account: { account_code: freshAccountId }, amount_in_cents: 10 } recurly.Transaction().create(inadequate, _.noop) } wrong.must.throw(Error) }) it('can create a transaction', done => { const options = { amount_in_cents: 100, currency: 'USD', account: { account_code: freshAccountId } } recurly.Transaction().create(options, (err, transaction) => { demand(err).not.exist() transaction.action.must.equal('purchase') transaction.amount_in_cents.must.equal(100) transaction.currency.must.equal('USD') transaction.status.must.equal('success') transaction.reference.must.exist() transaction.voidable.must.equal(true) transaction.refundable.must.equal(true) transaction.details.must.have.property('account') transaction.details.account.account_code.must.equal(freshAccountId) trans1 = transaction done() }) }) it.skip('can refund a transaction fully', done => { trans1.refund(err => { demand(err).not.exist() trans1.status.must.equal('void') trans1.voidable.must.equal(false) trans1.refundable.must.equal(false) done() }) }) it.skip('can refund a transaction partially', done => { const options = { amount_in_cents: 500, currency: 'USD', account: { account_code: freshAccountId } } recurly.Transaction().create(options, (err, transaction) => { demand(err).not.exist() transaction.refund(250, err => { demand(err).not.exist() transaction.amount_in_cents.must.equal(250) transaction.status.must.equal('success') transaction.voidable.must.equal(true) transaction.refundable.must.equal(false) done() }) }) }) }) describe('Invoices', () => { it('can fetch all invoices from the test Recurly account', done => { recurly.Invoice().all((err, invoices) => { demand(err).not.exist() invoices.must.be.an.object() const invoiceIds = Object.keys(invoices) invoiceIds.length.must.be.above(0) invoiceIds[0].must.not.equal('undefined') done() }) }) describe('refunds', () => { before(function(done) { recurly.Invoice().all({state: 'collected'}, (err, invoices) => { demand(err).not.exist() this.invoices = invoices done() }) }) it('requires an invoice_number when refunding an invoice', function() { const wrong = () => { const invoice = recurly.Invoice() invoice.id = this.invoices[0].uuid invoice.refund(_.noop) } wrong.must.throw(Error) }) it('can issue an open amount refund for a specific amount against an invoice', function(done) { const refundableInvoice = _.find(this.invoices, invoice => _.get(invoice, 'a.refund')) debug('invoice to refund', refundableInvoice) const refundOptions = { amount_in_cents: 5 } const invoice = recurly.Invoice() invoice.id = refundableInvoice.id invoice.invoice_number = refundableInvoice.invoice_number invoice.refund(refundOptions, err => { demand(err).not.exist() debug('new refund invoice', invoice) invoice.must.have.property('_resources') invoice._resources.must.be.an.object() invoice._resources.must.have.property('account') invoice._resources.must.have.property('original_invoice') invoice.must.have.property('properties') invoice.must.be.an.object() invoice.must.have.property('invoice_number') invoice.invoice_number.must.not.equal(refundableInvoice.invoice_number) invoice.subtotal_in_cents.must.be.below(0) invoice.subtotal_in_cents.must.equal(refundOptions.amount_in_cents * -1) done() }) }) it('can issue an open amount refund for the full amount against an invoice', function(done) { const refundableInvoice = _.findLast(this.invoices, invoice => _.get(invoice, 'a.refund')) debug('invoice to refund', refundableInvoice) const invoice = recurly.Invoice() invoice.id = refundableInvoice.id invoice.invoice_number = refundableInvoice.invoice_number invoice.refund(err => { demand(err).not.exist() debug('new refund invoice', invoice) invoice.must.have.property('_resources') invoice._resources.must.be.an.object() invoice._resources.must.have.property('account') invoice._resources.must.have.property('original_invoice') invoice.must.have.property('properties') invoice.must.be.an.object() invoice.must.have.property('invoice_number') invoice.invoice_number.must.not.equal(refundableInvoice.invoice_number) invoice.total_in_cents.must.be.below(0) invoice.total_in_cents.must.equal(refundableInvoice.total_in_cents * -1) done() }) }) }) describe('cancelations', () => { before(function(done) { const accountData = { id: uuid.v4() } // Create a test account recurly.Account().create(accountData, (err, account) => { if (err) return done(err) const adjustmentData = { currency: 'USD', unit_amount_in_cents: 2000, quantity: 1, description: 'A test charge for $20' } // add a charge to that account account.createAdjustment(adjustmentData, (err, adjustment) => { if (err) return done(err) // create an invoice with the pending charges account.createInvoice((err, invoice) => { if (err) return done(err) this.invoiceToMarkAsFailed = invoice done() }) }) }) }) it('can mark an open invoice as failed collection', function(done) { const invoice = recurly.Invoice() invoice.invoice_number = this.invoiceToMarkAsFailed.invoice_number invoice.markFailed(err => { demand(err).not.exist() invoice.state.must.equal('failed') done() }) }) }) }) describe('RecurlyError', () => { describe('General errors', () => { it('handles a general error', done => { const account = recurly.Account() account.id = 'some-invalid-id' account.fetch(err => { debug('Got error: %o', err) demand(err).to.exist() err.must.be.an.object() err.must.have.property('message') demand(err.message).to.not.be.undefined() err.must.have.property('errors') err.errors.must.be.an.array() err.errors.length.must.equal(1) err.errors[0].symbol.must.equal('not_found') done() }) }) it('handles a single field validation error', done => { const data = { id: uuid.v4(), email: 'test@example.com2', // Note invalid email address first_name: 'John', last_name: 'Whorfin', company_name: 'Yoyodyne Propulsion Systems' } recurly.Account().create(data, (err, newAccount) => { debug('Got error: %o', err) demand(err).to.exist() err.must.be.an.object() err.must.have.property('message') demand(err.message).to.not.be.undefined() err.must.have.property('errors') err.errors.must.be.an.array() err.errors.length.must.equal(1) err.errors[0].field.must.equal('account.email') err.errors[0].symbol.must.equal('invalid_email') done() }) }) }) describe('Transaction errors', () => { beforeEach(function(done) { const self = this const data = { id: uuid.v4(), email: 'test@example.com', first_name: 'John', last_name: 'Whorfin', company_name: 'Yoyodyne Propulsion Systems' } recurly.Account().create(data, (err, newAccount) => { demand(err).not.exist() self.account = newAccount done() }) }) it('handles multiple validation errors', function(done) { account = recurly.Account() account.id = this.account.id const binfo = recurly.BillingInfo() binfo.account_code = this.account.id const billingData = { first_name: this.account.properties.first_name, last_name: this.account.properties.last_name, number: '4111-1111', // Note invalid format month: 1, year: 2010, verification_value: '111' } binfo.update(billingData, err => { debug('Got error: %o', err) demand(err).to.exist() err.must.be.an.object() err.must.have.property('message') err.message.must.not.equal('undefined') err.must.have.property('errors') err.errors.must.be.an.array() err.errors.length.must.equal(6) done() }) }) it('handles multiple transaction errors', function(done) { account = recurly.Account() account.id = this.account.id const binfo = recurly.BillingInfo() binfo.account_code = this.account.id const billingData = { first_name: this.account.properties.first_name, last_name: this.account.properties.last_name, number: '4000-0000-0000-0101', month: 1, year: (new Date()).getFullYear() + 3, verification_value: '111', address1: '760 Market Street', address2: 'Suite 500', city: 'San Francisco', state: 'CA', country: 'USA', zip: '94102' } binfo.update(billingData, err => { debug('Got error: %o', err) demand(err).to.exist() err.must.be.an.object() err.must.have.property('message') err.message.must.not.equal('undefined') err.must.have.property('errors') err.must.have.property('error_code') err.must.have.property('error_category') err.must.have.property('merchant_message') err.must.have.property('customer_message') err.errors.must.be.an.array() err.errors.length.must.equal(1) err.error_code.must.equal('fraud_security_code') done() }) }) }) }) describe('Initialization', () => { it('should create a new Recurring instance', () => { const recurly1 = new Recurring() demand(recurly1.APIKEY).not.exist() recurly1.setAPIKey('123') recurly1.APIKEY.must.equal('123') const recurly2 = new Recurring() demand(recurly2.APIKEY).not.exist() recurly2.setAPIKey('abc') recurly2.APIKEY.must.equal('abc') recurly1.APIKEY.must.equal('123') recurly2.APIKEY.must.equal('abc') }) }) describe('Prerequsites', () => { before(() => { recurly.setAPIKey('') }) it('should raise an error if the API Key has not been set.', done => { const data = { id: uuid.v4(), email: 'test@example.com', first_name: 'John', last_name: 'Whorfin', company_name: 'Yoyodyne Propulsion Systems' } recurly.Account().create(data, (err, newAccount) => { demand(err).exist() err.must.be.an(Error) done() }) }) after(() => { recurly.setAPIKey(config.apikey) }) })