From ec450b6bca42bf1c2bf9be71e4d5c40a377f0a76 Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Mon, 28 Aug 2017 14:35:20 +1000 Subject: [PATCH 1/2] added support for contact groups --- lib/core.js | 1 + lib/entities/accounting/contactgroup.js | 82 ++++++++ .../accounting/contactgroups.js | 35 ++++ test/core/contactgroups_tests.js | 184 ++++++++++++++++++ test/testrunner.js | 1 + 5 files changed, 303 insertions(+) create mode 100644 lib/entities/accounting/contactgroup.js create mode 100644 lib/entity_helpers/accounting/contactgroups.js create mode 100644 test/core/contactgroups_tests.js diff --git a/lib/core.js b/lib/core.js index 04d6df500..8cc9578bd 100644 --- a/lib/core.js +++ b/lib/core.js @@ -23,6 +23,7 @@ const HELPERS = { reports: { file: 'reports' }, manualjournals: { file: 'manualjournals' }, repeatinginvoices: { file: 'repeatinginvoices' }, + contactGroups: { file: 'contactgroups' }, }; function Core(application) { diff --git a/lib/entities/accounting/contactgroup.js b/lib/entities/accounting/contactgroup.js new file mode 100644 index 000000000..79bb82489 --- /dev/null +++ b/lib/entities/accounting/contactgroup.js @@ -0,0 +1,82 @@ +'use strict'; + +const _ = require('lodash'); +const Entity = require('../entity'); +const ContactSchema = require('./contact').ContactSchema; + +const ContactGroupSchema = Entity.SchemaObject({ + ContactGroupID: { type: String, toObject: 'always' }, + Name: { type: String, toObject: 'always' }, + Status: { type: String, toObject: 'always' }, + Contacts: [ContactSchema], +}); + +const ContactGroup = Entity.extend(ContactGroupSchema, { + constructor: function(application, data, options) { + this.Entity.apply(this, arguments); + }, + initialize: function(data, options) {}, + save: function(options) { + const self = this; + let path = ''; + let method = ''; + if (this.ContactGroupID) { + path = `ContactGroups/${this.ContactGroupID}`; + method = 'post'; + } else { + path = 'ContactGroups'; + method = 'put'; + } + return this.application.putOrPostEntity( + method, + path, + JSON.stringify(self), + { + entityPath: 'ContactGroups', + entityConstructor: function(data) { + return self.application.core.contactGroups.newContactGroup(data); + }, + } + ); + }, + saveContacts: function(contacts) { + const self = this; + const path = `ContactGroups/${this.ContactGroupID}/Contacts`; + const method = 'put'; + + if (!_.isArray(contacts)) { + contacts = [contacts]; + } + + const payload = { + Contacts: contacts, + }; + + return this.application.putOrPostEntity( + method, + path, + JSON.stringify(payload), + { + entityPath: 'Contacts', + entityConstructor: function(data) { + return self.application.core.contacts.newContact(data); + }, + } + ); + }, + deleteContact: function(contactID) { + return this.deleteContacts(contactID); + }, + deleteContacts: function(contactID) { + let path = `ContactGroups/${this.ContactGroupID}/Contacts/`; + + if (contactID) { + path += contactID; + } + + return this.application.deleteEntities(path); + }, +}); + +module.exports.ContactGroup = ContactGroup; +module.exports.ContactGroupSchema = ContactGroupSchema; diff --git a/lib/entity_helpers/accounting/contactgroups.js b/lib/entity_helpers/accounting/contactgroups.js new file mode 100644 index 000000000..b9bcf6417 --- /dev/null +++ b/lib/entity_helpers/accounting/contactgroups.js @@ -0,0 +1,35 @@ +'use strict'; + +const _ = require('lodash'); +const EntityHelper = require('../entity_helper'); +const ContactGroup = require('../../entities/accounting/contactgroup').ContactGroup; + +const ContactGroups = EntityHelper.extend({ + constructor: function(application, options) { + EntityHelper.call( + this, + application, + Object.assign({ entityPlural: 'ContactGroups' }, options) + ); + }, + newContactGroup: function(data, options) { + return new ContactGroup(this.application, data, options); + }, + getContactGroups: function(options) { + return this.getEntities(this.setUpOptions(options)); + }, + getContactGroup: function(id) { + return this.getContactGroups({ id }).then(contactGroups => + _.first(contactGroups) + ); + }, + setUpOptions: function(options) { + const self = this; + const clonedOptions = _.clone(options || {}); + clonedOptions.entityPath = 'ContactGroups'; + clonedOptions.entityConstructor = data => self.newContactGroup(data); + return clonedOptions; + } +}); + +module.exports = ContactGroups; diff --git a/test/core/contactgroups_tests.js b/test/core/contactgroups_tests.js new file mode 100644 index 000000000..4e47055fc --- /dev/null +++ b/test/core/contactgroups_tests.js @@ -0,0 +1,184 @@ +'use strict'; + +const common = require('../common/common'); +const functions = require('../common/functions'); + +const expect = common.expect; +const wrapError = functions.wrapError; + +const validateContactGroup = contactGroup => { + if (!contactGroup) { + return false; + } + + expect(contactGroup.ContactGroupID).to.be.a('String'); + expect(contactGroup.Name).to.be.a('String'); + expect(contactGroup.Status).to.be.a('String'); + expect(contactGroup.Contacts.length).to.be.a('Number'); + contactGroup.Contacts.forEach(contact => { + expect(contact.ContactID).to.be.a('String'); + expect(contact.Name).to.be.a('String'); + }); + + return true; +}; + +describe('Contact Groups', () => { + let contactGroupID = ''; + const sampleContactGroup = { + Name: `Test Contact Group: ${Math.random()}`, + Status: 'ACTIVE', + }; + it('get all', done => { + common.currentApp.core.contactGroups + .getContactGroups() + .then(contactGroups => { + expect(contactGroups).to.have.length.greaterThan(0); + contactGroups.forEach(contactGroup => { + expect(validateContactGroup(contactGroup)).to.equal(true); + contactGroupID = contactGroup.ContactGroupID; + }); + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + + it('get single', done => { + common.currentApp.core.contactGroups + .getContactGroup(contactGroupID) + .then(contactGroup => { + expect(validateContactGroup(contactGroup)).to.equal(true); + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + + it('creates a contact group', done => { + const contactGroup = common.currentApp.core.contactGroups.newContactGroup( + sampleContactGroup + ); + + contactGroup + .save() + .then(response => { + expect(validateContactGroup(response.entities[0])).to.equal(true); + sampleContactGroup.ContactGroupID = response.entities[0].ContactGroupID; + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + + it('Updates a contact group', done => { + const updatedName = `Updated ${Math.random()}`; + common.currentApp.core.contactGroups + .getContactGroup(sampleContactGroup.ContactGroupID) + .then(contactGroup => { + expect(validateContactGroup(contactGroup)).to.equal(true); + contactGroup.Name = updatedName; + return contactGroup.save(); + }) + .then(response => { + expect(validateContactGroup(response.entities[0])).to.equal(true); + expect(response.entities[0].Name).to.equal(updatedName); + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + + it('Adds a contact to a contact group', done => { + const sampleContacts = []; + common.currentApp.core.contacts + .getContacts() + .then(contacts => { + for (let i = 0; i < 5; i += 1) { + sampleContacts.push({ + ContactID: contacts[i].ContactID, + }); + } + }) + .then(() => + common.currentApp.core.contactGroups.getContactGroup( + sampleContactGroup.ContactGroupID + ) + ) + .then(contactGroup => { + expect(validateContactGroup(contactGroup)).to.equal(true); + return contactGroup.saveContacts(sampleContacts); + }) + .then(response => { + response.entities.forEach((contact, idx) => { + expect(contact.ContactID).to.equal(sampleContacts[idx].ContactID); + }); + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + + it('Deletes a specific contact from a contact group', done => { + let contactIDToRemove = ''; + common.currentApp.core.contactGroups + .getContactGroup(sampleContactGroup.ContactGroupID) + .then(contactGroup => { + expect(validateContactGroup(contactGroup)).to.equal(true); + contactIDToRemove = contactGroup.Contacts[0].ContactID; + return contactGroup.deleteContact(contactIDToRemove); + }) + .then(() => { + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + + it('Deletes all contacts from a contact group', done => { + common.currentApp.core.contactGroups + .getContactGroup(sampleContactGroup.ContactGroupID) + .then(contactGroup => { + expect(validateContactGroup(contactGroup)).to.equal(true); + return contactGroup.deleteContacts(); + }) + .then(() => { + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + + it('Deletes a contact group', done => { + common.currentApp.core.contactGroups + .getContactGroup(sampleContactGroup.ContactGroupID) + .then(contactGroup => { + expect(validateContactGroup(contactGroup)).to.equal(true); + contactGroup.Status = 'DELETED'; + return contactGroup.save(); + }) + .then(response => { + expect(validateContactGroup(response.entities[0])).to.equal(true); + expect(response.entities[0].Status).to.equal('DELETED'); + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); +}); diff --git a/test/testrunner.js b/test/testrunner.js index 5e6af26ba..3aaa36e27 100644 --- a/test/testrunner.js +++ b/test/testrunner.js @@ -52,6 +52,7 @@ describe('Accounting API Tests', () => { 'repeatinginvoice_tests', `${__dirname}/core/repeatinginvoice_tests.js` ); + importTest('contactgroups_tests', `${__dirname}/core/contactgroups_tests.js`); }); describe.skip('Payroll API Tests', () => { From 127e0af0dde4be39d0422f1cbb8cf1ab215c8b93 Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Mon, 28 Aug 2017 15:22:14 +1000 Subject: [PATCH 2/2] added docs for contactgroups --- README.md | 1 + docs/Contact-Groups.md | 249 ++++++++++++++++++ lib/entities/accounting/contactgroup.js | 4 +- .../accounting/contactgroups.js | 3 + test/core/contactgroups_tests.js | 48 +++- test/core/contacts_tests.js | 8 +- 6 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 docs/Contact-Groups.md diff --git a/README.md b/README.md index 1969a59a1..5ec7e9eb1 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The following Xero API functions are supported: * Bank Transfers * Branding Themes * Contacts +* Contact Groups * Credit Notes * Currencies * Invoices diff --git a/docs/Contact-Groups.md b/docs/Contact-Groups.md new file mode 100644 index 000000000..2abe2c8ec --- /dev/null +++ b/docs/Contact-Groups.md @@ -0,0 +1,249 @@ +The following examples explain the Contacts Groups section of the SDK. The API documentation on Contact Groups can be found [here](https://developer.xero.com/documentation/api/contactgroups). + +### Supported functions + +* Create New Contact Groups +* Retrieve Contact Groups (all, by ID, or with 'where' clause) +* Update Contact Group Name +* Delete Contact Group +* Add Contacts to Contact Group +* Remove Contacts from Contact Group (single/all) + +These functions are explained further below. + +### Entity Helper + +The entity helper that has been created for the contact groups functions exists in the following place: + +`client.core.contactGroups` + +This helper contains the following functions: + +* `newContactGroup(data[, options])` +* `getContactGroups([options])` +* `getContactGroup(id[, modifiedAfter])` + +The ContactGroup itself has the following functions: + +* `save([options])` +* `saveContacts(contacts)` +* `deleteContact(contactID)` +* `deleteAllContacts()` + +### Creating a new contact group + +This code assumes you've already created a client using the xero-node sdk. + +```javascript + +var sampleContactGroup = { + Name: 'New Contacts', + Status: 'ACTIVE' +}; + +var contactGroupObj = xeroClient.core.contactGroups.newContactGroup(sampleContactGroup); + +contactGroupObj.save() + .then(function(contactGroups) { + //Contact Group has been created + var myGroup = contactGroups.entities[0]; + }) + .catch(function(err) { + //Some error occurred + }); +``` + +Some points to note with the code snippet above: + +* The `.newContactGroup()` function doesn't actually make any API call to Xero. It only creates an object according to the contact schema that _can_ be saved using the `.save()` function at some point in future. +* The `.save()` function returns a promise that can be met using the `.then()` function, and rejections can be caught using the `.catch()` function. +* The promise that is returned by the `.save()` function contains a response object. This has a bunch of information about the state of the response, but also contains an `entities` array. This array is what actually contains the object that was just created. +* For single object saving, this `entities` array should only ever contain one element, but some objects support a multiple object save and in this case the `entities` array will contain all of the objects that were created. + +### Creating multiple contact groups + +This functionality allows developers to create multiple contact groups in one call to the SDK, rather than iterating. + +```javascript + +var data = [{ + Name: 'Johnnies Coffee', + Status: 'ACTIVE' +},{ + Name: 'Jimmies Cups', + Status: 'ACTIVE' +}]; + +var contactGroups = []; + +contactGroups.push(xeroClient.core.contactGroups.newContactGroup(data[0])); +contactGroups.push(xeroClient.core.contactGroups.newContactGroup(data[1])); + +xeroClient.core.contactGroups.saveContactGroups(contactGroups) + .then(function(response) { + //Contact Groups have been created + console.log(response.entities[0].Name); // 'Johnnies' + console.log(response.entities[1].Name); // 'Jimmies' + }) + .catch(function(err) { + //Some error occurred + }); +``` + +### Retrieving All Contact Groups + +This example shows how to retrieve all contact groups in a single call. + +```javascript + +xeroClient.core.contactGroups.getContactGroups() + .then(function(contactGroups) { + //We've got some groups + contactGroups.forEach(function(contactGroup){ + //do something useful + console.log(contactGroup.Name); + }); + }) +``` + +* When using the getContactGroups method, as no object is being saved there is no `entities` array. Instead you are provided an array of contactGroup objects that you can use directly in your application. + +### Retrieving ContactGroups by ID + +This example shows how to retrieve a contact group using the Xero supplied GUID. + +```javascript + +var myContactGroupID = '288762e4-67a9-442d-9956-9a14e9d8826e'; + +xeroClient.core.contactGroups.getContactGroup(myContactGroupID) + .then(function(contactGroup) { + //We've got the contact group so do something useful + console.log(contactGroup.Name); + }); +``` + +### Retrieving Contact Groups with filters + +This example shows how to retrieve a Contact Group using the 'where' filter. + +```javascript + +//filter contact groups that are type Customer +const filter = 'Name.Contains("Jim")'; + +xeroClient.core.contactGroups.getContactGroups({where: filter}) + .then(function(contactGroups) { + //We've got some groups + contactGroups.forEach(function(group){ + //do something useful + console.log(group.Name); //will contain 'Jim' + }); + }) +``` + +### Updating Contact Groups + +This example shows how to update a contact group that's been retrieved via the SDK. + +```javascript + +var someContactGroupID = '75520d2e-e19d-4f36-b19b-e3b9000b2daa'; + +xeroClient.core.contactGroups.getContactGroup(someContactGroupID) + .then(function(group) { + //We've got the group so now let's change the name + group.Name = 'My awesome new name'; + + return group.save(); + }) + .then(function(response) { + const thisGroup = response.entities[0]; + console.log(thisGroup.Name); //'My awesome new name' + }) +``` + +### Add a Contact to a Contact Group + +This example shows how to add an existing contact to a contact group that has been created. + +_Note:_ It's not possible to create a contact group with contacts at the same time. This must be done with two calls to the SDK. + +```javascript + +const contacts = [{ + ContactID: '75520d2e-e19d-4f36-b19b-e3b9000b2daa' +}]; +const someContactGroupID = '9d9vcd9-a0df-2bfe-39fd-0c0d0es9f0'; + +xeroClient.core.contactGroups.getContactGroup(someContactGroupID) + .then(function(group) { + // We've got the group so now let's save the new contacts + return group.saveContacts(contacts); + }) + .then(function(response) { + // This response contains a list of contacts that were just added + const groupContacts = response.entities[0]; + console.log(groupContacts[0].ContactID); // '75520d2e-e19d-4f36-b19b-e3b9000b2daa' + }) +``` + +### Delete a Contact from a Contact Group + +This example shows how to remove a contact from a contact group. + +```javascript + +const contactIDToRemove = '75520d2e-e19d-4f36-b19b-e3b9000b2daa'; +const someContactGroupID = '9d9vcd9-a0df-2bfe-39fd-0c0d0es9f0'; + +xeroClient.core.contactGroups.getContactGroup(someContactGroupID) + .then(function(group) { + // We've got the group so now let's delete a contact + return group.deleteContact(contactIDToRemove); + }) + .then(function(response) { + // if all was successful response will be an empty object + console.log(response) // will be {} + }) +``` + +### Delete all Contacts from a Contact Group + +This example shows how to remove all contacts from a specified contact group. + +```javascript + +const someContactGroupID = '9d9vcd9-a0df-2bfe-39fd-0c0d0es9f0'; + +xeroClient.core.contactGroups.getContactGroup(someContactGroupID) + .then(function(group) { + // We've got the group so now let's delete a contact + return group.deleteAllContacts(); + }) + .then(function(response) { + // if all was successful response will be an empty object + console.log(response) // will be {} + }) +``` + +### Delete a Contact Group + +This example shows how to remove a Contact Group completely + +```javascript + +const someContactGroupID = '9d9vcd9-a0df-2bfe-39fd-0c0d0es9f0'; + +xeroClient.core.contactGroups.getContactGroup(someContactGroupID) + .then(function(group) { + //We've got the group so now let's change the Status + group.Status = 'DELETED'; + + return group.save(); + }) + .then(function(response) { + const thisGroup = response.entities[0]; + console.log(thisGroup.Status); // 'DELETED' + }) +``` diff --git a/lib/entities/accounting/contactgroup.js b/lib/entities/accounting/contactgroup.js index 79bb82489..f85d91326 100644 --- a/lib/entities/accounting/contactgroup.js +++ b/lib/entities/accounting/contactgroup.js @@ -65,9 +65,9 @@ const ContactGroup = Entity.extend(ContactGroupSchema, { ); }, deleteContact: function(contactID) { - return this.deleteContacts(contactID); + return this.deleteAllContacts(contactID); }, - deleteContacts: function(contactID) { + deleteAllContacts: function(contactID) { let path = `ContactGroups/${this.ContactGroupID}/Contacts/`; if (contactID) { diff --git a/lib/entity_helpers/accounting/contactgroups.js b/lib/entity_helpers/accounting/contactgroups.js index b9bcf6417..675073d95 100644 --- a/lib/entity_helpers/accounting/contactgroups.js +++ b/lib/entity_helpers/accounting/contactgroups.js @@ -23,6 +23,9 @@ const ContactGroups = EntityHelper.extend({ _.first(contactGroups) ); }, + saveContactGroups: function(contactGroups, options) { + return this.saveEntities(contactGroups, this.setUpOptions(options)); + }, setUpOptions: function(options) { const self = this; const clonedOptions = _.clone(options || {}); diff --git a/test/core/contactgroups_tests.js b/test/core/contactgroups_tests.js index 4e47055fc..477388d3e 100644 --- a/test/core/contactgroups_tests.js +++ b/test/core/contactgroups_tests.js @@ -59,6 +59,24 @@ describe('Contact Groups', () => { }); }); + it('get with filter', done => { + const filter = 'Name.Contains("Updated")'; + common.currentApp.core.contactGroups + .getContactGroups({ + where: filter, + }) + .then(contactGroups => { + contactGroups.forEach(contactGroup => { + expect(contactGroup.Name.indexOf('Updated')).to.be.greaterThan(-1); + }); + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + it('creates a contact group', done => { const contactGroup = common.currentApp.core.contactGroups.newContactGroup( sampleContactGroup @@ -77,6 +95,34 @@ describe('Contact Groups', () => { }); }); + it('create multiple contact groups', done => { + const contactGroups = []; + + for (let i = 0; i < 2; i += 1) { + contactGroups.push( + common.currentApp.core.contactGroups.newContactGroup({ + Name: `New Contacts ${Math.random()}`, + Status: 'ACTIVE', + }) + ); + } + + common.currentApp.core.contactGroups + .saveContactGroups(contactGroups) + .then(response => { + expect(response.entities).to.have.length.greaterThan(0); + response.entities.forEach(contactGroup => { + expect(contactGroup.ContactGroupID).to.not.equal(''); + expect(contactGroup.ContactGroupID).to.not.equal(undefined); + }); + done(); + }) + .catch(err => { + console.error(err); + done(wrapError(err)); + }); + }); + it('Updates a contact group', done => { const updatedName = `Updated ${Math.random()}`; common.currentApp.core.contactGroups @@ -152,7 +198,7 @@ describe('Contact Groups', () => { .getContactGroup(sampleContactGroup.ContactGroupID) .then(contactGroup => { expect(validateContactGroup(contactGroup)).to.equal(true); - return contactGroup.deleteContacts(); + return contactGroup.deleteAllContacts(); }) .then(() => { done(); diff --git a/test/core/contacts_tests.js b/test/core/contacts_tests.js index 4dd550fa7..cf24fde82 100644 --- a/test/core/contacts_tests.js +++ b/test/core/contacts_tests.js @@ -47,7 +47,7 @@ describe('contacts', () => { const modifiedAfter = new Date(); // take 30 seconds ago as we just created a contact - modifiedAfter.setTime(modifiedAfter.getTime()); + modifiedAfter.setTime(modifiedAfter.getTime() - 10000); currentApp.core.contacts .getContacts({ modifiedAfter: modifiedAfter }) @@ -116,7 +116,7 @@ describe('contacts', () => { done(wrapError(err)); }); }); - + it('get list of IDs', done => { currentApp.core.contacts .getContacts({ @@ -128,7 +128,7 @@ describe('contacts', () => { contacts.forEach(contact => { expect(contact.ContactID).to.be.oneOf(contactIDsList); }); - + done(); }) .catch(err => { @@ -136,7 +136,7 @@ describe('contacts', () => { done(wrapError(err)); }); }); - + it('get - invalid modified date', done => { currentApp.core.contacts .getContacts({ modifiedAfter: 'cats' })