From d2a3435f262528443f66ca9d209705304b2052a8 Mon Sep 17 00:00:00 2001 From: Noah Cooper Date: Wed, 1 Mar 2023 07:11:54 -0500 Subject: [PATCH] DOTORG-847: Add 'Create Gift' action (#1056) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * main (#5) * Publish - @segment/actions-cli-internal@3.123.2 - @segment/actions-cli@3.123.2 - @segment/action-destinations@3.127.2 * Subscribe to more events (Redo without required type) (#986) * Add subscriptions to track event * Generate types and new snapshot * Add test * Improve labels and descriptions * Generated types * Change description * Generate types * Remove required * Generate types * Update Ripe web destination (#968) * Update Ripe web destination - Add new endpoint setting for testing purposes - Remove alias call - Update misleading anonymousId descriptions * Update erroneous default paths * Set anonymousId in identify call * Heap 34916 - add session_id + update segment library for tracking purposes (#787) * Fix events payload * Use the single event not the bulk * Fix tests * Fix should not override * remove console log and update SEGMENT_LIB var * update constant value * update browser tests as well * Adding Group support for customerio -Rename identifier field names (#973) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * renaming id and type_id to object_id and object_type_id Co-authored-by: kishoredevarasettyn * SalesWings (Actions) Destination (#945) * Generated integration from scaffold * Fix action name * Implement SalesWings destination actions * Send user agent, rearrange fields * Bugfixes * Remove debug logging * First tests * Auth tests & track event tests * Page event tests * Identify event tests * Screen event tests * Event batch test * More event batch tests * Change API key description * Commit generated types * Minor cleanup * Fix square brackets in field description UI * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Hardcoding timestamps for snapshot tests * Extract email from properties of Track event * Add action description * Add default subscription to action * Add destination present * Merge URL fields * Dedicated actions per event type * Cleanup * Update field descriptions * Update geenrated types Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov * Changing default subscription to group for group call (#995) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * setting default subscription as group for createUpdateObject and addressing other review comments * correcting merge overrides Co-authored-by: kishoredevarasettyn * Add anonymous id as a user property (#981) * Update setting description in Google Ads Conversions (#983) * HGI-237 | Updated Description for Braze Cohorts Fields (#992) * udpated description for braze cohorts * updated description for braze cohorts * updated description in mapping fields * updated description * updated event_properties to hidden * made event_properties unhidden Co-authored-by: Gaurav Kochar * Increase CI timeout to 15 minutes and 10 minutes respectively (#985) * Increase CI timeout to 15 minutes * Bump browser tests to 10 minutes Co-authored-by: Nolan Chan * Pipedrive actions PE-20 (#996) * fix for pipedrive pe-20 issue * removing default visible_to * Register Saleswing Action (#999) Co-authored-by: Nolan Chan * Publish - @segment/browser-destinations@3.72.0 - @segment/actions-cli-internal@3.124.0 - @segment/actions-cli@3.124.0 - @segment/action-destinations@3.128.0 * ACT-362 Brackets Support (#993) * add support for brackets inside js keys in get method * add double quotes * explanatory text + new link * safari support * remove invalid bracket test since it is now supported * use class based regex to avoid parseError * actually convert the regex correctly * cleanup * split tests by functionality * Refactor .get test (#1000) * Heap Fix for empty event name (#1004) * fix for pe-52 * fixing breaking tests * Publish - @segment/actions-shared@1.32.0 - @segment/browser-destinations@3.73.0 - @segment/actions-cli-internal@3.125.0 - @segment/actions-cli@3.125.0 - @segment/actions-core@3.50.0 - @segment/action-destinations@3.129.0 * fix scaffolding for oauth (#1008) * [CHANNELS-329] Add WhatsApp support for Twilio Engage (#987) * feat: added whatsapp support * fix: added missing dependencies * refactor: minor cleanup * fix: moved dependency to package level * fix: uri encoding for get traits * fix: using same auth scheme for sms & whatsapp whatsApp thankfully allows using apiKeySid & apiSecret instead of accountSid & authToken * fix: reverted changed package version * feat: allow bypassing contentVariables reconciliation * Publish - @segment/actions-cli-internal@3.126.0 - @segment/actions-cli@3.126.0 - @segment/action-destinations@3.130.0 * Add browser destination tests with saucelabs (#994) * Node 18 Upgrade (#991) * packages * ci + nvm * lock for 18 * fix webpack hashing issue * node types to 18 * Update node version for browser-tests and snyk * Fix tests * Try to fix browser tests * Fix pipedrive unit tests * Fix domain in snapshot tests * Fix yarn subscriptions * Update README --------- Co-authored-by: Dan Lasky * Use node18 for browser tests destinations (#1014) * Contribution pe 53 (#1007) * updating contributing guidelines * adding extra instructions for post deployment changes * spelling corrections * spelling corrections * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider --------- Co-authored-by: SyedWasiHaider * Qualtrics upsert transaction (#963) * adding upsert contact transacion destination * fixing snapshots * Updating perform function for upsertTransaction * Adding dynamic fields for directoryId. Updating field descriptions. Update upsertTransaction defaultSubscription * updating types * Update qualtrics destination name and descriptons on actions --------- Co-authored-by: Carl Lee * fixing a couple of issues with new Ironclad destination (#1002) * fixing a couple of issues with new Ironclad destination * adding updated generated types * fixing broken test * [salesforce] - Verify the `instanceUrl` is a valid Salesforce domain (#997) * Regex and WIP unit tests * Unit tests working * Updates regex and unit tests * Updates other unit tests * Saving package.json * Adds a couple more unit tests * Removes package.json from commits * Removes package.json from commits * Imports request client using absolute path instead of relative path * Enforce https * Publish - @segment/actions-shared@1.33.0 - @segment/browser-destinations-integration-tests@0.1.0 - @segment/browser-destinations@3.74.0 - @segment/actions-cli-internal@3.127.0 - @segment/actions-cli@3.127.0 - @segment/actions-core@3.51.0 - @segment/action-destinations@3.131.0 - @segment/destination-subscriptions@3.15.0 * Fix CommandBar browser destination initialization when CommandBar has already been loaded through other means (#1009) Co-authored-by: Thomas Kainrad * remove flow that attempts to create a JIRA ticket (#1021) * Twilio Studio as a Segment Action Destination (#1023) * Twilio Studio as a Segment Action Destination * Replaced phone number with userid in the cache key * Addressed review comments * DOTORG-839: Blackbaud Raiser's Edge NXT Destination (#998) * 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 <91703194+twilio-hwong@users.noreply.github.com> --------- Co-authored-by: Nick Aguilar Co-authored-by: Stella Chung Co-authored-by: Simon Co-authored-by: A Murphy Co-authored-by: kishoredevarasettyn <97026912+kishoredevarasettyn@users.noreply.github.com> Co-authored-by: kishoredevarasettyn Co-authored-by: Denis Egorushkin <98813888+denis-egorushkin-sw@users.noreply.github.com> Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov Co-authored-by: maryamsharif <99763167+maryamsharif@users.noreply.github.com> Co-authored-by: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Co-authored-by: Gaurav Kochar Co-authored-by: Nolan Chan Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Co-authored-by: Nolan Chan Co-authored-by: Dan Co-authored-by: Seth Silesky <5115498+silesky@users.noreply.github.com> Co-authored-by: rhall-twilio <103517471+rhall-twilio@users.noreply.github.com> Co-authored-by: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Co-authored-by: SyedWasiHaider Co-authored-by: Dan Lasky Co-authored-by: drakauskas <119876674+drakauskas@users.noreply.github.com> Co-authored-by: Carl Lee Co-authored-by: Wasi Haider Co-authored-by: Thomas Kainrad <7394822+tkainrad@users.noreply.github.com> Co-authored-by: Thomas Kainrad Co-authored-by: aradhakrishnan-twilio <116877054+aradhakrishnan-twilio@users.noreply.github.com> Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> * main (#7) * Publish - @segment/actions-cli-internal@3.123.2 - @segment/actions-cli@3.123.2 - @segment/action-destinations@3.127.2 * Subscribe to more events (Redo without required type) (#986) * Add subscriptions to track event * Generate types and new snapshot * Add test * Improve labels and descriptions * Generated types * Change description * Generate types * Remove required * Generate types * Update Ripe web destination (#968) * Update Ripe web destination - Add new endpoint setting for testing purposes - Remove alias call - Update misleading anonymousId descriptions * Update erroneous default paths * Set anonymousId in identify call * Heap 34916 - add session_id + update segment library for tracking purposes (#787) * Fix events payload * Use the single event not the bulk * Fix tests * Fix should not override * remove console log and update SEGMENT_LIB var * update constant value * update browser tests as well * Adding Group support for customerio -Rename identifier field names (#973) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * renaming id and type_id to object_id and object_type_id Co-authored-by: kishoredevarasettyn * SalesWings (Actions) Destination (#945) * Generated integration from scaffold * Fix action name * Implement SalesWings destination actions * Send user agent, rearrange fields * Bugfixes * Remove debug logging * First tests * Auth tests & track event tests * Page event tests * Identify event tests * Screen event tests * Event batch test * More event batch tests * Change API key description * Commit generated types * Minor cleanup * Fix square brackets in field description UI * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Hardcoding timestamps for snapshot tests * Extract email from properties of Track event * Add action description * Add default subscription to action * Add destination present * Merge URL fields * Dedicated actions per event type * Cleanup * Update field descriptions * Update geenrated types Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov * Changing default subscription to group for group call (#995) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * setting default subscription as group for createUpdateObject and addressing other review comments * correcting merge overrides Co-authored-by: kishoredevarasettyn * Add anonymous id as a user property (#981) * Update setting description in Google Ads Conversions (#983) * HGI-237 | Updated Description for Braze Cohorts Fields (#992) * udpated description for braze cohorts * updated description for braze cohorts * updated description in mapping fields * updated description * updated event_properties to hidden * made event_properties unhidden Co-authored-by: Gaurav Kochar * Increase CI timeout to 15 minutes and 10 minutes respectively (#985) * Increase CI timeout to 15 minutes * Bump browser tests to 10 minutes Co-authored-by: Nolan Chan * Pipedrive actions PE-20 (#996) * fix for pipedrive pe-20 issue * removing default visible_to * Register Saleswing Action (#999) Co-authored-by: Nolan Chan * Publish - @segment/browser-destinations@3.72.0 - @segment/actions-cli-internal@3.124.0 - @segment/actions-cli@3.124.0 - @segment/action-destinations@3.128.0 * ACT-362 Brackets Support (#993) * add support for brackets inside js keys in get method * add double quotes * explanatory text + new link * safari support * remove invalid bracket test since it is now supported * use class based regex to avoid parseError * actually convert the regex correctly * cleanup * split tests by functionality * Refactor .get test (#1000) * Heap Fix for empty event name (#1004) * fix for pe-52 * fixing breaking tests * Publish - @segment/actions-shared@1.32.0 - @segment/browser-destinations@3.73.0 - @segment/actions-cli-internal@3.125.0 - @segment/actions-cli@3.125.0 - @segment/actions-core@3.50.0 - @segment/action-destinations@3.129.0 * fix scaffolding for oauth (#1008) * [CHANNELS-329] Add WhatsApp support for Twilio Engage (#987) * feat: added whatsapp support * fix: added missing dependencies * refactor: minor cleanup * fix: moved dependency to package level * fix: uri encoding for get traits * fix: using same auth scheme for sms & whatsapp whatsApp thankfully allows using apiKeySid & apiSecret instead of accountSid & authToken * fix: reverted changed package version * feat: allow bypassing contentVariables reconciliation * Publish - @segment/actions-cli-internal@3.126.0 - @segment/actions-cli@3.126.0 - @segment/action-destinations@3.130.0 * Add browser destination tests with saucelabs (#994) * Node 18 Upgrade (#991) * packages * ci + nvm * lock for 18 * fix webpack hashing issue * node types to 18 * Update node version for browser-tests and snyk * Fix tests * Try to fix browser tests * Fix pipedrive unit tests * Fix domain in snapshot tests * Fix yarn subscriptions * Update README --------- Co-authored-by: Dan Lasky * Use node18 for browser tests destinations (#1014) * Contribution pe 53 (#1007) * updating contributing guidelines * adding extra instructions for post deployment changes * spelling corrections * spelling corrections * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider --------- Co-authored-by: SyedWasiHaider * Qualtrics upsert transaction (#963) * adding upsert contact transacion destination * fixing snapshots * Updating perform function for upsertTransaction * Adding dynamic fields for directoryId. Updating field descriptions. Update upsertTransaction defaultSubscription * updating types * Update qualtrics destination name and descriptons on actions --------- Co-authored-by: Carl Lee * fixing a couple of issues with new Ironclad destination (#1002) * fixing a couple of issues with new Ironclad destination * adding updated generated types * fixing broken test * [salesforce] - Verify the `instanceUrl` is a valid Salesforce domain (#997) * Regex and WIP unit tests * Unit tests working * Updates regex and unit tests * Updates other unit tests * Saving package.json * Adds a couple more unit tests * Removes package.json from commits * Removes package.json from commits * Imports request client using absolute path instead of relative path * Enforce https * Publish - @segment/actions-shared@1.33.0 - @segment/browser-destinations-integration-tests@0.1.0 - @segment/browser-destinations@3.74.0 - @segment/actions-cli-internal@3.127.0 - @segment/actions-cli@3.127.0 - @segment/actions-core@3.51.0 - @segment/action-destinations@3.131.0 - @segment/destination-subscriptions@3.15.0 * Fix CommandBar browser destination initialization when CommandBar has already been loaded through other means (#1009) Co-authored-by: Thomas Kainrad * remove flow that attempts to create a JIRA ticket (#1021) * Twilio Studio as a Segment Action Destination (#1023) * Twilio Studio as a Segment Action Destination * Replaced phone number with userid in the cache key * Addressed review comments * DOTORG-839: Blackbaud Raiser's Edge NXT Destination (#998) * 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 <91703194+twilio-hwong@users.noreply.github.com> * Google Analytics 4 Web Destination (#1012) * addPaymentInfo Action * ga4 types, properties, and functions * set config fields action poc * set config fields action poc * GA4 all action created * Remove unit test cases files * Added defaultSubscription tag in viewItemList and generateLead Action * added register code for GA4 in broweser-destinations * add customEvent action * set config action & custom event action * Delete snapshot.test.ts * Delete index.test.ts * clean up & add mappings for preset * updated types and removed picklist * Added test cases for GA4 actions * Added test cases * added custom event unit test + cleaned up merge issues and commented code * fixed typo * add back ripe and commandbar to index file * added comment on non using variable * Apply suggestions from code review Co-authored-by: Neek Sandhu * revert set configuration field actions * added updateUser function, and updated event to payload * reverted back gtag function and datalayer setup * add gtag type and remove comment * update yarn.lock file * add viewItemList & disable linter for args * remove gtag.js type dependency * update unit tests * added events to defaultSubscriptions * update index.js --------- Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu * livelike-cloud action destination (PE-41) (#1020) * created livelike-cloud action destination with one trackEvent action and three presets and added unit tests * Update packages/destination-actions/src/destinations/livelike-cloud/trackEvent/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> --------- Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Qualtrics - Fixed timezone issue in unit test (#1026) * Fixed timezone issue in unit test * refactored to use dayjs from lib * generate types (#1028) * PE-47 - Merge Algolia Insights (#1027) * Algolia insights integration (#975) * feat: initial commit after scaffold * feat: clarify insights API and impliment as distinct actions * test: write test for conversion destination * test: write test for click and view destination * test: write auth schema test * create productClick presets * add presets to destination * create all event presets * remove default imports for actions * add testAuthentication method for algolia-insights --------- Co-authored-by: Beatrice Parfait * fixing snapshots and replacing userId and anonId with userToken * fixing timestamps in tests --------- Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait * VWO Cloud Mode Description changes (#1015) * Description changes and changes in utility * Tests altered for new format * Update CODEOWNERS (#1029) * [HEAP-38485] Move to the integrations endpoint (#1016) * [HEAP-32036] First trackEvent implementation (#8) * [HEAP-32037] Send identify and add user properties requests (#9) * [HEAP-32037] Add user properties and migrate anonymous users * Hash anonymous user ID * Use message ID as idempotency key * Throw if parameters are missing * [HEAP-32884] backport filtering event properties (#10) * [HEAP-32884] backport filtering event properties - flat properties from the request payload before sending them to heap * address comments: - remove the embeded object under the util.ts, move it to flattenObj - remove unnecessary tests on identifyUser - import and use the embeded object and flatten object in trackEvent and identifyUser test not part of comments: - rename the file from flattenObj to flat * PR comments * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * remove unused file * save changes * save * fix library * remove accidental file * update tests * remove session id * PR comments * fix the tests * add user properties if more that one identifier is present * Revert "add user properties if more that one identifier is present" This reverts commit bb4fb94684be54c933e9375ae1fc877e876fbbd1. --------- Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: murphpdx * Launchpad Segment Integration (#1010) * Launchpad Segment Integration We are now adding the ability to send events from segment into Launchpad.pm - the mission control for Product teams at scale. We are adding the following: * trackEvent * groupIdentifyUser * identifyUser We have added unit tests and have tested this manually. It requires the following: * apiRegion - EU by default. We have added the US as means of future extensibility. * apiSecret - given by Launchpad.pm while onboarding. * sourceName - to be added. * updating snapshots * Changes implemented following call with Joe Track() [x] Explicitly indicate which fields are required or optional [x] Look for traits and if not context.traits [x] Need description for the Action [x] Make Timestamp field required [x] Make messageId required. It will always be there. [x] getEventProperties: This line looks incorrect: id: payload.event, [x] source mapping looks incorrect: source: integration?.name == 'Iterable' ? 'Iterable' : 'segment', Identify() [x] Description should be updated. [x] identify() calls are often fired when a trait is collected, or when a userId is collected. [x] Maybe use wording: “Creates or updates a user profile, and adds or updates trait values on the user profile…” [x] Refactor the perform() function group() [x] Group Key - Amend description to better explain that group key is a way to connect multiple organizations together [x] groupId field - Should be updated [x] Consider adding anonymousId as a field [x] Handle when there are no traits. * changes * tests passing, removed the check on user_id, anonymous_id since none are required * fixing the types as well * fixing test --------- Co-authored-by: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> * [STRATCONN-1950] Init new destination `Pinterest Conversions API` (#1030) * Init new destination `Pinterest Conversions API` * Update index.ts * Update index.ts * Update index.ts --------- Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> * Try to only pass in NODE_OPTIONS for node18+ (#1032) * Try using conditional for scripts * Remove NODE_OPTIONS from else * Echo the nodeversion * Try setting the node major version and testing that * Use correct variable in condition * Try a different approach * Use bash instead * Try to fix script * Add other remaining scripts * Changing name of new Algolia Insights Integration (#1038) * Register new action destinations (#1036) * Register new action destinations: Launchpad, Livelike Cloud, Twilio Studio, BlackBaud Raisers Edge Nxt, Pinterest Conversions Api * Fix path * add action to pinterest conversions api * add action to pinterest conversions api * add description to pinterest action * Register algolia insights (Actions) --------- Co-authored-by: rvadera12 * Publish - @segment/actions-shared@1.34.0 - @segment/browser-destinations@3.75.0 - @segment/actions-cli-internal@3.128.0 - @segment/actions-cli@3.128.0 - @segment/actions-core@3.52.0 - @segment/action-destinations@3.132.0 - @segment/destination-subscriptions@3.16.0 * Use correct defaultPath for messageId (#1041) * Google ads v11 to v12 (#1018) * gooogle conversion v12 upgrade * changes * bug fix for test cases * gooogle conversion v12 upgrade * changes * bug fix for test cases * flag name changes * flag name change in test cases * review changes * revert ga4-types file change * revert ga4-types file change * revert ga4-types file change --------- Co-authored-by: manoj kumar * [STRATCONN-1779]Add datadog stats for google ads api version (#1046) * adds stats for api version * adds missing statscontext parameter to getCustomVariables * refactor params * Voucherify-Segment.io Integration using action-destinations (#970) * initial destination configuration * identify customer action * add trackEvent * Generate screenEvent and pageEvent * Change the structure of track, page and screen events. * Delete test folder for now * Change the type definition * Delete timestamp * create group event * removed created_at property * hit to localhost address * Add unit tests * Some fixes * Change the URLs in perform method * Update URLs in tests * Delete unused testing authentication fn * Add snapshots * Add ability to pass a custom URL * Set type to required in page and screen events * Delete snapshots * Replace api endpoint (with regions) with custom URL * if there's no userId then use the anonymousId * removed space * added type property to rest of events * changed name of property 'name' to 'event' * Update index.ts * Update generated-types.ts * Delete unnecessary test - 'should throw an error when the name is not provided using page event' * Delete the 'voucherify' prefix * Slightly change the descriptions * generated types * Separate URL functions into separate files Change the file names to be more descriptive. * Reduce the getVoucherifyEndpointURL function * delete * Update the descriptions - Also deleted the 'event' prop from screen/page events and now the 'name' in screen/page event is no longer required. * Commit the generated types * Reduce the number of events to three. Track Custom Event, Identify Customer, Add group to customer metadata * Add customer attributes to traits in upsertCustomer action * Update generated-types.ts * Add testAuthentication * Update names of actions in unit tests * add firstName and lastName * Add email to custom event processing * Delete email from upsertCustomer (leave it only in traits) * Add email to description * Add email to customer processing * Update testAuthentication * update addCustomEvent * Update packages/destination-actions/src/destinations/voucherify/upsertCustomer/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Fix issue with mapping the user attributes and improve Presets * generate types * change email properties * generated types * custom url information * generated types * Update the desc of Custom URL * minor changes from last PR * Update index.ts --------- Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * HGI 372 - Fix Segment Profiles Destination (#1047) * Added `All: false` in Tracking API event * Changed PAPI Token field from `string` to `password` * Fixed minor typos * HGI-368 | Fixed the missing x-signature in request header when batching is enabled (#1043) * worked on HGI-368 Fix * added a unit test case for same fix --------- Co-authored-by: Gaurav Kochar * Mixpanel destination: use user-agent for browser data (#1035) * remove device manufacturer - use user-agent * userAgent is only sent on web * New SalesWings API urls (#1039) * Use new SalesWings API urls * Do not check response status in testAuthentication * Fix testAuthentication * Remove unused file --------- Co-authored-by: Nick Aguilar Co-authored-by: Stella Chung Co-authored-by: Simon Co-authored-by: A Murphy Co-authored-by: kishoredevarasettyn <97026912+kishoredevarasettyn@users.noreply.github.com> Co-authored-by: kishoredevarasettyn Co-authored-by: Denis Egorushkin <98813888+denis-egorushkin-sw@users.noreply.github.com> Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov Co-authored-by: maryamsharif <99763167+maryamsharif@users.noreply.github.com> Co-authored-by: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Co-authored-by: Gaurav Kochar Co-authored-by: Nolan Chan Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Co-authored-by: Nolan Chan Co-authored-by: Dan Co-authored-by: Seth Silesky <5115498+silesky@users.noreply.github.com> Co-authored-by: rhall-twilio <103517471+rhall-twilio@users.noreply.github.com> Co-authored-by: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Co-authored-by: SyedWasiHaider Co-authored-by: Dan Lasky Co-authored-by: drakauskas <119876674+drakauskas@users.noreply.github.com> Co-authored-by: Carl Lee Co-authored-by: Wasi Haider Co-authored-by: Thomas Kainrad <7394822+tkainrad@users.noreply.github.com> Co-authored-by: Thomas Kainrad Co-authored-by: aradhakrishnan-twilio <116877054+aradhakrishnan-twilio@users.noreply.github.com> Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu Co-authored-by: Abhishek Kansal Co-authored-by: Sayan Das <109198085+sayan-das-in@users.noreply.github.com> Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait Co-authored-by: Prathamesh Tamanekar Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: murphpdx Co-authored-by: Stefan Sabev Co-authored-by: Logan Luque <98849774+LLuque-twilio@users.noreply.github.com> Co-authored-by: rvadera12 Co-authored-by: Logan Luque Co-authored-by: immanojkumar <117071418+immanojkumar@users.noreply.github.com> Co-authored-by: manoj kumar Co-authored-by: Varadarajan V <109586712+varadarajan-tw@users.noreply.github.com> Co-authored-by: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Sonya Park <68977514+spjtls9@users.noreply.github.com> * main (#9) * Publish - @segment/actions-cli-internal@3.123.2 - @segment/actions-cli@3.123.2 - @segment/action-destinations@3.127.2 * Subscribe to more events (Redo without required type) (#986) * Add subscriptions to track event * Generate types and new snapshot * Add test * Improve labels and descriptions * Generated types * Change description * Generate types * Remove required * Generate types * Update Ripe web destination (#968) * Update Ripe web destination - Add new endpoint setting for testing purposes - Remove alias call - Update misleading anonymousId descriptions * Update erroneous default paths * Set anonymousId in identify call * Heap 34916 - add session_id + update segment library for tracking purposes (#787) * Fix events payload * Use the single event not the bulk * Fix tests * Fix should not override * remove console log and update SEGMENT_LIB var * update constant value * update browser tests as well * Adding Group support for customerio -Rename identifier field names (#973) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * renaming id and type_id to object_id and object_type_id Co-authored-by: kishoredevarasettyn * SalesWings (Actions) Destination (#945) * Generated integration from scaffold * Fix action name * Implement SalesWings destination actions * Send user agent, rearrange fields * Bugfixes * Remove debug logging * First tests * Auth tests & track event tests * Page event tests * Identify event tests * Screen event tests * Event batch test * More event batch tests * Change API key description * Commit generated types * Minor cleanup * Fix square brackets in field description UI * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Hardcoding timestamps for snapshot tests * Extract email from properties of Track event * Add action description * Add default subscription to action * Add destination present * Merge URL fields * Dedicated actions per event type * Cleanup * Update field descriptions * Update geenrated types Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov * Changing default subscription to group for group call (#995) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * setting default subscription as group for createUpdateObject and addressing other review comments * correcting merge overrides Co-authored-by: kishoredevarasettyn * Add anonymous id as a user property (#981) * Update setting description in Google Ads Conversions (#983) * HGI-237 | Updated Description for Braze Cohorts Fields (#992) * udpated description for braze cohorts * updated description for braze cohorts * updated description in mapping fields * updated description * updated event_properties to hidden * made event_properties unhidden Co-authored-by: Gaurav Kochar * Increase CI timeout to 15 minutes and 10 minutes respectively (#985) * Increase CI timeout to 15 minutes * Bump browser tests to 10 minutes Co-authored-by: Nolan Chan * Pipedrive actions PE-20 (#996) * fix for pipedrive pe-20 issue * removing default visible_to * Register Saleswing Action (#999) Co-authored-by: Nolan Chan * Publish - @segment/browser-destinations@3.72.0 - @segment/actions-cli-internal@3.124.0 - @segment/actions-cli@3.124.0 - @segment/action-destinations@3.128.0 * ACT-362 Brackets Support (#993) * add support for brackets inside js keys in get method * add double quotes * explanatory text + new link * safari support * remove invalid bracket test since it is now supported * use class based regex to avoid parseError * actually convert the regex correctly * cleanup * split tests by functionality * Refactor .get test (#1000) * Heap Fix for empty event name (#1004) * fix for pe-52 * fixing breaking tests * Publish - @segment/actions-shared@1.32.0 - @segment/browser-destinations@3.73.0 - @segment/actions-cli-internal@3.125.0 - @segment/actions-cli@3.125.0 - @segment/actions-core@3.50.0 - @segment/action-destinations@3.129.0 * fix scaffolding for oauth (#1008) * [CHANNELS-329] Add WhatsApp support for Twilio Engage (#987) * feat: added whatsapp support * fix: added missing dependencies * refactor: minor cleanup * fix: moved dependency to package level * fix: uri encoding for get traits * fix: using same auth scheme for sms & whatsapp whatsApp thankfully allows using apiKeySid & apiSecret instead of accountSid & authToken * fix: reverted changed package version * feat: allow bypassing contentVariables reconciliation * Publish - @segment/actions-cli-internal@3.126.0 - @segment/actions-cli@3.126.0 - @segment/action-destinations@3.130.0 * Add browser destination tests with saucelabs (#994) * Node 18 Upgrade (#991) * packages * ci + nvm * lock for 18 * fix webpack hashing issue * node types to 18 * Update node version for browser-tests and snyk * Fix tests * Try to fix browser tests * Fix pipedrive unit tests * Fix domain in snapshot tests * Fix yarn subscriptions * Update README --------- Co-authored-by: Dan Lasky * Use node18 for browser tests destinations (#1014) * Contribution pe 53 (#1007) * updating contributing guidelines * adding extra instructions for post deployment changes * spelling corrections * spelling corrections * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider --------- Co-authored-by: SyedWasiHaider * Qualtrics upsert transaction (#963) * adding upsert contact transacion destination * fixing snapshots * Updating perform function for upsertTransaction * Adding dynamic fields for directoryId. Updating field descriptions. Update upsertTransaction defaultSubscription * updating types * Update qualtrics destination name and descriptons on actions --------- Co-authored-by: Carl Lee * fixing a couple of issues with new Ironclad destination (#1002) * fixing a couple of issues with new Ironclad destination * adding updated generated types * fixing broken test * [salesforce] - Verify the `instanceUrl` is a valid Salesforce domain (#997) * Regex and WIP unit tests * Unit tests working * Updates regex and unit tests * Updates other unit tests * Saving package.json * Adds a couple more unit tests * Removes package.json from commits * Removes package.json from commits * Imports request client using absolute path instead of relative path * Enforce https * Publish - @segment/actions-shared@1.33.0 - @segment/browser-destinations-integration-tests@0.1.0 - @segment/browser-destinations@3.74.0 - @segment/actions-cli-internal@3.127.0 - @segment/actions-cli@3.127.0 - @segment/actions-core@3.51.0 - @segment/action-destinations@3.131.0 - @segment/destination-subscriptions@3.15.0 * Fix CommandBar browser destination initialization when CommandBar has already been loaded through other means (#1009) Co-authored-by: Thomas Kainrad * remove flow that attempts to create a JIRA ticket (#1021) * Twilio Studio as a Segment Action Destination (#1023) * Twilio Studio as a Segment Action Destination * Replaced phone number with userid in the cache key * Addressed review comments * DOTORG-839: Blackbaud Raiser's Edge NXT Destination (#998) * 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 <91703194+twilio-hwong@users.noreply.github.com> * Google Analytics 4 Web Destination (#1012) * addPaymentInfo Action * ga4 types, properties, and functions * set config fields action poc * set config fields action poc * GA4 all action created * Remove unit test cases files * Added defaultSubscription tag in viewItemList and generateLead Action * added register code for GA4 in broweser-destinations * add customEvent action * set config action & custom event action * Delete snapshot.test.ts * Delete index.test.ts * clean up & add mappings for preset * updated types and removed picklist * Added test cases for GA4 actions * Added test cases * added custom event unit test + cleaned up merge issues and commented code * fixed typo * add back ripe and commandbar to index file * added comment on non using variable * Apply suggestions from code review Co-authored-by: Neek Sandhu * revert set configuration field actions * added updateUser function, and updated event to payload * reverted back gtag function and datalayer setup * add gtag type and remove comment * update yarn.lock file * add viewItemList & disable linter for args * remove gtag.js type dependency * update unit tests * added events to defaultSubscriptions * update index.js --------- Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu * livelike-cloud action destination (PE-41) (#1020) * created livelike-cloud action destination with one trackEvent action and three presets and added unit tests * Update packages/destination-actions/src/destinations/livelike-cloud/trackEvent/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> --------- Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Qualtrics - Fixed timezone issue in unit test (#1026) * Fixed timezone issue in unit test * refactored to use dayjs from lib * generate types (#1028) * PE-47 - Merge Algolia Insights (#1027) * Algolia insights integration (#975) * feat: initial commit after scaffold * feat: clarify insights API and impliment as distinct actions * test: write test for conversion destination * test: write test for click and view destination * test: write auth schema test * create productClick presets * add presets to destination * create all event presets * remove default imports for actions * add testAuthentication method for algolia-insights --------- Co-authored-by: Beatrice Parfait * fixing snapshots and replacing userId and anonId with userToken * fixing timestamps in tests --------- Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait * VWO Cloud Mode Description changes (#1015) * Description changes and changes in utility * Tests altered for new format * Update CODEOWNERS (#1029) * [HEAP-38485] Move to the integrations endpoint (#1016) * [HEAP-32036] First trackEvent implementation (#8) * [HEAP-32037] Send identify and add user properties requests (#9) * [HEAP-32037] Add user properties and migrate anonymous users * Hash anonymous user ID * Use message ID as idempotency key * Throw if parameters are missing * [HEAP-32884] backport filtering event properties (#10) * [HEAP-32884] backport filtering event properties - flat properties from the request payload before sending them to heap * address comments: - remove the embeded object under the util.ts, move it to flattenObj - remove unnecessary tests on identifyUser - import and use the embeded object and flatten object in trackEvent and identifyUser test not part of comments: - rename the file from flattenObj to flat * PR comments * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * remove unused file * save changes * save * fix library * remove accidental file * update tests * remove session id * PR comments * fix the tests * add user properties if more that one identifier is present * Revert "add user properties if more that one identifier is present" This reverts commit bb4fb94684be54c933e9375ae1fc877e876fbbd1. --------- Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: murphpdx * Launchpad Segment Integration (#1010) * Launchpad Segment Integration We are now adding the ability to send events from segment into Launchpad.pm - the mission control for Product teams at scale. We are adding the following: * trackEvent * groupIdentifyUser * identifyUser We have added unit tests and have tested this manually. It requires the following: * apiRegion - EU by default. We have added the US as means of future extensibility. * apiSecret - given by Launchpad.pm while onboarding. * sourceName - to be added. * updating snapshots * Changes implemented following call with Joe Track() [x] Explicitly indicate which fields are required or optional [x] Look for traits and if not context.traits [x] Need description for the Action [x] Make Timestamp field required [x] Make messageId required. It will always be there. [x] getEventProperties: This line looks incorrect: id: payload.event, [x] source mapping looks incorrect: source: integration?.name == 'Iterable' ? 'Iterable' : 'segment', Identify() [x] Description should be updated. [x] identify() calls are often fired when a trait is collected, or when a userId is collected. [x] Maybe use wording: “Creates or updates a user profile, and adds or updates trait values on the user profile…” [x] Refactor the perform() function group() [x] Group Key - Amend description to better explain that group key is a way to connect multiple organizations together [x] groupId field - Should be updated [x] Consider adding anonymousId as a field [x] Handle when there are no traits. * changes * tests passing, removed the check on user_id, anonymous_id since none are required * fixing the types as well * fixing test --------- Co-authored-by: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> * [STRATCONN-1950] Init new destination `Pinterest Conversions API` (#1030) * Init new destination `Pinterest Conversions API` * Update index.ts * Update index.ts * Update index.ts --------- Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> * Try to only pass in NODE_OPTIONS for node18+ (#1032) * Try using conditional for scripts * Remove NODE_OPTIONS from else * Echo the nodeversion * Try setting the node major version and testing that * Use correct variable in condition * Try a different approach * Use bash instead * Try to fix script * Add other remaining scripts * Changing name of new Algolia Insights Integration (#1038) * Register new action destinations (#1036) * Register new action destinations: Launchpad, Livelike Cloud, Twilio Studio, BlackBaud Raisers Edge Nxt, Pinterest Conversions Api * Fix path * add action to pinterest conversions api * add action to pinterest conversions api * add description to pinterest action * Register algolia insights (Actions) --------- Co-authored-by: rvadera12 * Publish - @segment/actions-shared@1.34.0 - @segment/browser-destinations@3.75.0 - @segment/actions-cli-internal@3.128.0 - @segment/actions-cli@3.128.0 - @segment/actions-core@3.52.0 - @segment/action-destinations@3.132.0 - @segment/destination-subscriptions@3.16.0 * Use correct defaultPath for messageId (#1041) * Google ads v11 to v12 (#1018) * gooogle conversion v12 upgrade * changes * bug fix for test cases * gooogle conversion v12 upgrade * changes * bug fix for test cases * flag name changes * flag name change in test cases * review changes * revert ga4-types file change * revert ga4-types file change * revert ga4-types file change --------- Co-authored-by: manoj kumar * [STRATCONN-1779]Add datadog stats for google ads api version (#1046) * adds stats for api version * adds missing statscontext parameter to getCustomVariables * refactor params * Voucherify-Segment.io Integration using action-destinations (#970) * initial destination configuration * identify customer action * add trackEvent * Generate screenEvent and pageEvent * Change the structure of track, page and screen events. * Delete test folder for now * Change the type definition * Delete timestamp * create group event * removed created_at property * hit to localhost address * Add unit tests * Some fixes * Change the URLs in perform method * Update URLs in tests * Delete unused testing authentication fn * Add snapshots * Add ability to pass a custom URL * Set type to required in page and screen events * Delete snapshots * Replace api endpoint (with regions) with custom URL * if there's no userId then use the anonymousId * removed space * added type property to rest of events * changed name of property 'name' to 'event' * Update index.ts * Update generated-types.ts * Delete unnecessary test - 'should throw an error when the name is not provided using page event' * Delete the 'voucherify' prefix * Slightly change the descriptions * generated types * Separate URL functions into separate files Change the file names to be more descriptive. * Reduce the getVoucherifyEndpointURL function * delete * Update the descriptions - Also deleted the 'event' prop from screen/page events and now the 'name' in screen/page event is no longer required. * Commit the generated types * Reduce the number of events to three. Track Custom Event, Identify Customer, Add group to customer metadata * Add customer attributes to traits in upsertCustomer action * Update generated-types.ts * Add testAuthentication * Update names of actions in unit tests * add firstName and lastName * Add email to custom event processing * Delete email from upsertCustomer (leave it only in traits) * Add email to description * Add email to customer processing * Update testAuthentication * update addCustomEvent * Update packages/destination-actions/src/destinations/voucherify/upsertCustomer/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Fix issue with mapping the user attributes and improve Presets * generate types * change email properties * generated types * custom url information * generated types * Update the desc of Custom URL * minor changes from last PR * Update index.ts --------- Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * HGI 372 - Fix Segment Profiles Destination (#1047) * Added `All: false` in Tracking API event * Changed PAPI Token field from `string` to `password` * Fixed minor typos * HGI-368 | Fixed the missing x-signature in request header when batching is enabled (#1043) * worked on HGI-368 Fix * added a unit test case for same fix --------- Co-authored-by: Gaurav Kochar * Mixpanel destination: use user-agent for browser data (#1035) * remove device manufacturer - use user-agent * userAgent is only sent on web * New SalesWings API urls (#1039) * Use new SalesWings API urls * Do not check response status in testAuthentication * Fix testAuthentication * Remove unused file * Mixpanel - identify null id fix (#1033) * fix engage for null user_id * use distinct_id with default for /engage * formatting fix * revert some more auto formatting * formatting * fix test * Remove distinct_id mapping and use null-coalescing * Descriptions changed for VWO Web Destination (#1006) * Descriptions changed * Revert Changes for cloud mode * Test altered for new format * fixing Mixpanel broken test (#1055) --------- Co-authored-by: Nick Aguilar Co-authored-by: Stella Chung Co-authored-by: Simon Co-authored-by: A Murphy Co-authored-by: kishoredevarasettyn <97026912+kishoredevarasettyn@users.noreply.github.com> Co-authored-by: kishoredevarasettyn Co-authored-by: Denis Egorushkin <98813888+denis-egorushkin-sw@users.noreply.github.com> Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov Co-authored-by: maryamsharif <99763167+maryamsharif@users.noreply.github.com> Co-authored-by: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Co-authored-by: Gaurav Kochar Co-authored-by: Nolan Chan Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Co-authored-by: Nolan Chan Co-authored-by: Dan Co-authored-by: Seth Silesky <5115498+silesky@users.noreply.github.com> Co-authored-by: rhall-twilio <103517471+rhall-twilio@users.noreply.github.com> Co-authored-by: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Co-authored-by: SyedWasiHaider Co-authored-by: Dan Lasky Co-authored-by: drakauskas <119876674+drakauskas@users.noreply.github.com> Co-authored-by: Carl Lee Co-authored-by: Wasi Haider Co-authored-by: Thomas Kainrad <7394822+tkainrad@users.noreply.github.com> Co-authored-by: Thomas Kainrad Co-authored-by: aradhakrishnan-twilio <116877054+aradhakrishnan-twilio@users.noreply.github.com> Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu Co-authored-by: Abhishek Kansal Co-authored-by: Sayan Das <109198085+sayan-das-in@users.noreply.github.com> Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait Co-authored-by: Prathamesh Tamanekar Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: murphpdx Co-authored-by: Stefan Sabev Co-authored-by: Logan Luque <98849774+LLuque-twilio@users.noreply.github.com> Co-authored-by: rvadera12 Co-authored-by: Logan Luque Co-authored-by: immanojkumar <117071418+immanojkumar@users.noreply.github.com> Co-authored-by: manoj kumar Co-authored-by: Varadarajan V <109586712+varadarajan-tw@users.noreply.github.com> Co-authored-by: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Sonya Park <68977514+spjtls9@users.noreply.github.com> * DOTORG-847: Add 'Create Gift' action * Merge remote-tracking branch 'upstream/main' into blackbaud-raisers-edge-nxt * Merge branch 'segmentio-main' * Addressed comments * Fixed tests * Ignore timezone for fuzzy date * Removed choices for object fields * Reverted linter changes * Updated snapshot test --------- Co-authored-by: Nick Aguilar Co-authored-by: Stella Chung Co-authored-by: Simon Co-authored-by: A Murphy Co-authored-by: kishoredevarasettyn <97026912+kishoredevarasettyn@users.noreply.github.com> Co-authored-by: kishoredevarasettyn Co-authored-by: Denis Egorushkin <98813888+denis-egorushkin-sw@users.noreply.github.com> Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov Co-authored-by: maryamsharif <99763167+maryamsharif@users.noreply.github.com> Co-authored-by: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Co-authored-by: Gaurav Kochar Co-authored-by: Nolan Chan Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Co-authored-by: Nolan Chan Co-authored-by: Dan Co-authored-by: Seth Silesky <5115498+silesky@users.noreply.github.com> Co-authored-by: rhall-twilio <103517471+rhall-twilio@users.noreply.github.com> Co-authored-by: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Co-authored-by: SyedWasiHaider Co-authored-by: Dan Lasky Co-authored-by: drakauskas <119876674+drakauskas@users.noreply.github.com> Co-authored-by: Carl Lee Co-authored-by: Wasi Haider Co-authored-by: Thomas Kainrad <7394822+tkainrad@users.noreply.github.com> Co-authored-by: Thomas Kainrad Co-authored-by: aradhakrishnan-twilio <116877054+aradhakrishnan-twilio@users.noreply.github.com> Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu Co-authored-by: Abhishek Kansal Co-authored-by: Sayan Das <109198085+sayan-das-in@users.noreply.github.com> Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait Co-authored-by: Prathamesh Tamanekar Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: murphpdx Co-authored-by: Stefan Sabev Co-authored-by: Logan Luque <98849774+LLuque-twilio@users.noreply.github.com> Co-authored-by: rvadera12 Co-authored-by: Logan Luque Co-authored-by: immanojkumar <117071418+immanojkumar@users.noreply.github.com> Co-authored-by: manoj kumar Co-authored-by: Varadarajan V <109586712+varadarajan-tw@users.noreply.github.com> Co-authored-by: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Sonya Park <68977514+spjtls9@users.noreply.github.com> Co-authored-by: Hywel Wong --- .../__snapshots__/snapshot.test.ts.snap | 51 +- .../__tests__/snapshot.test.ts | 4 +- .../blackbaud-raisers-edge-nxt/api/index.ts | 484 ++++++++- .../constants/index.ts | 4 +- .../__snapshots__/snapshot.test.ts.snap | 22 + .../createGift/__tests__/index.test.ts | 84 ++ .../createGift/__tests__/snapshot.test.ts | 79 ++ .../createGift/fixtures.ts | 65 ++ .../createGift/generated-types.ts | 170 +++ .../createGift/index.ts | 261 +++++ .../__snapshots__/snapshot.test.ts.snap | 34 +- .../__tests__/index.test.ts | 82 +- .../fixtures.ts | 2 +- .../generated-types.ts | 4 + .../index.ts | 980 ++++++------------ .../blackbaud-raisers-edge-nxt/index.ts | 2 + .../blackbaud-raisers-edge-nxt/types/index.ts | 79 +- .../blackbaud-raisers-edge-nxt/utils/index.ts | 199 +++- 18 files changed, 1795 insertions(+), 811 deletions(-) create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/fixtures.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/index.ts diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/__snapshots__/snapshot.test.ts.snap index 4a48abe88e..3d7321fcd7 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,21 +1,44 @@ // 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: createGift action - all fields 1`] = ` +Object { + "birthdate": Object { + "d": "1", + "m": "2", + "y": "2021", + }, + "first": "D0dEn", + "gender": "D0dEn", + "income": "D0dEn", + "last": "D0dEn", + "lookup_id": "D0dEn", +} +`; -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: createGift action - required fields 1`] = ` +Object { + "lookup_id": "D0dEn", +} +`; -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)", - ], +exports[`Testing snapshot for actions-blackbaud-raisers-edge-nxt destination: createOrUpdateIndividualConstituent action - all fields 1`] = ` +Object { + "birthdate": Object { + "d": "1", + "m": "2", + "y": "2021", }, + "first": "hUXnkT]ixm7mm*HX", + "gender": "hUXnkT]ixm7mm*HX", + "income": "hUXnkT]ixm7mm*HX", + "last": "hUXnkT]ixm7mm*HX", + "lookup_id": "hUXnkT]ixm7mm*HX", +} +`; + +exports[`Testing snapshot for actions-blackbaud-raisers-edge-nxt destination: createOrUpdateIndividualConstituent action - required fields 1`] = ` +Object { + "last": "Smith", + "lookup_id": "hUXnkT]ixm7mm*HX", } `; diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/snapshot.test.ts index 6d3d45a07f..cf6503a644 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/snapshot.test.ts @@ -13,7 +13,9 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) - eventData.last = 'Smith' + if (actionSlug === 'createOrUpdateIndividualConstituent') { + eventData.last = 'Smith' + } nock(/.*/).persist().get(/.*/).reply(200, {}) nock(/.*/).persist().patch(/.*/).reply(200, {}) diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/api/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/api/index.ts index 30be7c5ea1..2ab79c2916 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/api/index.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/api/index.ts @@ -1,5 +1,21 @@ -import type { RequestClient, ModifiedResponse } from '@segment/actions-core' -import { SKY_API_BASE_URL } from '../constants' +import { IntegrationError, ModifiedResponse, RequestClient, RetryableError } from '@segment/actions-core' +import { SKY_API_CONSTITUENT_URL, SKY_API_GIFTS_URL } from '../constants' +import { + Address, + Constituent, + CreateConstituentResult, + Email, + ExistingAddress, + ExistingConstituentResult, + ExistingEmail, + ExistingOnlinePresence, + ExistingPhone, + Gift, + OnlinePresence, + Phone, + UpdateConstituentResult +} from '../types' +import { filterObjectListByMatchFields, isRequestErrorRetryable } from '../utils' export class BlackbaudSkyApi { request: RequestClient @@ -8,24 +24,94 @@ export class BlackbaudSkyApi { this.request = request } - async getExistingConstituents(searchField: string, searchText: string): Promise { + async searchForConstituents(searchField: string, searchText: string): Promise { return this.request( - `${SKY_API_BASE_URL}/constituents/search?search_field=${searchField}&search_text=${searchText}`, + `${SKY_API_CONSTITUENT_URL}/constituents/search?search_field=${searchField}&search_text=${searchText}`, { method: 'get' } ) } - async createConstituent(constituentData: object): Promise { - return this.request(`${SKY_API_BASE_URL}/constituents`, { + async getExistingConstituent(emailData?: Partial, lookupId?: string): Promise { + let constituentId = undefined + + // default to searching by email + let searchField = 'email_address' + let searchText = emailData?.address || '' + + if (lookupId) { + // search by lookup_id if one is provided + searchField = 'lookup_id' + searchText = lookupId + } + + const constituentSearchResponse = await this.searchForConstituents(searchField, searchText) + const constituentSearchResults = await constituentSearchResponse.json() + + if (constituentSearchResults.count > 1) { + // multiple existing constituents, throw an error + throw new IntegrationError('Multiple records returned for given traits', 'MULTIPLE_EXISTING_RECORDS', 400) + } else if (constituentSearchResults.count === 1) { + // existing constituent + constituentId = constituentSearchResults.value[0].id + } else if (constituentSearchResults.count !== 0) { + // if constituent count is not >= 0, something went wrong + throw new IntegrationError('Unexpected constituent record count for given traits', 'UNEXPECTED_RECORD_COUNT', 500) + } + + return Promise.resolve({ + id: constituentId + }) + } + + async createConstituent(constituentData: Constituent): Promise { + return this.request(`${SKY_API_CONSTITUENT_URL}/constituents`, { method: 'post', json: constituentData }) } - async updateConstituent(constituentId: string, constituentData: object): Promise { - return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}`, { + async createConstituentWithRelatedObjects( + constituentData: Constituent, + addressData: Partial
, + emailData: Partial, + onlinePresenceData: Partial, + phoneData: Partial + ): Promise { + // hardcode type + constituentData.type = 'Individual' + if (!constituentData.last) { + // last name is required to create a new constituent + // no last name, throw an error + throw new IntegrationError('Missing last name value', 'MISSING_REQUIRED_FIELD', 400) + } + // request has last name + // append other data objects to constituent + if (Object.keys(addressData).length > 0) { + constituentData.address = addressData as Address + } + if (Object.keys(emailData).length > 0) { + constituentData.email = emailData as Email + } + if (Object.keys(onlinePresenceData).length > 0) { + constituentData.online_presence = onlinePresenceData as OnlinePresence + } + if (Object.keys(phoneData).length > 0) { + constituentData.phone = phoneData as Phone + } + + // create constituent + const createConstituentResponse = await this.createConstituent(constituentData) + const constituentResult = await createConstituentResponse.json() + + return Promise.resolve({ + id: constituentResult.id + }) + } + + async updateConstituent(constituentId: string, constituentData: Partial): Promise { + return this.request(`${SKY_API_CONSTITUENT_URL}/constituents/${constituentId}`, { method: 'patch', json: constituentData, throwHttpErrors: false @@ -33,28 +119,28 @@ export class BlackbaudSkyApi { } async getConstituentAddressList(constituentId: string): Promise { - return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/addresses?include_inactive=true`, { + return this.request(`${SKY_API_CONSTITUENT_URL}/constituents/${constituentId}/addresses?include_inactive=true`, { method: 'get', throwHttpErrors: false }) } - async createConstituentAddress(constituentId: string, constituentAddressData: object): Promise { - return this.request(`${SKY_API_BASE_URL}/addresses`, { + async createConstituentAddress(constituentId: string, addressData: Address): Promise { + return this.request(`${SKY_API_CONSTITUENT_URL}/addresses`, { method: 'post', json: { - ...constituentAddressData, + ...addressData, constituent_id: constituentId }, throwHttpErrors: false }) } - async updateConstituentAddressById(addressId: string, constituentAddressData: object): Promise { - return this.request(`${SKY_API_BASE_URL}/addresses/${addressId}`, { + async updateConstituentAddressById(addressId: string, addressData: Address): Promise { + return this.request(`${SKY_API_CONSTITUENT_URL}/addresses/${addressId}`, { method: 'patch', json: { - ...constituentAddressData, + ...addressData, inactive: false }, throwHttpErrors: false @@ -62,28 +148,31 @@ export class BlackbaudSkyApi { } async getConstituentEmailList(constituentId: string): Promise { - return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/emailaddresses?include_inactive=true`, { - method: 'get', - throwHttpErrors: false - }) + return this.request( + `${SKY_API_CONSTITUENT_URL}/constituents/${constituentId}/emailaddresses?include_inactive=true`, + { + method: 'get', + throwHttpErrors: false + } + ) } - async createConstituentEmail(constituentId: string, constituentEmailData: object): Promise { - return this.request(`${SKY_API_BASE_URL}/emailaddresses`, { + async createConstituentEmail(constituentId: string, emailData: Email): Promise { + return this.request(`${SKY_API_CONSTITUENT_URL}/emailaddresses`, { method: 'post', json: { - ...constituentEmailData, + ...emailData, constituent_id: constituentId }, throwHttpErrors: false }) } - async updateConstituentEmailById(emailId: string, constituentEmailData: object): Promise { - return this.request(`${SKY_API_BASE_URL}/emailaddresses/${emailId}`, { + async updateConstituentEmailById(emailId: string, emailData: Email): Promise { + return this.request(`${SKY_API_CONSTITUENT_URL}/emailaddresses/${emailId}`, { method: 'patch', json: { - ...constituentEmailData, + ...emailData, inactive: false }, throwHttpErrors: false @@ -91,20 +180,23 @@ export class BlackbaudSkyApi { } async getConstituentOnlinePresenceList(constituentId: string): Promise { - return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/onlinepresences?include_inactive=true`, { - method: 'get', - throwHttpErrors: false - }) + return this.request( + `${SKY_API_CONSTITUENT_URL}/constituents/${constituentId}/onlinepresences?include_inactive=true`, + { + method: 'get', + throwHttpErrors: false + } + ) } async createConstituentOnlinePresence( constituentId: string, - constituentOnlinePresenceData: object + onlinePresenceData: OnlinePresence ): Promise { - return this.request(`${SKY_API_BASE_URL}/onlinepresences`, { + return this.request(`${SKY_API_CONSTITUENT_URL}/onlinepresences`, { method: 'post', json: { - ...constituentOnlinePresenceData, + ...onlinePresenceData, constituent_id: constituentId }, throwHttpErrors: false @@ -113,12 +205,12 @@ export class BlackbaudSkyApi { async updateConstituentOnlinePresenceById( onlinePresenceId: string, - constituentOnlinePresenceData: object + onlinePresenceData: OnlinePresence ): Promise { - return this.request(`${SKY_API_BASE_URL}/onlinepresences/${onlinePresenceId}`, { + return this.request(`${SKY_API_CONSTITUENT_URL}/onlinepresences/${onlinePresenceId}`, { method: 'patch', json: { - ...constituentOnlinePresenceData, + ...onlinePresenceData, inactive: false }, throwHttpErrors: false @@ -126,31 +218,339 @@ export class BlackbaudSkyApi { } async getConstituentPhoneList(constituentId: string): Promise { - return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/phones?include_inactive=true`, { + return this.request(`${SKY_API_CONSTITUENT_URL}/constituents/${constituentId}/phones?include_inactive=true`, { method: 'get', throwHttpErrors: false }) } - async createConstituentPhone(constituentId: string, constituentPhoneData: object): Promise { - return this.request(`${SKY_API_BASE_URL}/phones`, { + async createConstituentPhone(constituentId: string, phoneData: Phone): Promise { + return this.request(`${SKY_API_CONSTITUENT_URL}/phones`, { method: 'post', json: { - ...constituentPhoneData, + ...phoneData, constituent_id: constituentId }, throwHttpErrors: false }) } - async updateConstituentPhoneById(phoneId: string, constituentPhoneData: object): Promise { - return this.request(`${SKY_API_BASE_URL}/phones/${phoneId}`, { + async updateConstituentWithRelatedObjects( + constituentId: string, + constituentData: Partial, + addressData: Partial
, + emailData: Partial, + onlinePresenceData: Partial, + phoneData: Partial + ): Promise { + // aggregate all errors + const integrationErrors = [] + if (Object.keys(constituentData).length > 0) { + // request has at least one constituent field to update + // update constituent + const updateConstituentResponse = await this.updateConstituent(constituentId, constituentData) + if (updateConstituentResponse.status !== 200) { + const statusCode = updateConstituentResponse.status + const errorMessage = statusCode + ? `${statusCode} error occurred when updating constituent` + : 'Error occurred when updating constituent' + if (isRequestErrorRetryable(statusCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (Object.keys(addressData).length > 0) { + // request has address data + // get existing addresses + const getConstituentAddressListResponse = await this.getConstituentAddressList(constituentId) + let updateAddressErrorCode = undefined + if (getConstituentAddressListResponse.status !== 200) { + updateAddressErrorCode = getConstituentAddressListResponse.status + } else { + const constituentAddressListResults = await getConstituentAddressListResponse.json() + + // check address list for one that matches request + let existingAddress: ExistingAddress | undefined = undefined + if (constituentAddressListResults.count > 0) { + existingAddress = filterObjectListByMatchFields(constituentAddressListResults.value, addressData, [ + 'address_lines', + 'city', + 'postal_code', + 'state' + ]) as ExistingAddress | undefined + } + + if (!existingAddress) { + // new address + // if this is the only address, make it primary + if (addressData.primary !== false && constituentAddressListResults.count === 0) { + addressData.primary = true + } + // create address + const createConstituentAddressResponse = await this.createConstituentAddress( + constituentId, + addressData as Address + ) + if (createConstituentAddressResponse.status !== 200) { + updateAddressErrorCode = createConstituentAddressResponse.status + } + } else { + // existing address + if ( + existingAddress.inactive || + (addressData.do_not_mail !== undefined && addressData.do_not_mail !== existingAddress.do_not_mail) || + (addressData.primary !== undefined && + addressData.primary && + addressData.primary !== existingAddress.primary) || + addressData.type !== existingAddress.type + ) { + // request has at least one address field to update + // update address + const updateConstituentAddressByIdResponse = await this.updateConstituentAddressById( + existingAddress.id, + addressData as Address + ) + if (updateConstituentAddressByIdResponse.status !== 200) { + updateAddressErrorCode = updateConstituentAddressByIdResponse.status + } + } + } + } + + if (updateAddressErrorCode) { + const errorMessage = updateAddressErrorCode + ? `${updateAddressErrorCode} error occurred when updating constituent address` + : 'Error occurred when updating constituent address' + if (isRequestErrorRetryable(updateAddressErrorCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (Object.keys(emailData).length > 0) { + // request has email data + // get existing addresses + const getConstituentEmailListResponse = await this.getConstituentEmailList(constituentId) + let updateEmailErrorCode = undefined + if (getConstituentEmailListResponse.status !== 200) { + updateEmailErrorCode = getConstituentEmailListResponse.status + } else { + const constituentEmailListResults = await getConstituentEmailListResponse.json() + + // check email list for one that matches request + let existingEmail: ExistingEmail | undefined = undefined + if (constituentEmailListResults.count > 0) { + existingEmail = filterObjectListByMatchFields(constituentEmailListResults.value, emailData, ['address']) as + | ExistingEmail + | undefined + } + + if (!existingEmail) { + // new email + // if this is the only email, make it primary + if (emailData.primary !== false && constituentEmailListResults.count === 0) { + emailData.primary = true + } + // create email + const createConstituentEmailResponse = await this.createConstituentEmail(constituentId, emailData as Email) + if (createConstituentEmailResponse.status !== 200) { + updateEmailErrorCode = createConstituentEmailResponse.status + } + } else { + // existing email + if ( + existingEmail.inactive || + (emailData.do_not_email !== undefined && emailData.do_not_email !== existingEmail.do_not_email) || + (emailData.primary !== undefined && emailData.primary && emailData.primary !== existingEmail.primary) || + emailData.type !== existingEmail.type + ) { + // request has at least one email field to update + // update email + const updateConstituentEmailByIdResponse = await this.updateConstituentEmailById( + existingEmail.id, + emailData as Email + ) + if (updateConstituentEmailByIdResponse.status !== 200) { + updateEmailErrorCode = updateConstituentEmailByIdResponse.status + } + } + } + } + + if (updateEmailErrorCode) { + const errorMessage = updateEmailErrorCode + ? `${updateEmailErrorCode} error occurred when updating constituent email` + : 'Error occurred when updating constituent email' + if (isRequestErrorRetryable(updateEmailErrorCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (Object.keys(onlinePresenceData).length > 0) { + // request has online presence data + // get existing online presences + const getConstituentOnlinePresenceListResponse = await this.getConstituentOnlinePresenceList(constituentId) + let updateOnlinePresenceErrorCode = undefined + if (getConstituentOnlinePresenceListResponse.status !== 200) { + updateOnlinePresenceErrorCode = getConstituentOnlinePresenceListResponse.status + } else { + const constituentOnlinePresenceListResults = await getConstituentOnlinePresenceListResponse.json() + + // check online presence list for one that matches request + let existingOnlinePresence: ExistingOnlinePresence | undefined = undefined + if (constituentOnlinePresenceListResults.count > 0) { + existingOnlinePresence = filterObjectListByMatchFields( + constituentOnlinePresenceListResults.value, + onlinePresenceData, + ['address'] + ) as ExistingOnlinePresence | undefined + } + + if (!existingOnlinePresence) { + // new online presence + // if this is the only online presence, make it primary + if (onlinePresenceData.primary !== false && constituentOnlinePresenceListResults.count === 0) { + onlinePresenceData.primary = true + } + // create online presence + const createConstituentOnlinePresenceResponse = await this.createConstituentOnlinePresence( + constituentId, + onlinePresenceData as OnlinePresence + ) + if (createConstituentOnlinePresenceResponse.status !== 200) { + updateOnlinePresenceErrorCode = createConstituentOnlinePresenceResponse.status + } + } else { + // existing online presence + if ( + existingOnlinePresence.inactive || + (onlinePresenceData.primary !== undefined && + onlinePresenceData.primary !== existingOnlinePresence.primary) || + onlinePresenceData.type !== existingOnlinePresence.type + ) { + // request has at least one online presence field to update + // update online presence + const updateConstituentOnlinePresenceByIdResponse = await this.updateConstituentOnlinePresenceById( + existingOnlinePresence.id, + onlinePresenceData as OnlinePresence + ) + if (updateConstituentOnlinePresenceByIdResponse.status !== 200) { + updateOnlinePresenceErrorCode = updateConstituentOnlinePresenceByIdResponse.status + } + } + } + } + + if (updateOnlinePresenceErrorCode) { + const errorMessage = updateOnlinePresenceErrorCode + ? `${updateOnlinePresenceErrorCode} error occurred when updating constituent online presence` + : 'Error occurred when updating constituent online presence' + if (isRequestErrorRetryable(updateOnlinePresenceErrorCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (Object.keys(phoneData).length > 0) { + // request has phone data + // get existing phones + const getConstituentPhoneListResponse = await this.getConstituentPhoneList(constituentId) + let updatePhoneErrorCode = undefined + if (getConstituentPhoneListResponse.status !== 200) { + updatePhoneErrorCode = getConstituentPhoneListResponse.status + } else { + const constituentPhoneListResults = await getConstituentPhoneListResponse.json() + + // check phone list for one that matches request + let existingPhone: ExistingPhone | undefined = undefined + if (constituentPhoneListResults.count > 0) { + existingPhone = filterObjectListByMatchFields(constituentPhoneListResults.value, phoneData, [ + 'int:number' + ]) as ExistingPhone | undefined + } + + if (!existingPhone) { + // new phone + // if this is the only phone, make it primary + if (phoneData.primary !== false && constituentPhoneListResults.count === 0) { + phoneData.primary = true + } + // create phone + const createConstituentPhoneResponse = await this.createConstituentPhone(constituentId, phoneData as Phone) + if (createConstituentPhoneResponse.status !== 200) { + updatePhoneErrorCode = createConstituentPhoneResponse.status + } + } else { + // existing phone + if ( + existingPhone.inactive || + (phoneData.do_not_call !== undefined && phoneData.do_not_call !== existingPhone.do_not_call) || + (phoneData.primary !== undefined && phoneData.primary !== existingPhone.primary) || + phoneData.type !== existingPhone.type + ) { + // request has at least one phone field to update + // update phone + const updateConstituentPhoneByIdResponse = await this.updateConstituentPhoneById( + existingPhone.id, + phoneData as Phone + ) + if (updateConstituentPhoneByIdResponse.status !== 200) { + updatePhoneErrorCode = updateConstituentPhoneByIdResponse.status + } + } + } + } + + if (updatePhoneErrorCode) { + const errorMessage = updatePhoneErrorCode + ? `${updatePhoneErrorCode} error occurred when updating constituent online presence` + : 'Error occurred when updating constituent online presence' + if (isRequestErrorRetryable(updatePhoneErrorCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (integrationErrors.length > 0) { + throw new IntegrationError( + 'One or more errors occurred when updating existing constituent: ' + integrationErrors.join(', '), + 'UPDATE_CONSTITUENT_ERROR', + 500 + ) + } + + return Promise.resolve({ + id: constituentId + }) + } + + async updateConstituentPhoneById(phoneId: string, phoneData: Partial): Promise { + return this.request(`${SKY_API_CONSTITUENT_URL}/phones/${phoneId}`, { method: 'patch', json: { - ...constituentPhoneData, + ...phoneData, inactive: false }, throwHttpErrors: false }) } + + async createGift(giftData: Gift): Promise { + return this.request(`${SKY_API_GIFTS_URL}/gifts`, { + method: 'post', + json: giftData + }) + } } diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/constants/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/constants/index.ts index 2edb2bac05..9008e60545 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/constants/index.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/constants/index.ts @@ -1,2 +1,4 @@ -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' +export const SKY_API_BASE_URL = 'https://api.sky.blackbaud.com' +export const SKY_API_CONSTITUENT_URL = `${SKY_API_BASE_URL}/constituent/v1` +export const SKY_API_GIFTS_URL = `${SKY_API_BASE_URL}/gift/v1` diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..264ac2f746 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createGift destination action: all fields 1`] = ` +Object { + "birthdate": Object { + "d": "1", + "m": "2", + "y": "2021", + }, + "first": "O#lbz", + "gender": "O#lbz", + "income": "O#lbz", + "last": "O#lbz", + "lookup_id": "O#lbz", +} +`; + +exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createGift destination action: required fields 1`] = ` +Object { + "lookup_id": "O#lbz", +} +`; diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/index.test.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/index.test.ts new file mode 100644 index 0000000000..0607d278a3 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/index.test.ts @@ -0,0 +1,84 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration, IntegrationError } from '@segment/actions-core' +import Destination from '../../index' +import { SKY_API_CONSTITUENT_URL, SKY_API_GIFTS_URL } from '../../constants' +import { trackEventData, trackEventDataNewConstituent, trackEventDataNoConstituent } from '../fixtures' + +const testDestination = createTestIntegration(Destination) + +const mapping = { + constituent_email: { + address: { + '@path': '$.properties.email' + }, + type: { + '@path': '$.properties.emailType' + } + }, + constituent_id: { + '@path': '$.properties.constituentId' + }, + fund_id: { + '@path': '$.properties.fundId' + }, + payment_method: { + '@path': '$.properties.paymentMethod' + } +} + +describe('BlackbaudRaisersEdgeNxt.createGift', () => { + test('should create a new gift successfully', async () => { + const event = createTestEvent(trackEventData) + + nock(SKY_API_GIFTS_URL).post('/gifts').reply(200, { + id: '1000' + }) + + await expect( + testDestination.testAction('createGift', { + event, + mapping, + useDefaultMappings: true + }) + ).resolves.not.toThrowError() + }) + + test('should create a new constituent and associate gift with it', async () => { + const event = createTestEvent(trackEventDataNewConstituent) + + nock(SKY_API_CONSTITUENT_URL) + .get('/constituents/search?search_field=email_address&search_text=john@example.biz') + .reply(200, { + count: 0, + value: [] + }) + + nock(SKY_API_CONSTITUENT_URL).post('/constituents').reply(200, { + id: '456' + }) + + nock(SKY_API_GIFTS_URL).post('/gifts').reply(200, { + id: '1001' + }) + + await expect( + testDestination.testAction('createGift', { + event, + mapping, + useDefaultMappings: true + }) + ).resolves.not.toThrowError() + }) + + test('should throw an IntegrationError if no constituent provided', async () => { + const event = createTestEvent(trackEventDataNoConstituent) + + await expect( + testDestination.testAction('createGift', { + event, + mapping, + useDefaultMappings: true + }) + ).rejects.toThrowError(new IntegrationError('Missing constituent_id value', 'MISSING_REQUIRED_FIELD', 400)) + }) +}) diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..2fea0211a8 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/snapshot.test.ts @@ -0,0 +1,79 @@ +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 actionSlug = 'createGift' +const destinationSlug = 'BlackbaudRaisersEdgeNxt' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + eventData.date = '2023-01-01T01:00:00Z' + + 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('all fields', async () => { + 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() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/fixtures.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/fixtures.ts new file mode 100644 index 0000000000..341dd86602 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/fixtures.ts @@ -0,0 +1,65 @@ +import { SegmentEvent } from '@segment/actions-core' + +// track events +export const trackEventData: Partial = { + type: 'track', + properties: { + constituentId: '123', + fundId: '1', + paymentMethod: 'CreditCard', + revenue: 100 + } +} + +export const trackEventDataNewConstituent: Partial = { + type: 'track', + properties: { + email: 'john@example.biz', + emailType: 'Personal', + firstName: 'John', + lastName: 'Doe', + fundId: '1', + paymentMethod: 'CreditCard', + revenue: 100 + } +} + +export const trackEventDataNoConstituent: Partial = { + type: 'track', + properties: { + fundId: '1', + paymentMethod: 'CreditCard', + revenue: 100 + } +} + +// gift data +export const giftPayload = { + amount: { + value: 100 + }, + constituent_id: '123', + gift_splits: [ + { + amount: { + value: 100 + }, + fund_id: '1' + } + ], + payments: [ + { + payment_method: 'CreditCard' + } + ] +} + +// constituent data +export const constituentPayload = { + email: { + address: 'john@example.biz', + type: 'Personal' + }, + first: 'John', + last: 'Doe' +} diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/generated-types.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/generated-types.ts new file mode 100644 index 0000000000..2ec3f4f67b --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/generated-types.ts @@ -0,0 +1,170 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The gift acknowledgement. + */ + acknowledgement?: { + /** + * The date associated with the acknowledgement in ISO-8601 format. + */ + date?: string | number + /** + * The status of the acknowledgement. Available values are: ACKNOWLEDGED, NEEDSACKNOWLEDGEMENT, and DONOTACKNOWLEDGE. + */ + status?: string + } + /** + * The monetary amount of the gift in number format, e.g. 12.34 + */ + amount: number + /** + * The check date in ISO-8601 format. + */ + check_date?: string | number + /** + * The check number in string format, e.g. "12345" + */ + check_number?: string + /** + * The gift date in ISO-8601 format. + */ + date?: string | number + /** + * The ID of the fund associated with the gift. + */ + fund_id: string + /** + * The status of the gift. Available values are "Active", "Held", "Terminated", "Completed", and "Cancelled". + */ + gift_status?: string + /** + * Indicates whether the gift is anonymous. + */ + is_anonymous?: boolean + /** + * The recurring gift associated with the payment being added. When adding a recurring gift payment, a linked_gifts field must be included as an array of strings with the ID of the recurring gift to which the payment is linked. + */ + linked_gifts?: string[] + /** + * The organization-defined identifier for the gift. + */ + lookup_id?: string + /** + * The payment method. Available values are "Cash", "CreditCard", "PersonalCheck", "DirectDebit", "Other", "PayPal", or "Venmo". + */ + payment_method: string + /** + * The date that the gift was posted to general ledger in ISO-8601 format. + */ + post_date?: string | number + /** + * The general ledger post status of the gift. Available values are "Posted", "NotPosted", and "DoNotPost". + */ + post_status?: string + /** + * The gift receipt. + */ + receipt?: { + /** + * The date that the gift was receipted. Includes an offset from UTC in ISO-8601 format: 1969-11-21T10:29:43. + */ + date?: string | number + /** + * The receipt status of the gift. Available values are RECEIPTED, NEEDSRECEIPT, and DONOTRECEIPT. + */ + status?: string + } + /** + * The recurring gift schedule. When adding a recurring gift, a schedule is required. + */ + recurring_gift_schedule?: { + /** + * Date the recurring gift should end in ISO-8601 format. + */ + end_date?: string | number + /** + * Installment frequency of the recurring gift to add. Available values are WEEKLY, EVERY_TWO_WEEKS, EVERY_FOUR_WEEKS, MONTHLY, QUARTERLY, ANNUALLY. + */ + frequency?: string + /** + * Date the recurring gift should start in ISO-8601 format. + */ + start_date?: string | number + } + /** + * The subtype of the gift. + */ + subtype?: string + /** + * The gift type. Available values are "Donation", "Other", "GiftInKind", "RecurringGift", and "RecurringGiftPayment". + */ + type?: string + /** + * The constituent's address. + */ + constituent_address?: { + address_lines?: string + city?: string + country?: string + do_not_mail?: boolean + postal_code?: string + primary?: boolean + state?: string + type?: string + } + /** + * The constituent's birthdate. + */ + constituent_birthdate?: string | number + /** + * The ID of the constituent. + */ + constituent_id?: string + /** + * The constituent's email address. + */ + constituent_email?: { + address?: string + do_not_email?: boolean + primary?: boolean + type?: string + } + /** + * The constituent's first name up to 50 characters. + */ + constituent_first?: string + /** + * The constituent's gender. + */ + constituent_gender?: string + /** + * The constituent's income. + */ + constituent_income?: string + /** + * The constituent's last name up to 100 characters. This is required to create a constituent. + */ + constituent_last?: string + /** + * The organization-defined identifier for the constituent. + */ + constituent_lookup_id?: string + /** + * The constituent's online presence. + */ + constituent_online_presence?: { + address?: string + primary?: boolean + type?: string + } + /** + * The constituent's phone number. + */ + constituent_phone?: { + do_not_call?: boolean + number?: string + primary?: boolean + type?: string + } +} diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/index.ts new file mode 100644 index 0000000000..6b3f70dd29 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/index.ts @@ -0,0 +1,261 @@ +import { ActionDefinition, ExecuteInput, InputField, IntegrationError, RequestFn } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { + fields as createOrUpdateIndividualConstituentFields, + perform as performCreateOrUpdateIndividualConstituent +} from '../createOrUpdateIndividualConstituent' +import { BlackbaudSkyApi } from '../api' +import { Gift, StringIndexedObject } from '../types' +import { buildConstituentPayloadFromPayload, buildGiftDataFromPayload } from '../utils' + +const fields: Record = { + acknowledgement: { + label: 'Acknowledgement', + description: 'The gift acknowledgement.', + type: 'object', + defaultObjectUI: 'keyvalue:only', + additionalProperties: false, + properties: { + date: { + label: 'Date', + type: 'datetime', + description: 'The date associated with the acknowledgement in ISO-8601 format.' + }, + status: { + label: 'Status', + type: 'string', + description: + 'The status of the acknowledgement. Available values are: ACKNOWLEDGED, NEEDSACKNOWLEDGEMENT, and DONOTACKNOWLEDGE.' + } + }, + default: { + date: { + '@path': '$.properties.acknowledgement.date' + }, + status: { + '@path': '$.properties.acknowledgement.status' + } + } + }, + amount: { + label: 'Gift Amount', + description: 'The monetary amount of the gift in number format, e.g. 12.34', + type: 'number', + required: true, + default: { + '@path': '$.properties.revenue' + } + }, + check_date: { + label: 'Check Date', + description: 'The check date in ISO-8601 format.', + type: 'datetime' + }, + check_number: { + label: 'Check Number', + description: 'The check number in string format, e.g. "12345"', + type: 'string' + }, + date: { + label: 'Gift Date', + description: 'The gift date in ISO-8601 format.', + type: 'datetime' + }, + fund_id: { + label: 'Fund ID', + description: 'The ID of the fund associated with the gift.', + type: 'string', + required: true + }, + gift_status: { + label: 'Gift Status', + description: + 'The status of the gift. Available values are "Active", "Held", "Terminated", "Completed", and "Cancelled".', + type: 'string', + choices: [ + { label: 'Active', value: 'Active' }, + { label: 'Held', value: 'Held' }, + { label: 'Terminated', value: 'Terminated' }, + { label: 'Completed', value: 'Completed' }, + { label: 'Cancelled', value: 'Cancelled' } + ] + }, + is_anonymous: { + label: 'Is Anonymous', + description: 'Indicates whether the gift is anonymous.', + type: 'boolean' + }, + linked_gifts: { + label: 'Linked Gifts', + description: + 'The recurring gift associated with the payment being added. When adding a recurring gift payment, a linked_gifts field must be included as an array of strings with the ID of the recurring gift to which the payment is linked.', + type: 'string', + multiple: true + }, + lookup_id: { + label: 'Lookup ID', + description: 'The organization-defined identifier for the gift.', + type: 'string' + }, + payment_method: { + label: 'Payment Method', + description: + 'The payment method. Available values are "Cash", "CreditCard", "PersonalCheck", "DirectDebit", "Other", "PayPal", or "Venmo".', + type: 'string', + required: true, + choices: [ + { label: 'Cash', value: 'Cash' }, + { label: 'Credit Card', value: 'CreditCard' }, + { label: 'Personal Check', value: 'PersonalCheck' }, + { label: 'Direct Debit', value: 'DirectDebit' }, + { label: 'Other', value: 'Other' }, + { label: 'PayPal', value: 'PayPal' }, + { label: 'Venmo', value: 'Venmo' } + ] + }, + post_date: { + label: 'Post Date', + description: 'The date that the gift was posted to general ledger in ISO-8601 format.', + type: 'datetime' + }, + post_status: { + label: 'Post Status', + description: + 'The general ledger post status of the gift. Available values are "Posted", "NotPosted", and "DoNotPost".', + type: 'string', + default: 'NotPosted', + choices: [ + { label: 'Posted', value: 'Posted' }, + { label: 'Not Posted', value: 'NotPosted' }, + { label: 'Do Not Post', value: 'DoNotPost' } + ] + }, + receipt: { + label: 'Receipt', + description: 'The gift receipt.', + type: 'object', + defaultObjectUI: 'keyvalue:only', + additionalProperties: false, + properties: { + date: { + label: 'Date', + type: 'datetime', + description: + 'The date that the gift was receipted. Includes an offset from UTC in ISO-8601 format: 1969-11-21T10:29:43.' + }, + status: { + label: 'Status', + type: 'string', + description: 'The receipt status of the gift. Available values are RECEIPTED, NEEDSRECEIPT, and DONOTRECEIPT.' + } + }, + default: { + date: { + '@path': '$.properties.receipt.date' + }, + status: { + '@path': '$.properties.receipt.status' + } + } + }, + recurring_gift_schedule: { + label: 'Recurring Gift Schedule', + description: 'The recurring gift schedule. When adding a recurring gift, a schedule is required.', + type: 'object', + defaultObjectUI: 'keyvalue:only', + additionalProperties: false, + properties: { + end_date: { + label: 'End Date', + type: 'datetime', + description: 'Date the recurring gift should end in ISO-8601 format.' + }, + frequency: { + label: 'Frequency', + type: 'string', + description: + 'Installment frequency of the recurring gift to add. Available values are WEEKLY, EVERY_TWO_WEEKS, EVERY_FOUR_WEEKS, MONTHLY, QUARTERLY, ANNUALLY.' + }, + start_date: { + label: 'Start Date', + type: 'datetime', + description: 'Date the recurring gift should start in ISO-8601 format.' + } + }, + default: { + end_date: { + '@path': '$.properties.recurring_gift_schedule.end_date' + }, + frequency: { + '@path': '$.properties.recurring_gift_schedule.frequency' + }, + start_date: { + '@path': '$.properties.recurring_gift_schedule.start_date' + } + } + }, + subtype: { + label: 'Subtype', + description: 'The subtype of the gift.', + type: 'string' + }, + type: { + label: 'Type', + description: + 'The gift type. Available values are "Donation", "Other", "GiftInKind", "RecurringGift", and "RecurringGiftPayment".', + type: 'string', + default: 'Donation', + choices: [ + { label: 'Donation', value: 'Donation' }, + { label: 'Other', value: 'Other' }, + { label: 'GiftInKind', value: 'GiftInKind' }, + { label: 'RecurringGift', value: 'RecurringGift' }, + { label: 'RecurringGiftPayment', value: 'RecurringGiftPayment' } + ] + } +} + +Object.keys(createOrUpdateIndividualConstituentFields).forEach((key: string) => { + let fieldKey = 'constituent_' + key + let fieldLabel = 'Constituent ' + createOrUpdateIndividualConstituentFields[key].label + if (key === 'constituent_id') { + fieldKey = key + fieldLabel = createOrUpdateIndividualConstituentFields[key].label + } + fields[fieldKey] = { + ...createOrUpdateIndividualConstituentFields[key], + label: fieldLabel + } +}) + +const perform: RequestFn = async (request, { settings, payload }) => { + const constituentPayload = buildConstituentPayloadFromPayload(payload as StringIndexedObject) + + let constituentId = payload.constituent_id + if (Object.keys(constituentPayload).length > 0) { + const createOrUpdateIndividualConstituentResponse = await performCreateOrUpdateIndividualConstituent(request, { + settings: settings, + payload: constituentPayload + } as ExecuteInput) + constituentId = createOrUpdateIndividualConstituentResponse.id + } else if (constituentId === undefined) { + throw new IntegrationError('Missing constituent_id value', 'MISSING_REQUIRED_FIELD', 400) + } + + const blackbaudSkyApiClient: BlackbaudSkyApi = new BlackbaudSkyApi(request) + + const giftData = buildGiftDataFromPayload(constituentId as string, payload) as Gift + + return blackbaudSkyApiClient.createGift(giftData) +} + +const action: ActionDefinition = { + title: 'Create Gift', + description: "Create a Gift record in Raiser's Edge NXT.", + defaultSubscription: 'type = "track" and event = "Donation Completed"', + fields, + perform +} + +export default action diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/__snapshots__/snapshot.test.ts.snap index 67d00bfa49..8aa3dba1e4 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,21 +1,23 @@ // 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)", - ], +exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createOrUpdateIndividualConstituent destination action: all fields 1`] = ` +Object { + "birthdate": Object { + "d": "1", + "m": "2", + "y": "2021", }, + "first": "H*Z39ROa", + "gender": "H*Z39ROa", + "income": "H*Z39ROa", + "last": "H*Z39ROa", + "lookup_id": "H*Z39ROa", +} +`; + +exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createOrUpdateIndividualConstituent destination action: required fields 1`] = ` +Object { + "last": "Smith", + "lookup_id": "H*Z39ROa", } `; diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/index.test.ts index 036aa2cc5e..7f989a4157 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/index.test.ts @@ -1,7 +1,7 @@ import nock from 'nock' import { createTestEvent, createTestIntegration, IntegrationError, RetryableError } from '@segment/actions-core' import Destination from '../../index' -import { SKY_API_BASE_URL } from '../../constants' +import { SKY_API_CONSTITUENT_URL } from '../../constants' import { identifyEventData, identifyEventDataNoEmail, @@ -43,7 +43,7 @@ const mapping = { } }, lookup_id: { - '@path': '$.traits.lookup_id' + '@path': '$.traits.lookupId' }, online_presence: { address: { @@ -67,14 +67,14 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { test('should create a new constituent successfully', async () => { const event = createTestEvent(identifyEventData) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/search?search_field=email_address&search_text=john@example.biz') .reply(200, { count: 0, value: [] }) - nock(SKY_API_BASE_URL).post('/constituents').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/constituents').reply(200, { id: '123' }) @@ -90,7 +90,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { test('should create a new constituent without email or lookup_id successfully', async () => { const event = createTestEvent(identifyEventDataNoEmail) - nock(SKY_API_BASE_URL).post('/constituents').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/constituents').reply(200, { id: '456' }) @@ -106,7 +106,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { test('should update an existing constituent matched by email successfully', async () => { const event = createTestEvent(identifyEventDataUpdated) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/search?search_field=email_address&search_text=john@example.biz') .reply(200, { count: 1, @@ -121,9 +121,9 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).patch('/constituents/123').reply(200) + nock(SKY_API_CONSTITUENT_URL).patch('/constituents/123').reply(200) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/addresses?include_inactive=true') .reply(200, { count: 1, @@ -146,11 +146,11 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/addresses').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/addresses').reply(200, { id: '1001' }) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/emailaddresses?include_inactive=true') .reply(200, { count: 1, @@ -169,9 +169,9 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).patch('/emailaddresses/2000').reply(200) + nock(SKY_API_CONSTITUENT_URL).patch('/emailaddresses/2000').reply(200) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/onlinepresences?include_inactive=true') .reply(200, { count: 1, @@ -187,11 +187,11 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/onlinepresences').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/onlinepresences').reply(200, { id: '3001' }) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/phones?include_inactive=true') .reply(200, { count: 1, @@ -208,7 +208,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/phones').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/phones').reply(200, { id: '4001' }) @@ -224,7 +224,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { test('should update an existing constituent matched by lookup_id successfully', async () => { const event = createTestEvent(identifyEventDataWithLookupId) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/search?search_field=lookup_id&search_text=abcd1234') .reply(200, { count: 1, @@ -239,9 +239,9 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).patch('/constituents/123').reply(200) + nock(SKY_API_CONSTITUENT_URL).patch('/constituents/123').reply(200) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/addresses?include_inactive=true') .reply(200, { count: 2, @@ -279,11 +279,11 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/addresses').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/addresses').reply(200, { id: '1002' }) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/emailaddresses?include_inactive=true') .reply(200, { count: 1, @@ -302,11 +302,11 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/emailaddresses').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/emailaddresses').reply(200, { id: '2001' }) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/onlinepresences?include_inactive=true') .reply(200, { count: 1, @@ -322,7 +322,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/phones?include_inactive=true') .reply(200, { count: 1, @@ -351,7 +351,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { test('should throw an IntegrationError if multiple records matched', async () => { const event = createTestEvent(identifyEventData) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/search?search_field=email_address&search_text=john@example.biz') .reply(200, { count: 2, @@ -387,7 +387,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { test('should throw an IntegrationError if new constituent has no last name', async () => { const event = createTestEvent(identifyEventDataNoLastName) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/search?search_field=email_address&search_text=john@example.org') .reply(200, { count: 0, @@ -406,7 +406,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { test('should throw an IntegrationError if one or more request returns a 400 when updating an existing constituent', async () => { const event = createTestEvent(identifyEventDataWithInvalidWebsite) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/search?search_field=email_address&search_text=john@example.biz') .reply(200, { count: 1, @@ -421,9 +421,9 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).patch('/constituents/123').reply(200) + nock(SKY_API_CONSTITUENT_URL).patch('/constituents/123').reply(200) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/addresses?include_inactive=true') .reply(200, { count: 1, @@ -446,11 +446,11 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/addresses').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/addresses').reply(200, { id: '1001' }) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/emailaddresses?include_inactive=true') .reply(200, { count: 1, @@ -469,9 +469,9 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).patch('/emailaddresses/2000').reply(200) + nock(SKY_API_CONSTITUENT_URL).patch('/emailaddresses/2000').reply(200) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/onlinepresences?include_inactive=true') .reply(200, { count: 1, @@ -487,9 +487,9 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/onlinepresences').reply(400) + nock(SKY_API_CONSTITUENT_URL).post('/onlinepresences').reply(400) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/phones?include_inactive=true') .reply(200, { count: 1, @@ -506,7 +506,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/phones').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/phones').reply(200, { id: '4001' }) @@ -528,7 +528,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { test('should throw a RetryableError if a request returns a 429 when updating an existing constituent', async () => { const event = createTestEvent(identifyEventDataUpdated) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/search?search_field=email_address&search_text=john@example.biz') .reply(200, { count: 1, @@ -543,9 +543,9 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).patch('/constituents/123').reply(200) + nock(SKY_API_CONSTITUENT_URL).patch('/constituents/123').reply(200) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/onlinepresences?include_inactive=true') .reply(200, { count: 1, @@ -561,7 +561,7 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL) + nock(SKY_API_CONSTITUENT_URL) .get('/constituents/123/addresses?include_inactive=true') .reply(200, { count: 1, @@ -584,11 +584,11 @@ describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { ] }) - nock(SKY_API_BASE_URL).post('/addresses').reply(200, { + nock(SKY_API_CONSTITUENT_URL).post('/addresses').reply(200, { id: '1001' }) - nock(SKY_API_BASE_URL).get('/constituents/123/emailaddresses?include_inactive=true').reply(429) + nock(SKY_API_CONSTITUENT_URL).get('/constituents/123/emailaddresses?include_inactive=true').reply(429) await expect( testDestination.testAction('createOrUpdateIndividualConstituent', { diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/fixtures.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/fixtures.ts index 96d7a7820e..39fadad17b 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/fixtures.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/fixtures.ts @@ -69,7 +69,7 @@ export const identifyEventDataWithLookupId: Partial = { birthday: '2001-01-01T01:01:01-05:00', email: 'john.doe@aol.com', emailType: 'Personal', - lookup_id: 'abcd1234' + lookupId: 'abcd1234' } } diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/generated-types.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/generated-types.ts index d43337cd2f..10c45981cf 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/generated-types.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/generated-types.ts @@ -18,6 +18,10 @@ export interface Payload { * The constituent's birthdate. */ birthdate?: string | number + /** + * The ID of the constituent. + */ + constituent_id?: string /** * The constituent's email address. */ diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/index.ts index c523538b77..f9fdec2f25 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/index.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/index.ts @@ -1,751 +1,367 @@ -import { ActionDefinition, IntegrationError, RetryableError } from '@segment/actions-core' +import { ActionDefinition, InputField, RequestFn } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { BlackbaudSkyApi } from '../api' -import { - Address, - Constituent, - Email, - ExistingAddress, - ExistingEmail, - ExistingOnlinePresence, - ExistingPhone, - OnlinePresence, - Phone -} from '../types' -import { dateStringToFuzzyDate, filterObjectListByMatchFields, isRequestErrorRetryable } from '../utils' - -const action: ActionDefinition = { - title: 'Create or Update Individual Constituent', - description: "Create or update an Individual Constituent record in Raiser's Edge NXT.", - defaultSubscription: 'type = "identify"', - fields: { - address: { - label: 'Address', - description: "The constituent's address.", - type: 'object', - properties: { - address_lines: { - label: 'Address Lines', - type: 'string' - }, - city: { - label: 'City', - type: 'string' - }, - country: { - label: 'Country', - type: 'string' - }, - do_not_mail: { - label: 'Do Not Mail', - type: 'boolean' - }, - postal_code: { - label: 'ZIP/Postal Code', - type: 'string' - }, - primary: { - label: 'Is Primary', - type: 'boolean' - }, - state: { - label: 'State/Province', - type: 'string' - }, - type: { - label: 'Address Type', - type: 'string' - } +import { Address, Constituent, Email, OnlinePresence, Phone } from '../types' +import { splitConstituentPayload } from '../utils' + +export const fields: Record = { + address: { + label: 'Address', + description: "The constituent's address.", + type: 'object', + properties: { + address_lines: { + label: 'Address Lines', + type: 'string' }, - default: { - address_lines: { - '@if': { - exists: { - '@path': '$.traits.address.street' - }, - then: { - '@path': '$.traits.address.street' - }, - else: { - '@path': '$.properties.address.street' - } - } - }, - city: { - '@if': { - exists: { - '@path': '$.traits.address.city' - }, - then: { - '@path': '$.traits.address.city' - }, - else: { - '@path': '$.properties.address.city' - } - } - }, - country: { - '@if': { - exists: { - '@path': '$.traits.address.country' - }, - then: { - '@path': '$.traits.address.country' - }, - else: { - '@path': '$.properties.address.country' - } - } - }, - do_not_mail: '', - postal_code: { - '@if': { - exists: { - '@path': '$.traits.address.postalCode' - }, - then: { - '@path': '$.traits.address.postalCode' - }, - else: { - '@path': '$.properties.address.postalCode' - } - } - }, - primary: '', - state: { - '@if': { - exists: { - '@path': '$.traits.address.state' - }, - then: { - '@path': '$.traits.address.state' - }, - else: { - '@path': '$.properties.address.state' - } - } - }, - type: '' + city: { + label: 'City', + type: 'string' + }, + country: { + label: 'Country', + type: 'string' + }, + do_not_mail: { + label: 'Do Not Mail', + type: 'boolean' + }, + postal_code: { + label: 'ZIP/Postal Code', + type: 'string' + }, + primary: { + label: 'Is Primary', + type: 'boolean' + }, + state: { + label: 'State/Province', + type: 'string' + }, + type: { + label: 'Address Type', + type: 'string' } }, - birthdate: { - label: 'Birthdate', - description: "The constituent's birthdate.", - type: 'datetime', - default: { + default: { + address_lines: { '@if': { exists: { - '@path': '$.traits.birthday' + '@path': '$.traits.address.street' }, then: { - '@path': '$.traits.birthday' + '@path': '$.traits.address.street' }, else: { - '@path': '$.properties.birthday' + '@path': '$.properties.address.street' } } - } - }, - email: { - label: 'Email', - description: "The constituent's email address.", - type: 'object', - properties: { - address: { - label: 'Email Address', - type: 'string' - }, - do_not_email: { - label: 'Do Not Email', - type: 'boolean' - }, - primary: { - label: 'Is Primary', - type: 'boolean' - }, - type: { - label: 'Email Type', - type: 'string' - } }, - default: { - address: { - '@if': { - exists: { - '@path': '$.traits.email' - }, - then: { - '@path': '$.traits.email' - }, - else: { - '@path': '$.properties.email' - } + city: { + '@if': { + exists: { + '@path': '$.traits.address.city' + }, + then: { + '@path': '$.traits.address.city' + }, + else: { + '@path': '$.properties.address.city' } - }, - do_not_email: '', - primary: '', - type: '' - } - }, - first: { - label: 'First Name', - description: "The constituent's first name up to 50 characters.", - type: 'string', - default: { + } + }, + country: { '@if': { exists: { - '@path': '$.traits.firstName' + '@path': '$.traits.address.country' }, then: { - '@path': '$.traits.firstName' + '@path': '$.traits.address.country' }, else: { - '@path': '$.properties.firstName' + '@path': '$.properties.address.country' } } - } - }, - gender: { - label: 'Gender', - description: "The constituent's gender.", - type: 'string', - default: { + }, + do_not_mail: '', + postal_code: { '@if': { exists: { - '@path': '$.traits.gender' + '@path': '$.traits.address.postalCode' }, then: { - '@path': '$.traits.gender' + '@path': '$.traits.address.postalCode' }, else: { - '@path': '$.properties.gender' + '@path': '$.properties.address.postalCode' } } - } - }, - income: { - label: 'Income', - description: "The constituent's income.", - type: 'string' - }, - last: { - label: 'Last Name', - description: "The constituent's last name up to 100 characters. This is required to create a constituent.", - type: 'string', - default: { + }, + primary: '', + state: { '@if': { exists: { - '@path': '$.traits.lastName' + '@path': '$.traits.address.state' }, then: { - '@path': '$.traits.lastName' + '@path': '$.traits.address.state' }, else: { - '@path': '$.properties.lastName' + '@path': '$.properties.address.state' } } - } - }, - lookup_id: { - label: 'Lookup ID', - description: 'The organization-defined identifier for the constituent.', - type: 'string', - default: '' - }, - online_presence: { - label: 'Online Presence', - description: "The constituent's online presence.", - type: 'object', - properties: { - address: { - label: 'Web Address', - type: 'string' + }, + type: '' + } + }, + birthdate: { + label: 'Birthdate', + description: "The constituent's birthdate.", + type: 'datetime', + default: { + '@if': { + exists: { + '@path': '$.traits.birthday' }, - primary: { - label: 'Is Primary', - type: 'boolean' + then: { + '@path': '$.traits.birthday' }, - type: { - label: 'Online Presence Type', - type: 'string' + else: { + '@path': '$.properties.birthday' } + } + } + }, + constituent_id: { + label: 'Constituent ID', + description: 'The ID of the constituent.', + type: 'string' + }, + email: { + label: 'Email', + description: "The constituent's email address.", + type: 'object', + properties: { + address: { + label: 'Email Address', + type: 'string' }, - default: { - address: { - '@if': { - exists: { - '@path': '$.traits.website' - }, - then: { - '@path': '$.traits.website' - }, - else: { - '@path': '$.properties.website' - } - } - }, - primary: '', - type: '' + do_not_email: { + label: 'Do Not Email', + type: 'boolean' + }, + primary: { + label: 'Is Primary', + type: 'boolean' + }, + type: { + label: 'Email Type', + type: 'string' } }, - phone: { - label: 'Phone', - description: "The constituent's phone number.", - type: 'object', - properties: { - do_not_call: { - label: 'Do Not Call', - type: 'boolean' - }, - number: { - label: 'Phone Number', - type: 'string' - }, - primary: { - label: 'Is Primary', - type: 'boolean' - }, - type: { - label: 'Phone Type', - type: 'string' + default: { + address: { + '@if': { + exists: { + '@path': '$.traits.email' + }, + then: { + '@path': '$.traits.email' + }, + else: { + '@path': '$.properties.email' + } } }, - default: { - do_not_call: '', - number: { - '@if': { - exists: { - '@path': '$.traits.phone' - }, - then: { - '@path': '$.traits.phone' - }, - else: { - '@path': '$.properties.phone' - } - } - }, - primary: '', - type: '' - } + do_not_email: '', + primary: '', + type: '' } }, - perform: async (request, { payload }) => { - const blackbaudSkyApiClient: BlackbaudSkyApi = new BlackbaudSkyApi(request) - - // search for existing constituent - let constituentId = undefined - if (payload.email?.address || payload.lookup_id) { - // default to searching by email - let searchField = 'email_address' - let searchText = payload.email?.address || '' - - if (payload.lookup_id) { - // search by lookup_id if one is provided - searchField = 'lookup_id' - searchText = payload.lookup_id - } - - const constituentSearchResponse = await blackbaudSkyApiClient.getExistingConstituents(searchField, searchText) - const constituentSearchResults = await constituentSearchResponse.json() - - if (constituentSearchResults.count > 1) { - // multiple existing constituents, throw an error - throw new IntegrationError('Multiple records returned for given traits', 'MULTIPLE_EXISTING_RECORDS', 400) - } else if (constituentSearchResults.count === 1) { - // existing constituent - constituentId = constituentSearchResults.value[0].id - } - } - - // data for constituent call - const constituentData: Constituent = { - first: payload.first, - gender: payload.gender, - income: payload.income, - last: payload.last, - lookup_id: payload.lookup_id - } - Object.keys(constituentData).forEach((key) => { - if (!constituentData[key as keyof Constituent]) { - delete constituentData[key as keyof Constituent] - } - }) - if (payload.birthdate) { - const birthdateFuzzyDate = dateStringToFuzzyDate(payload.birthdate) - if (birthdateFuzzyDate) { - constituentData.birthdate = birthdateFuzzyDate - } - } - - // data for address call - let constituentAddressData: Address = {} - if ( - payload.address && - (payload.address.address_lines || - payload.address.city || - payload.address.country || - payload.address.postal_code || - payload.address.state) && - payload.address.type - ) { - constituentAddressData = payload.address - } - - // data for email call - let constituentEmailData: Email = {} - if (payload.email && payload.email.address && payload.email.type) { - constituentEmailData = payload.email - } - - // data for online presence call - let constituentOnlinePresenceData: OnlinePresence = {} - if (payload.online_presence && payload.online_presence.address && payload.online_presence.type) { - constituentOnlinePresenceData = payload.online_presence - } - - // data for phone call - let constituentPhoneData: Phone = {} - if (payload.phone && payload.phone.number && payload.phone.type) { - constituentPhoneData = payload.phone - } - - if (!constituentId) { - // new constituent - // hardcode type - constituentData.type = 'Individual' - if (!constituentData.last) { - // last name is required to create a new constituent - // no last name, throw an error - throw new IntegrationError('Missing last name value', 'MISSING_REQUIRED_FIELD', 400) - } else { - // request has last name - // append other data objects to constituent - if (Object.keys(constituentAddressData).length > 0) { - constituentData.address = constituentAddressData - } - if (Object.keys(constituentEmailData).length > 0) { - constituentData.email = constituentEmailData - } - if (Object.keys(constituentOnlinePresenceData).length > 0) { - constituentData.online_presence = constituentOnlinePresenceData - } - if (Object.keys(constituentPhoneData).length > 0) { - constituentData.phone = constituentPhoneData + first: { + label: 'First Name', + description: "The constituent's first name up to 50 characters.", + type: 'string', + default: { + '@if': { + exists: { + '@path': '$.traits.firstName' + }, + then: { + '@path': '$.traits.firstName' + }, + else: { + '@path': '$.properties.firstName' } - - // create constituent - await blackbaudSkyApiClient.createConstituent(constituentData) } - - return - } else { - // existing constituent - // aggregate all errors - const integrationErrors = [] - if (Object.keys(constituentData).length > 0) { - // request has at least one constituent field to update - // update constituent - const updateConstituentResponse = await blackbaudSkyApiClient.updateConstituent(constituentId, constituentData) - if (updateConstituentResponse.status !== 200) { - const statusCode = updateConstituentResponse.status - const errorMessage = statusCode - ? `${statusCode} error occurred when updating constituent` - : 'Error occurred when updating constituent' - if (isRequestErrorRetryable(statusCode)) { - throw new RetryableError(errorMessage) - } else { - integrationErrors.push(errorMessage) - } + } + }, + gender: { + label: 'Gender', + description: "The constituent's gender.", + type: 'string', + default: { + '@if': { + exists: { + '@path': '$.traits.gender' + }, + then: { + '@path': '$.traits.gender' + }, + else: { + '@path': '$.properties.gender' } } - - if (Object.keys(constituentAddressData).length > 0) { - // request has address data - // get existing addresses - const getConstituentAddressListResponse = await blackbaudSkyApiClient.getConstituentAddressList(constituentId) - let updateAddressErrorCode = undefined - if (getConstituentAddressListResponse.status !== 200) { - updateAddressErrorCode = getConstituentAddressListResponse.status - } else { - const constituentAddressListResults = await getConstituentAddressListResponse.json() - - // check address list for one that matches request - let existingAddress: ExistingAddress | undefined = undefined - if (constituentAddressListResults.count > 0) { - existingAddress = filterObjectListByMatchFields( - constituentAddressListResults.value, - constituentAddressData, - ['address_lines', 'city', 'postal_code', 'state'] - ) as ExistingAddress | undefined - } - - if (!existingAddress) { - // new address - // if this is the only address, make it primary - if (constituentAddressData.primary !== false && constituentAddressListResults.count === 0) { - constituentAddressData.primary = true - } - // create address - const createConstituentAddressResponse = await blackbaudSkyApiClient.createConstituentAddress( - constituentId, - constituentAddressData - ) - if (createConstituentAddressResponse.status !== 200) { - updateAddressErrorCode = createConstituentAddressResponse.status - } - } else { - // existing address - if ( - existingAddress.inactive || - (constituentAddressData.do_not_mail !== undefined && - constituentAddressData.do_not_mail !== existingAddress.do_not_mail) || - (constituentAddressData.primary !== undefined && - constituentAddressData.primary && - constituentAddressData.primary !== existingAddress.primary) || - constituentAddressData.type !== existingAddress.type - ) { - // request has at least one address field to update - // update address - const updateConstituentAddressByIdResponse = await blackbaudSkyApiClient.updateConstituentAddressById( - existingAddress.id, - constituentAddressData - ) - if (updateConstituentAddressByIdResponse.status !== 200) { - updateAddressErrorCode = updateConstituentAddressByIdResponse.status - } - } - } - } - - if (updateAddressErrorCode) { - const errorMessage = updateAddressErrorCode - ? `${updateAddressErrorCode} error occurred when updating constituent address` - : 'Error occurred when updating constituent address' - if (isRequestErrorRetryable(updateAddressErrorCode)) { - throw new RetryableError(errorMessage) - } else { - integrationErrors.push(errorMessage) - } + } + }, + income: { + label: 'Income', + description: "The constituent's income.", + type: 'string' + }, + last: { + label: 'Last Name', + description: "The constituent's last name up to 100 characters. This is required to create a constituent.", + type: 'string', + default: { + '@if': { + exists: { + '@path': '$.traits.lastName' + }, + then: { + '@path': '$.traits.lastName' + }, + else: { + '@path': '$.properties.lastName' } } - - if (Object.keys(constituentEmailData).length > 0) { - // request has email data - // get existing addresses - const getConstituentEmailListResponse = await blackbaudSkyApiClient.getConstituentEmailList(constituentId) - let updateEmailErrorCode = undefined - if (getConstituentEmailListResponse.status !== 200) { - updateEmailErrorCode = getConstituentEmailListResponse.status - } else { - const constituentEmailListResults = await getConstituentEmailListResponse.json() - - // check email list for one that matches request - let existingEmail: ExistingEmail | undefined = undefined - if (constituentEmailListResults.count > 0) { - existingEmail = filterObjectListByMatchFields(constituentEmailListResults.value, constituentEmailData, [ - 'address' - ]) as ExistingEmail | undefined - } - - if (!existingEmail) { - // new email - // if this is the only email, make it primary - if (constituentEmailData.primary !== false && constituentEmailListResults.count === 0) { - constituentEmailData.primary = true - } - // create email - const createConstituentEmailResponse = await blackbaudSkyApiClient.createConstituentEmail( - constituentId, - constituentEmailData - ) - if (createConstituentEmailResponse.status !== 200) { - updateEmailErrorCode = createConstituentEmailResponse.status - } - } else { - // existing email - if ( - existingEmail.inactive || - (constituentEmailData.do_not_email !== undefined && - constituentEmailData.do_not_email !== existingEmail.do_not_email) || - (constituentEmailData.primary !== undefined && - constituentEmailData.primary && - constituentEmailData.primary !== existingEmail.primary) || - constituentEmailData.type !== existingEmail.type - ) { - // request has at least one email field to update - // update email - const updateConstituentEmailByIdResponse = await blackbaudSkyApiClient.updateConstituentEmailById( - existingEmail.id, - constituentEmailData - ) - if (updateConstituentEmailByIdResponse.status !== 200) { - updateEmailErrorCode = updateConstituentEmailByIdResponse.status - } - } - } - } - - if (updateEmailErrorCode) { - const errorMessage = updateEmailErrorCode - ? `${updateEmailErrorCode} error occurred when updating constituent email` - : 'Error occurred when updating constituent email' - if (isRequestErrorRetryable(updateEmailErrorCode)) { - throw new RetryableError(errorMessage) - } else { - integrationErrors.push(errorMessage) - } - } + } + }, + lookup_id: { + label: 'Lookup ID', + description: 'The organization-defined identifier for the constituent.', + type: 'string' + }, + online_presence: { + label: 'Online Presence', + description: "The constituent's online presence.", + type: 'object', + properties: { + address: { + label: 'Web Address', + type: 'string' + }, + primary: { + label: 'Is Primary', + type: 'boolean' + }, + type: { + label: 'Online Presence Type', + type: 'string' } - - if (Object.keys(constituentOnlinePresenceData).length > 0) { - // request has online presence data - // get existing online presences - const getConstituentOnlinePresenceListResponse = await blackbaudSkyApiClient.getConstituentOnlinePresenceList( - constituentId - ) - let updateOnlinePresenceErrorCode = undefined - if (getConstituentOnlinePresenceListResponse.status !== 200) { - updateOnlinePresenceErrorCode = getConstituentOnlinePresenceListResponse.status - } else { - const constituentOnlinePresenceListResults = await getConstituentOnlinePresenceListResponse.json() - - // check online presence list for one that matches request - let existingOnlinePresence: ExistingOnlinePresence | undefined = undefined - if (constituentOnlinePresenceListResults.count > 0) { - existingOnlinePresence = filterObjectListByMatchFields( - constituentOnlinePresenceListResults.value, - constituentOnlinePresenceData, - ['address'] - ) as ExistingOnlinePresence | undefined - } - - if (!existingOnlinePresence) { - // new online presence - // if this is the only online presence, make it primary - if (constituentOnlinePresenceData.primary !== false && constituentOnlinePresenceListResults.count === 0) { - constituentOnlinePresenceData.primary = true - } - // create online presence - const createConstituentOnlinePresenceResponse = await blackbaudSkyApiClient.createConstituentOnlinePresence( - constituentId, - constituentOnlinePresenceData - ) - if (createConstituentOnlinePresenceResponse.status !== 200) { - updateOnlinePresenceErrorCode = createConstituentOnlinePresenceResponse.status - } - } else { - // existing online presence - if ( - existingOnlinePresence.inactive || - (constituentOnlinePresenceData.primary !== undefined && - constituentOnlinePresenceData.primary !== existingOnlinePresence.primary) || - constituentOnlinePresenceData.type !== existingOnlinePresence.type - ) { - // request has at least one online presence field to update - // update online presence - const updateConstituentOnlinePresenceByIdResponse = - await blackbaudSkyApiClient.updateConstituentOnlinePresenceById( - existingOnlinePresence.id, - constituentOnlinePresenceData - ) - if (updateConstituentOnlinePresenceByIdResponse.status !== 200) { - updateOnlinePresenceErrorCode = updateConstituentOnlinePresenceByIdResponse.status - } - } - } - } - - if (updateOnlinePresenceErrorCode) { - const errorMessage = updateOnlinePresenceErrorCode - ? `${updateOnlinePresenceErrorCode} error occurred when updating constituent online presence` - : 'Error occurred when updating constituent online presence' - if (isRequestErrorRetryable(updateOnlinePresenceErrorCode)) { - throw new RetryableError(errorMessage) - } else { - integrationErrors.push(errorMessage) + }, + default: { + address: { + '@if': { + exists: { + '@path': '$.traits.website' + }, + then: { + '@path': '$.traits.website' + }, + else: { + '@path': '$.properties.website' } } + }, + primary: '', + type: '' + } + }, + phone: { + label: 'Phone', + description: "The constituent's phone number.", + type: 'object', + properties: { + do_not_call: { + label: 'Do Not Call', + type: 'boolean' + }, + number: { + label: 'Phone Number', + type: 'string' + }, + primary: { + label: 'Is Primary', + type: 'boolean' + }, + type: { + label: 'Phone Type', + type: 'string' } - - if (Object.keys(constituentPhoneData).length > 0) { - // request has phone data - // get existing phones - const getConstituentPhoneListResponse = await blackbaudSkyApiClient.getConstituentPhoneList(constituentId) - let updatePhoneErrorCode = undefined - if (getConstituentPhoneListResponse.status !== 200) { - updatePhoneErrorCode = getConstituentPhoneListResponse.status - } else { - const constituentPhoneListResults = await getConstituentPhoneListResponse.json() - - // check phone list for one that matches request - let existingPhone: ExistingPhone | undefined = undefined - if (constituentPhoneListResults.count > 0) { - existingPhone = filterObjectListByMatchFields(constituentPhoneListResults.value, constituentPhoneData, [ - 'int:number' - ]) as ExistingPhone | undefined - } - - if (!existingPhone) { - // new phone - // if this is the only phone, make it primary - if (constituentPhoneData.primary !== false && constituentPhoneListResults.count === 0) { - constituentPhoneData.primary = true - } - // create phone - const createConstituentPhoneResponse = await blackbaudSkyApiClient.createConstituentPhone( - constituentId, - constituentPhoneData - ) - if (createConstituentPhoneResponse.status !== 200) { - updatePhoneErrorCode = createConstituentPhoneResponse.status - } - } else { - // existing phone - if ( - existingPhone.inactive || - (constituentPhoneData.do_not_call !== undefined && - constituentPhoneData.do_not_call !== existingPhone.do_not_call) || - (constituentPhoneData.primary !== undefined && constituentPhoneData.primary !== existingPhone.primary) || - constituentPhoneData.type !== existingPhone.type - ) { - // request has at least one phone field to update - // update phone - const updateConstituentPhoneByIdResponse = await blackbaudSkyApiClient.updateConstituentPhoneById( - existingPhone.id, - constituentPhoneData - ) - if (updateConstituentPhoneByIdResponse.status !== 200) { - updatePhoneErrorCode = updateConstituentPhoneByIdResponse.status - } - } + }, + default: { + do_not_call: '', + number: { + '@if': { + exists: { + '@path': '$.traits.phone' + }, + then: { + '@path': '$.traits.phone' + }, + else: { + '@path': '$.properties.phone' } } + }, + primary: '', + type: '' + } + } +} - if (updatePhoneErrorCode) { - const errorMessage = updatePhoneErrorCode - ? `${updatePhoneErrorCode} error occurred when updating constituent online presence` - : 'Error occurred when updating constituent online presence' - if (isRequestErrorRetryable(updatePhoneErrorCode)) { - throw new RetryableError(errorMessage) - } else { - integrationErrors.push(errorMessage) - } - } - } +export const perform: RequestFn = async (request, { payload }) => { + const blackbaudSkyApiClient: BlackbaudSkyApi = new BlackbaudSkyApi(request) - if (integrationErrors.length > 0) { - throw new IntegrationError( - 'One or more errors occurred when updating existing constituent: ' + integrationErrors.join(', '), - 'UPDATE_CONSTITUENT_ERROR', - 500 - ) - } + let constituentId = payload.constituent_id + if (!constituentId && (payload.email?.address || payload.lookup_id)) { + const getExistingConstituentResponse = await blackbaudSkyApiClient.getExistingConstituent( + payload.email, + payload.lookup_id + ) + constituentId = getExistingConstituentResponse.id + } - return - } + const [constituentData, addressData, emailData, onlinePresenceData, phoneData] = splitConstituentPayload(payload) + + if (!constituentId) { + return blackbaudSkyApiClient.createConstituentWithRelatedObjects( + constituentData as Constituent, + addressData as Partial
, + emailData as Partial, + onlinePresenceData as Partial, + phoneData as Partial + ) + } else { + return blackbaudSkyApiClient.updateConstituentWithRelatedObjects( + constituentId, + constituentData as Partial, + addressData as Partial
, + emailData as Partial, + onlinePresenceData as Partial, + phoneData as Partial + ) } } +const action: ActionDefinition = { + title: 'Create or Update Individual Constituent', + description: "Create or update an Individual Constituent record in Raiser's Edge NXT.", + defaultSubscription: 'type = "identify"', + fields, + perform +} + export default action diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/index.ts index 280cc3651b..ef24abe14e 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/index.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/index.ts @@ -2,6 +2,7 @@ import type { DestinationDefinition } from '@segment/actions-core' import type { Settings } from './generated-types' import { SKY_API_BASE_URL, SKY_OAUTH2_TOKEN_URL } from './constants' import { RefreshTokenResponse } from './types' +import createGift from './createGift' import createOrUpdateIndividualConstituent from './createOrUpdateIndividualConstituent' const destination: DestinationDefinition = { @@ -54,6 +55,7 @@ const destination: DestinationDefinition = { }, actions: { + createGift, createOrUpdateIndividualConstituent } } diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/types/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/types/index.ts index 5d25799dd2..f1be3ceab5 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/types/index.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/types/index.ts @@ -6,6 +6,18 @@ export interface StringIndexedObject { [key: string]: any } +export interface ExistingConstituentResult { + id: string | undefined +} + +export interface CreateConstituentResult { + id: string +} + +export interface UpdateConstituentResult { + id: string +} + export interface FuzzyDate { d: string m: string @@ -19,7 +31,7 @@ export interface Constituent { first?: string gender?: string income?: string - last?: string + last: string lookup_id?: string online_presence?: OnlinePresence phone?: Phone @@ -34,7 +46,7 @@ export interface Address { postal_code?: string primary?: boolean state?: string - type?: string + type: string inactive?: boolean } @@ -43,10 +55,10 @@ export interface ExistingAddress extends Address { } export interface Email { - address?: string + address: string do_not_email?: boolean primary?: boolean - type?: string + type: string inactive?: boolean } @@ -55,9 +67,9 @@ export interface ExistingEmail extends Email { } export interface OnlinePresence { - address?: string + address: string primary?: boolean - type?: string + type: string inactive?: boolean } @@ -67,12 +79,63 @@ export interface ExistingOnlinePresence extends OnlinePresence { export interface Phone { do_not_call?: boolean - number?: string + number: string primary?: boolean - type?: string + type: string inactive?: boolean } export interface ExistingPhone extends Phone { id: string } + +export interface Gift { + acknowledgements?: GiftAcknowledgement[] + amount: GiftAmount + constituent_id: string + date: string | number + gift_splits: GiftSplit[] + gift_status?: string + is_anonymous?: boolean + is_manual: boolean + linked_gifts?: string[] + lookup_id?: string + payments: GiftPayment[] + post_date?: string | number + post_status?: string + receipts?: GiftReceipt[] + recurring_gift_schedule?: RecurringGiftSchedule + subtype?: string + type: string +} + +export interface GiftAcknowledgement { + date?: string | number + status?: string +} + +export interface GiftAmount { + value: number +} + +export interface GiftSplit { + amount: GiftAmount + fund_id: string +} + +export interface GiftPayment { + check_date?: FuzzyDate + check_number?: string + payment_method: string +} + +export interface GiftReceipt { + date?: string | number + status?: string +} + +export interface RecurringGiftSchedule { + end_date?: string | number + frequency: string + start_date: string | number +} diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/utils/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/utils/index.ts index 6c49070771..ee49d45f9c 100644 --- a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/utils/index.ts +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/utils/index.ts @@ -1,7 +1,20 @@ -import { StringIndexedObject } from '../types' +import { + Address, + Constituent, + Email, + Gift, + GiftAcknowledgement, + GiftReceipt, + OnlinePresence, + Phone, + StringIndexedObject +} from '../types' +import { Payload as CreateOrUpdateIndividualConstituentPayload } from '../createOrUpdateIndividualConstituent/generated-types' +import { Payload as CreateGiftPayload } from '../createGift/generated-types' export const dateStringToFuzzyDate = (dateString: string | number) => { - const date = new Date(dateString) + // Ignore timezone + const date = new Date((dateString + '').split('T')[0]) if (isNaN(date.getTime())) { // invalid date object return false @@ -10,13 +23,189 @@ export const dateStringToFuzzyDate = (dateString: string | number) => { // convert date to a "Fuzzy date" // https://developer.blackbaud.com/skyapi/renxt/constituent/entities#FuzzyDate return { - d: date.getDate().toString(), - m: (date.getMonth() + 1).toString(), - y: date.getFullYear().toString() + d: date.getUTCDate().toString(), + m: (date.getUTCMonth() + 1).toString(), + y: date.getUTCFullYear().toString() } } } +export const splitConstituentPayload = (payload: CreateOrUpdateIndividualConstituentPayload) => { + const constituentData: Partial = { + first: payload.first, + gender: payload.gender, + income: payload.income, + last: payload.last, + lookup_id: payload.lookup_id + } + Object.keys(constituentData).forEach((key) => { + if (!constituentData[key as keyof Constituent]) { + delete constituentData[key as keyof Constituent] + } + }) + if (payload.birthdate) { + const birthdateFuzzyDate = dateStringToFuzzyDate(payload.birthdate) + if (birthdateFuzzyDate) { + constituentData.birthdate = birthdateFuzzyDate + } + } + + let addressData: Partial
= {} + if ( + payload.address && + (payload.address.address_lines || + payload.address.city || + payload.address.country || + payload.address.postal_code || + payload.address.state) && + payload.address.type + ) { + addressData = payload.address + } + + let emailData: Partial = {} + if (payload.email && payload.email.address && payload.email.type) { + emailData = payload.email + } + + let onlinePresenceData: Partial = {} + if (payload.online_presence && payload.online_presence.address && payload.online_presence.type) { + onlinePresenceData = payload.online_presence + } + + let phoneData: Partial = {} + if (payload.phone && payload.phone.number && payload.phone.type) { + phoneData = payload.phone + } + + return [constituentData, addressData, emailData, onlinePresenceData, phoneData] +} + +export const buildConstituentPayloadFromPayload = (payload: StringIndexedObject) => { + // check if request includes fields to create or update a constituent + // if so, append them to a new payload + const constituentPayload: StringIndexedObject = {} + Object.keys(payload).forEach((key: string) => { + if (key.startsWith('constituent_')) { + // only append non-empty fields/objects + if ( + payload[key] && + (typeof payload[key] !== 'object' || + (Object.keys(payload[key]).length > 0 && Object.values(payload[key]).every((x) => !!x))) + ) { + let constituentPayloadKey = key.substring('constituent_'.length) + if (key === 'constituent_id') { + constituentPayloadKey = key + } + constituentPayload[constituentPayloadKey] = payload[key] + } + } + }) + return constituentPayload as Partial +} + +export const buildGiftDataFromPayload = (constituentId: string, payload: CreateGiftPayload) => { + // data for gift call + const giftData: Partial = { + amount: { + value: payload.amount + }, + constituent_id: constituentId, + date: payload.date, + gift_status: payload.gift_status, + is_anonymous: payload.is_anonymous, + // hardcode is_manual + is_manual: true, + lookup_id: payload.lookup_id, + post_date: payload.post_date, + post_status: payload.post_status, + subtype: payload.subtype, + type: payload.type + } + Object.keys(giftData).forEach((key) => { + if (!giftData[key as keyof Gift]) { + delete giftData[key as keyof Gift] + } + }) + + // default date + giftData.date = giftData.date || new Date().toISOString() + + // create acknowledgements array + if (payload.acknowledgement) { + const acknowledgementData: GiftAcknowledgement = { + status: payload.acknowledgement.status || 'NEEDSACKNOWLEDGEMENT' + } + if ( + acknowledgementData.status !== 'NEEDSACKNOWLEDGEMENT' && + acknowledgementData.status !== 'DONOTACKNOWLEDGE' && + payload.acknowledgement.date + ) { + acknowledgementData.date = payload.acknowledgement.date + } + giftData.acknowledgements = [acknowledgementData] + } + + // create gift splits array + giftData.gift_splits = [ + { + amount: { + value: payload.amount + }, + fund_id: payload.fund_id + } + ] + + // create payments array + giftData.payments = [ + { + payment_method: payload.payment_method + } + ] + + // fields for check gifts + if (giftData.payments[0].payment_method === 'PersonalCheck') { + giftData.payments[0].check_number = payload.check_number + if (payload.check_date) { + const checkDateFuzzyDate = dateStringToFuzzyDate(payload.check_date) + if (checkDateFuzzyDate) { + giftData.payments[0].check_date = checkDateFuzzyDate + } + } + } + + // default post date + if ((giftData.post_status === 'NotPosted' || giftData.post_status === 'Posted') && !giftData.post_date) { + giftData.post_date = payload.date + } + + // create receipts array + if (payload.receipt) { + const receiptData: GiftReceipt = { + status: payload.receipt.status || 'NEEDSRECEIPT' + } + if (receiptData.status === 'RECEIPTED' && payload.receipt.date) { + receiptData.date = payload.receipt.date + } + giftData.receipts = [receiptData] + } + + // fields for recurring gifts + if (giftData.type === 'RecurringGift') { + if (payload.recurring_gift_schedule) { + giftData.recurring_gift_schedule = { + end_date: payload.recurring_gift_schedule.end_date, + frequency: payload.recurring_gift_schedule.frequency || '', + start_date: payload.recurring_gift_schedule.start_date || '' + } + } + } else if (giftData.type === 'RecurringGiftPayment' && payload.linked_gifts) { + giftData.linked_gifts = payload.linked_gifts + } + + return giftData +} + export const filterObjectListByMatchFields = ( list: StringIndexedObject[], data: StringIndexedObject,