Write Contact Points flow component
Overview#
Class name: ContactPointWriteComponent Friendly name: Write Contact Points Category: MoveData: NPC Purpose: Writes a person's email, phone and address as ContactPointEmail, ContactPointPhone and ContactPointAddress records on their account, reusing any record that already holds the value
Class description#
Nonprofit Cloud keeps a person's contactable values in three related objects rather than on the person record, and those objects are what its reporting, list building and Marketing Cloud sync read. This component maintains them.
It runs once per person the notification resolves, from the contact post-upsert flow, after the person record has been saved and their account id is known. The person record is left alone: their Email, Phone and address fields keep behaving exactly as they always have, one value each, overwritten by the next donation that carries a different one. Contact points accumulate instead, so a supporter who gives a second email address ends up with two records rather than one replaced one.
A value that already has a contact point is reused, and a value that does not gets one. What decides "already has" is the match key: person, contact point type, and the normalised value. That key is the record's identity, which is why the component normalises rather than comparing verbatim. Platforms send the same address in different casing and the same phone number spaced and bracketed differently, and a duplicate contact point is not cosmetic: consent records attach to contact points, so a second copy of a value would split a supporter's permissions across two rows.
The whole component is off until an organisation turns Write Contact Points on. Nothing here throws, either. A contact point is an enrichment of a donation rather than the donation itself, so a rejected write is reported through Error and the donation stands.
Input parameters#
Person and parent#
| Parameter | API name | Type | Description |
|---|---|---|---|
| Parent Account ID | ParentId |
Id | The account the contact points belong to. Never a contact: ParentId references Account and Individual only. No id, nothing written. |
| Anonymous Account ID | AnonymousAccountId |
String | The organisation's shared anonymous person account. When the parent is that account, nothing is written. |
| Activity Date Time | ActivityDateTime |
Datetime | Stamped as ActiveFromDate when a record is created, in the running user's timezone. Today's date when absent. |
| List of Log Entries | LogList |
List | The flow's log collection. Every contact point created, matched or skipped is appended to it and handed back. |
Email#
| Parameter | API name | Type | Description |
|---|---|---|---|
| Email Address | Email |
String | The person's email address. Blank writes nothing. |
| Email Type | EmailUsageType |
String | The platform's own email type: home, work or other. Mapped to UsageType where Salesforce has an equivalent. |
Phone#
| Parameter | API name | Type | Description |
|---|---|---|---|
| Telephone Number | Phone |
String | The person's phone number, written as the platform sent it. Blank writes nothing. |
| Phone Type | PhoneType |
String | Stored in ContactPointPhone.PhoneType. Home or Mobile, the only two values Salesforce allows on that field. |
| Phone Type (payload) | PhoneUsageType |
String | The platform's own phone type: home, work or other. Maps to UsageType, not to PhoneType, because there is no Mobile in home, work or other. |
| Phone Is SMS Capable | IsSmsCapable |
Boolean | Sets IsSmsCapable on new phone contact points. One value for every number the organisation writes. |
| Phone Minimum Digits | MinimumPhoneDigits |
Decimal | Numbers with fewer digits than this are skipped as unusable. Zero or absent checks nothing. |
Address#
| Parameter | API name | Type | Description |
|---|---|---|---|
| Address Name | AddressName |
String | Name is required on ContactPointAddress and has no default. No name, no address contact point. |
| Address Type | AddressType |
String | Billing or Shipping. There is no Mailing. |
| Address Usage Type | AddressUsageType |
String | The platform's own address type, where one is sent. Falls back to the organisation's default. |
| Address Street | Street |
String | |
| Address City | City |
String | |
| Address State | State |
String | |
| Address Postal Code | PostalCode |
String | |
| Address Country | Country |
String | |
| Address Quality Index | AddressQualityIndex |
Decimal | The platform's completeness score for this address, 0.00 to 1.00. Only JustGiving, Benevity and EventMaster send one. |
| Address Quality Threshold | AddressQualityThreshold |
Decimal | The organisation's minimum address quality. Most organisations have set none. |
Shared#
| Parameter | API name | Type | Description |
|---|---|---|---|
| Usage Type Default | UsageTypeDefault |
String | The organisation's default UsageType, used on all three contact point types and only where the platform sent no type of its own. |
Output parameters#
| Parameter | API name | Type | Description |
|---|---|---|---|
| Success | Success |
Boolean (required) | False when a write failed. The donation still succeeds. |
| Error | Error |
String | Null unless a write failed or a configured type value was rejected. The flow appends it to the pipeline's error collection. |
| List of Log Entries | LogList |
List (required) | One entry per contact point created, matched, updated or skipped. |
| Json Output of Log Entries | LogJson |
String | JSON-encoded version of the same log entries. |
| Record IDs | RecordList |
List of String | Every contact point this person's donation created, matched or updated. The pipeline adds them to the notification's Records section. |
| Email Contact Points Created | EmailsCreated |
Integer | Records created, not records matched. |
| Phone Contact Points Created | PhonesCreated |
Integer | |
| Address Contact Points Created | AddressesCreated |
Integer | |
| Phone Skipped Reason | PhoneSkippedReason |
String | Why no phone contact point was written when there was a number to write. |
| Address Skipped Reason | AddressSkippedReason |
String | Why no address contact point was written when there was an address to write. |
Where the values come from#
The Write Contact Points element in the contact post-upsert flow supplies every input, so a value that looks wrong in a notification's log can be traced to either a setting or the payload:
| Input | Supplied from |
|---|---|
ParentId |
Record.Id, the person account the stage has just upserted |
AnonymousAccountId |
Config_AnonymousContactId |
ActivityDateTime |
The donation date, or the person's created date when the donation carries none |
Email, Phone |
The payload's own values, the same ones the person record is mapped from |
EmailUsageType, PhoneUsageType |
The payload's EmailType and PhoneType |
Street, City, State, PostalCode, Country |
The payload's MailingAddress_ fields |
AddressQualityIndex |
MailingAddress_QualityIndex, the platform's score |
AddressName |
Config_ContactPointAddressName, parsed by movedata__NamePatternParserComponent |
AddressType, PhoneType, UsageTypeDefault, IsSmsCapable, MinimumPhoneDigits |
The matching contact point settings |
AddressQualityThreshold |
Config_ContactAddressQualityIndex, shared with the contact mapping flow |
AddressUsageType is the one input the package's own flow does not wire, because no platform sends an address type today. Address contact points therefore take the organisation's default.
Behaviour#
Matching and reuse#
Each type is matched on its own normalised value, within that one person's records:
| Type | How two values are treated as the same |
|---|---|
| Trimmed and case-folded. Nothing else: dots and plus-addressing are real distinctions at some providers, and treating two mailboxes as one address is worse than holding two records. | |
| Phone | The digits, and only the digits. Spaces, brackets and dashes fold away; a country code or a trunk zero does not. |
| Address | Street, postcode and country, lower-cased with punctuation, spaces and newlines removed. City and state are deliberately outside the key. |
So (07700) 900 123 and 07700900123 are one phone record, and 021 123 456 and +64 21 123 456 are two. The second pair is one New Zealand mobile to a reader, and that is a known and accepted cost: deciding that a country code and a trunk zero describe the same subscriber is a question every notifier and every extension has, so it is answered centrally rather than inside this component. Until it is, an organisation whose platforms send both forms holds two phone contact points for the one number.
City and state sitting outside the address key is what lets a correction land. A platform that sends the same street and postcode with a corrected city updates the address record the organisation already holds instead of adding a second one, and those two fields are the only source values a matched record is ever updated with.
What is written once and never again#
IsPrimary, ActiveFromDate, the address Name, and the AddressType, PhoneType, UsageType and IsSmsCapable values are set when a record is created and left alone afterwards. A later donation has nothing new to say about them, and re-applying a default would overwrite a choice the organisation's team or another system made.
A matched phone number is not rewritten into the format this platform happens to send, for the same reason. For an organisation running two platforms that would flip the stored value back and forth on every donation, which is the overwrite behaviour contact points exist to avoid.
IsPrimary is true only on the first record of its type for that person. A second email address therefore arrives non-primary, and the first record is untouched.
The address quality gate#
An address scoring below the organisation's threshold is not written. The condition is the person record's own, condition for condition: the skip needs a threshold set, a score present, and the score below the threshold. A missing threshold passes and a missing score passes, which is almost all traffic.
The gate sits above the match, so a below-threshold address cannot update an address the organisation already holds any more than it can add a new one. That follows from reusing the person record's gate rather than from a separate choice, and it is the behaviour the person record has had all along.
When nothing is written#
Each of these is a log line naming the reason, not an error:
| Nothing written | Reason |
|---|---|
| Anything at all | No ParentId, or the parent is the anonymous account. A fundraiser on the same notification is a different account and still gets theirs. |
| Phone | Fewer digits than the configured minimum. |
| Address | No street, postcode or country, so there is nothing to match the record on. |
| Address | Below the quality threshold. |
| Address | No address name pattern configured, and Name is required on the object. |
One invocation, many people#
Existing contact points are read once per invocation, one query per object, and matched in memory. Records created earlier in the same invocation are matched against too, so two requests for the same person in one notification cannot each create the same contact point.
What ends up on the notification#
Every created, matched, updated and skipped contact point is a log entry on the notification, naming the record and the value it matched on. The ids of the records touched go back as RecordList, matched and updated records as well as created ones, so a consultant can see which record a donation attached to and open it from the notification's Records section.
Error handling#
Configured type values#
All four type settings are checked against the field's active picklist values in that organisation, and a value the field does not carry is dropped, reported in Error, and the contact point written without it.
That check matters most where it is least obvious. AddressType and UsageType are unrestricted picklists, so Salesforce would accept a typo, store it, and then quietly leave the record out of every filtered report. PhoneType is restricted, so a typo there would reject every phone contact point outright. Both failure modes are someone's setting to fix, which is why they are reported rather than silently ignored.
A type the platform sent that the organisation's picklist does not carry falls back to the default without an error, because only a setting is someone's mistake.
Failed writes#
Records are inserted and updated with partial success allowed, so one rejected contact point neither rolls the donation back nor is reported against the other people on the same notification. A rejection sets Success to false, appends the reason to Error and writes a log line. Missing field-level access on the three objects is the common cause; see permission sets.
The flow element also carries a fault connector, so an unexpected failure is caught and recorded rather than failing the stage.
Dependencies#
Required objects#
ContactPointEmail,ContactPointPhone,ContactPointAddress: the Nonprofit Cloud contact point objectsAccount: the parent of every contact point written
Required classes#
movedata.MoveDataLogEntry: the pipeline's log entry typemovedata__NamePatternParserComponent: resolves the address name pattern before this component runs