Skip to content

Commit

Permalink
DOTORG-839: Blackbaud Raiser's Edge NXT Destination (segmentio#998)
Browse files Browse the repository at this point in the history
* DOTORG-839: Create or Update Individual Constituent Action (#1)

* DOTORG-839 Added OAuth2 settings for Blackbaud (#2)

* Move bbApiSubscriptionKey to settings

* Only aggregate integrationErrors

* Update Online Presence label

* Update directory structure

* Add types

* Abstract API calls

* Add dateStringToFuzzyDate

* Add types

* Don't retry 401s

* Don't catch errors on constituent search or creation

* Concatenate integrationErrors

* Add throwHttpErrors

* Set default for lookup_id to userId

* Pass constituentId to updateConstituent

* Remove try/catch

* Use camelCase traits

* Add filterObjectListByMatchFields

* Check if primary property is defined

* DOTORG-839 Added authentication test (#3)

* Don't match on country

* Use datetime type

* Strip non-numeric characters from phone when matching

* Don't match on undefined boolean fields

* Update generated-types.ts

* Fix linting errors

* Move fixtures out of tests directory

* Update constituentData

* Update default lookup_id mapping

* Update testAuthentication

* Remove UNEXPECTED_RECORD_COUNT error

* Update tests

---------

Co-authored-by: twilio-hwong <[email protected]>
  • Loading branch information
noahcooper and twilio-hwong authored Feb 7, 2023
1 parent 4fbbdab commit e7baeb0
Show file tree
Hide file tree
Showing 15 changed files with 2,164 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing snapshot for actions-blackbaud-raisers-edge-nxt destination: createOrUpdateIndividualConstituent action - all fields 1`] = `""`;

exports[`Testing snapshot for actions-blackbaud-raisers-edge-nxt destination: createOrUpdateIndividualConstituent action - required fields 1`] = `""`;

exports[`Testing snapshot for actions-blackbaud-raisers-edge-nxt destination: createOrUpdateIndividualConstituent action - required fields 2`] = `
Headers {
Symbol(map): Object {
"authorization": Array [
"Bearer undefined",
],
"bb-api-subscription-key": Array [
"hUXnkT]ixm7mm*HX",
],
"user-agent": Array [
"Segment (Actions)",
],
},
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import nock from 'nock'
// import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import { createTestIntegration } from '@segment/actions-core'
import { SKY_API_BASE_URL } from '../constants'
import Definition from '../index'

const testDestination = createTestIntegration(Definition)

describe("Blackbaud Raiser's Edge NXT", () => {
describe('testAuthentication', () => {
it('should validate authentication inputs', async () => {
nock(SKY_API_BASE_URL).get('/emailaddresstypes').reply(200, {})

const settings = {
bbApiSubscriptionKey: 'subscription_key'
}

await expect(testDestination.testAuthentication(settings)).resolves.not.toThrowError()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import { generateTestData } from '../../../lib/test-data'
import destination from '../index'
import nock from 'nock'

const testDestination = createTestIntegration(destination)
const destinationSlug = 'actions-blackbaud-raisers-edge-nxt'

describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
for (const actionSlug in destination.actions) {
it(`${actionSlug} action - required fields`, async () => {
const seedName = `${destinationSlug}#${actionSlug}`
const action = destination.actions[actionSlug]
const [eventData, settingsData] = generateTestData(seedName, destination, action, true)

eventData.last = 'Smith'

nock(/.*/).persist().get(/.*/).reply(200, {})
nock(/.*/).persist().patch(/.*/).reply(200, {})
nock(/.*/).persist().post(/.*/).reply(200, {})
nock(/.*/).persist().put(/.*/).reply(200, {})

const event = createTestEvent({
properties: eventData
})

const responses = await testDestination.testAction(actionSlug, {
event: event,
mapping: event.properties,
settings: settingsData,
auth: undefined
})

const request = responses[0].request
const rawBody = await request.text()

try {
const json = JSON.parse(rawBody)
expect(json).toMatchSnapshot()
return
} catch (err) {
expect(rawBody).toMatchSnapshot()
}

expect(request.headers).toMatchSnapshot()
})

it(`${actionSlug} action - all fields`, async () => {
const seedName = `${destinationSlug}#${actionSlug}`
const action = destination.actions[actionSlug]
const [eventData, settingsData] = generateTestData(seedName, destination, action, false)

nock(/.*/).persist().get(/.*/).reply(200, {})
nock(/.*/).persist().patch(/.*/).reply(200, {})
nock(/.*/).persist().post(/.*/).reply(200, {})
nock(/.*/).persist().put(/.*/).reply(200, {})

const event = createTestEvent({
properties: eventData
})

const responses = await testDestination.testAction(actionSlug, {
event: event,
mapping: event.properties,
settings: settingsData,
auth: undefined
})

const request = responses[0].request
const rawBody = await request.text()

try {
const json = JSON.parse(rawBody)
expect(json).toMatchSnapshot()
return
} catch (err) {
expect(rawBody).toMatchSnapshot()
}
})
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import type { RequestClient, ModifiedResponse } from '@segment/actions-core'
import { SKY_API_BASE_URL } from '../constants'

export class BlackbaudSkyApi {
request: RequestClient

constructor(request: RequestClient) {
this.request = request
}

async getExistingConstituents(searchField: string, searchText: string): Promise<ModifiedResponse> {
return this.request(
`${SKY_API_BASE_URL}/constituents/search?search_field=${searchField}&search_text=${searchText}`,
{
method: 'get'
}
)
}

async createConstituent(constituentData: object): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/constituents`, {
method: 'post',
json: constituentData
})
}

async updateConstituent(constituentId: string, constituentData: object): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}`, {
method: 'patch',
json: constituentData,
throwHttpErrors: false
})
}

async getConstituentAddressList(constituentId: string): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/addresses?include_inactive=true`, {
method: 'get',
throwHttpErrors: false
})
}

async createConstituentAddress(constituentId: string, constituentAddressData: object): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/addresses`, {
method: 'post',
json: {
...constituentAddressData,
constituent_id: constituentId
},
throwHttpErrors: false
})
}

async updateConstituentAddressById(addressId: string, constituentAddressData: object): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/addresses/${addressId}`, {
method: 'patch',
json: {
...constituentAddressData,
inactive: false
},
throwHttpErrors: false
})
}

async getConstituentEmailList(constituentId: string): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/emailaddresses?include_inactive=true`, {
method: 'get',
throwHttpErrors: false
})
}

async createConstituentEmail(constituentId: string, constituentEmailData: object): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/emailaddresses`, {
method: 'post',
json: {
...constituentEmailData,
constituent_id: constituentId
},
throwHttpErrors: false
})
}

async updateConstituentEmailById(emailId: string, constituentEmailData: object): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/emailaddresses/${emailId}`, {
method: 'patch',
json: {
...constituentEmailData,
inactive: false
},
throwHttpErrors: false
})
}

async getConstituentOnlinePresenceList(constituentId: string): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/onlinepresences?include_inactive=true`, {
method: 'get',
throwHttpErrors: false
})
}

async createConstituentOnlinePresence(
constituentId: string,
constituentOnlinePresenceData: object
): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/onlinepresences`, {
method: 'post',
json: {
...constituentOnlinePresenceData,
constituent_id: constituentId
},
throwHttpErrors: false
})
}

async updateConstituentOnlinePresenceById(
onlinePresenceId: string,
constituentOnlinePresenceData: object
): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/onlinepresences/${onlinePresenceId}`, {
method: 'patch',
json: {
...constituentOnlinePresenceData,
inactive: false
},
throwHttpErrors: false
})
}

async getConstituentPhoneList(constituentId: string): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/phones?include_inactive=true`, {
method: 'get',
throwHttpErrors: false
})
}

async createConstituentPhone(constituentId: string, constituentPhoneData: object): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/phones`, {
method: 'post',
json: {
...constituentPhoneData,
constituent_id: constituentId
},
throwHttpErrors: false
})
}

async updateConstituentPhoneById(phoneId: string, constituentPhoneData: object): Promise<ModifiedResponse> {
return this.request(`${SKY_API_BASE_URL}/phones/${phoneId}`, {
method: 'patch',
json: {
...constituentPhoneData,
inactive: false
},
throwHttpErrors: false
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const SKY_API_BASE_URL = 'https://api.sky.blackbaud.com/constituent/v1'
export const SKY_OAUTH2_TOKEN_URL = 'https://oauth2.sky.blackbaud.com/token'
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createOrUpdateIndividualConstituent destination action: all fields 1`] = `""`;

exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createOrUpdateIndividualConstituent destination action: required fields 1`] = `""`;

exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createOrUpdateIndividualConstituent destination action: required fields 2`] = `
Headers {
Symbol(map): Object {
"authorization": Array [
"Bearer undefined",
],
"bb-api-subscription-key": Array [
"H*Z39ROa",
],
"user-agent": Array [
"Segment (Actions)",
],
},
}
`;
Loading

0 comments on commit e7baeb0

Please sign in to comment.