Skip to content
Merged
2 changes: 2 additions & 0 deletions lib/chartmogul.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Ping = require('./chartmogul/ping');
const Task = require('./chartmogul/task');

const Invoice = require('./chartmogul/invoice');
const LineItem = require('./chartmogul/line-item');
const Subscription = require('./chartmogul/subscription');
const Transaction = require('./chartmogul/transaction');
const SubscriptionEvent = require('./chartmogul/subscription_event');
Expand All @@ -39,6 +40,7 @@ const ChartMogul = {
Enrichment,
Import,
Invoice,
LineItem,
Metrics,
Opportunity,
Ping,
Expand Down
28 changes: 28 additions & 0 deletions lib/chartmogul/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,38 @@

const Resource = require('./resource.js');

const VALID_INCLUDE_FIELDS = [
'churn_recognition',
'churn_when_zero_mrr',
'auto_churn_subscription',
'refund_handling',
'proximate_movement_reclassification'
];

class Account extends Resource {
static get path () {
return '/v1/account';
}
}

// @Override retrieve to validate include param
Account._retrieve = Resource._method('GET');
Account.retrieve = function (config, params, callback) {
if (params && params.include) {
const fields = params.include.split(',').map(function (f) { return f.trim(); });
const invalid = fields.filter(function (f) { return VALID_INCLUDE_FIELDS.indexOf(f) === -1; });
if (invalid.length > 0) {
console.warn(
'[chartmogul] Account.retrieve: unknown include field(s): ' +
invalid.join(', ') +
'. Allowed: ' + VALID_INCLUDE_FIELDS.join(', ')
);
}
}
const args = [config];
if (params) args.push(params);
if (typeof callback === 'function') args.push(callback);
return Account._retrieve.apply(this, args);
};

module.exports = Account;
49 changes: 49 additions & 0 deletions lib/chartmogul/invoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,53 @@ Invoice.destroy_all = Resource._method('DELETE', 'v1/data_sources{/data_source_u

Invoice.retrieve = Resource._method('GET', '/v1/invoices{/uuid}');

// PUT /v1/data_sources/:ds_uuid/invoices/:invoice_external_id/status
// Body: { status: 'voided' | 'written_off' }
Invoice.updateStatus = Resource._method('PUT', '/v1/data_sources{/data_source_uuid}/invoices{/invoice_external_id}/status');

// Disable/enable via PATCH /v1/invoices/:uuid/disabled_state
// Convenience wrapper: automatically sends { disabled: true }.
Invoice._setDisabledState = Resource._method('PATCH', '/v1/invoices{/uuid}/disabled_state');
Invoice.disable = function (config, uuid, dataOrCb, callback) {
// Support: disable(config, uuid), disable(config, uuid, cb), disable(config, uuid, body, cb)
let body = { disabled: true };
let cb;
if (typeof dataOrCb === 'function') {
cb = dataOrCb;
} else if (dataOrCb && typeof dataOrCb === 'object') {
body = { ...dataOrCb, disabled: true };
cb = callback;
}
const args = [config, uuid, body];
if (typeof cb === 'function') args.push(cb);
return Invoice._setDisabledState.apply(this, args);
};

Invoice.enable = function (config, uuid, callback) {
const args = [config, uuid, { disabled: false }];
if (typeof callback === 'function') args.push(callback);
return Invoice._setDisabledState.apply(this, args);
};

// PATCH /v1/invoices?data_source_uuid=X&external_id=Y { body }
Invoice.update = Resource._method('PATCH', '/v1/invoices');

// DELETE /v1/invoices?data_source_uuid=X&external_id=Y
Invoice.destroyByExternalId = Resource._method('DELETE', '/v1/invoices');

// PATCH /v1/invoices/disabled_state?data_source_uuid=X&external_id=Y { disabled: bool }
Invoice._setDisabledStateByExternalId = Resource._method('PATCH', '/v1/invoices/disabled_state');
Invoice.disableByExternalId = function (config, params, callback) {
const data = { qs: params, disabled: true };
const args = [config, data];
if (typeof callback === 'function') args.push(callback);
return Invoice._setDisabledStateByExternalId.apply(this, args);
};
Invoice.enableByExternalId = function (config, params, callback) {
const data = { qs: params, disabled: false };
const args = [config, data];
if (typeof callback === 'function') args.push(callback);
return Invoice._setDisabledStateByExternalId.apply(this, args);
};

module.exports = Invoice;
35 changes: 35 additions & 0 deletions lib/chartmogul/line-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const Resource = require('./resource.js');

class LineItem extends Resource {
static get path () {
return '/v1/line_items';
}
}

// GET /v1/line_items?data_source_uuid=X&external_id=Y
LineItem.all = Resource._method('GET', '/v1/line_items');

// PATCH /v1/line_items?data_source_uuid=X&external_id=Y { body }
LineItem.update = Resource._method('PATCH', '/v1/line_items');

// DELETE /v1/line_items?data_source_uuid=X&external_id=Y
LineItem.destroy = Resource._method('DELETE', '/v1/line_items');

// PATCH /v1/line_items/disabled_state?data_source_uuid=X&external_id=Y { disabled: bool }
LineItem._setDisabledState = Resource._method('PATCH', '/v1/line_items/disabled_state');
LineItem.disable = function (config, params, callback) {
const data = { qs: params, disabled: true };
const args = [config, data];
if (typeof callback === 'function') args.push(callback);
return LineItem._setDisabledState.apply(this, args);
};
LineItem.enable = function (config, params, callback) {
const data = { qs: params, disabled: false };
const args = [config, data];
if (typeof callback === 'function') args.push(callback);
return LineItem._setDisabledState.apply(this, args);
};

module.exports = LineItem;
23 changes: 20 additions & 3 deletions lib/chartmogul/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,23 @@ class Resource {
return reject(error);
}

const qs = method === 'GET' ? data : {};
const body = method === 'GET' ? {} : stripUndefined(data);
// For GET requests, all data becomes query string.
// For non-GET requests, data becomes the body — unless data contains
// a `qs` key, in which case data.qs becomes the query string and
// the remaining keys become the body. This allows endpoints like
// PATCH /v1/invoices?data_source_uuid=X&external_id=Y { body }
let qs, body;
if (method === 'GET') {
qs = data || {};
body = {};
} else if (data && data.qs) {
qs = data.qs;
const { qs: _, ...rest } = data;
body = stripUndefined(rest);
} else {
qs = {};
body = stripUndefined(data);
}

const options = {
qs,
Expand Down Expand Up @@ -146,7 +161,9 @@ class Resource {
static _method (verb, pathOverride) {
return function (config) {
// node v4.x doesn't support rest param
const args = Array.from(arguments).splice(1);
// Filter trailing undefined args so that an explicit undefined callback
// doesn't get popped as cb and swallow the data argument.
const args = Array.from(arguments).splice(1).filter(function (a) { return a !== undefined; });
let cb, data;
if (typeof args[args.length - 1] === 'function') {
cb = args.pop();
Expand Down
35 changes: 33 additions & 2 deletions lib/chartmogul/subscription_event.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,44 @@

const Resource = require('./resource.js');

function wrapParams (data) {
if (data && typeof data === 'object' && !data.subscription_event) {
return { subscription_event: data };
}
return data;
}

function wrapMethod (name, verb, path) {
const inner = Resource._method(verb, path);
SubscriptionEvent[name] = function (config, data, callback) {
const args = [config, wrapParams(data)];
if (typeof callback === 'function') args.push(callback);
return inner.apply(this, args);
};
}

class SubscriptionEvent extends Resource {
static get path () {
return '/v1/subscription_events';
}
}

SubscriptionEvent.updateWithParams = Resource._method('PATCH', this.path);
SubscriptionEvent.deleteWithParams = Resource._method('DELETE', this.path);
wrapMethod('create', 'POST');
wrapMethod('updateWithParams', 'PATCH', '/v1/subscription_events');
wrapMethod('deleteWithParams', 'DELETE', '/v1/subscription_events');

// Disable/enable state toggling via PATCH /v1/subscription_events/:id/disabled_state
// These convenience wrappers automatically set { disabled: true/false }.
SubscriptionEvent._setDisabledState = Resource._method('PATCH', '/v1/subscription_events{/id}/disabled_state');
SubscriptionEvent.disable = function (config, id, callback) {
const args = [config, id, { disabled: true }];
if (typeof callback === 'function') args.push(callback);
return SubscriptionEvent._setDisabledState.apply(this, args);
};
SubscriptionEvent.enable = function (config, id, callback) {
const args = [config, id, { disabled: false }];
if (typeof callback === 'function') args.push(callback);
return SubscriptionEvent._setDisabledState.apply(this, args);
};

module.exports = SubscriptionEvent;
24 changes: 24 additions & 0 deletions lib/chartmogul/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,28 @@ class Transaction extends Resource {
}
}

// GET /v1/transactions?data_source_uuid=X&external_id=Y
Transaction.all = Resource._method('GET', '/v1/transactions');

// PATCH /v1/transactions?data_source_uuid=X&external_id=Y { body }
Transaction.update = Resource._method('PATCH', '/v1/transactions');

// DELETE /v1/transactions?data_source_uuid=X&external_id=Y
Transaction.destroy = Resource._method('DELETE', '/v1/transactions');

// PATCH /v1/transactions/disabled_state?data_source_uuid=X&external_id=Y { disabled: bool }
Transaction._setDisabledState = Resource._method('PATCH', '/v1/transactions/disabled_state');
Transaction.disable = function (config, params, callback) {
const data = { qs: params, disabled: true };
const args = [config, data];
if (typeof callback === 'function') args.push(callback);
return Transaction._setDisabledState.apply(this, args);
};
Transaction.enable = function (config, params, callback) {
const data = { qs: params, disabled: false };
const args = [config, data];
if (typeof callback === 'function') args.push(callback);
return Transaction._setDisabledState.apply(this, args);
};

module.exports = Transaction;
44 changes: 44 additions & 0 deletions test/chartmogul/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('Account', () => {
.get('/v1/account')
.reply(200, {
/* eslint-disable camelcase */
id: 'acc_a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'Example Test Company',
currency: 'EUR',
time_zone: 'Europe/Berlin',
Expand All @@ -20,9 +21,52 @@ describe('Account', () => {
});

const account = await Account.retrieve(config);
expect(account.id).to.be.equal('acc_a1b2c3d4-e5f6-7890-abcd-ef1234567890');
expect(account.name).to.be.equal('Example Test Company');
expect(account.currency).to.be.equal('EUR');
expect(account.time_zone).to.be.equal('Europe/Berlin');
expect(account.week_start_on).to.be.equal('sunday');
});

it('should retrieve account with include params', async () => {
nock(config.API_BASE)
.get('/v1/account')
.query({
/* eslint-disable camelcase */
include: 'churn_recognition,churn_when_zero_mrr'
/* eslint-enable camelcase */
})
.reply(200, {
/* eslint-disable camelcase */
id: 'acc_a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'Example Test Company',
currency: 'EUR',
time_zone: 'Europe/Berlin',
week_start_on: 'sunday',
churn_recognition: 'immediate',
churn_when_zero_mrr: true
/* eslint-enable camelcase */
});

const account = await Account.retrieve(config, {
include: 'churn_recognition,churn_when_zero_mrr'
});
expect(account.id).to.be.equal('acc_a1b2c3d4-e5f6-7890-abcd-ef1234567890');
expect(account.churn_recognition).to.be.equal('immediate');
expect(account.churn_when_zero_mrr).to.be.equal(true);
});

it('should reject when retrieving account with invalid include param', () => {
nock(config.API_BASE)
.get('/v1/account')
.query({ include: 'invalid_field' })
.reply(400, { message: 'Invalid include parameter: invalid_field' });

return Account.retrieve(config, { include: 'invalid_field' })
.then(() => { throw new Error('Expected rejection'); })
.catch(e => {
if (e.message === 'Expected rejection') throw e;
expect(e.status).to.equal(400);
Comment thread
wscourge marked this conversation as resolved.
});
});
});
Loading
Loading