What's needed
philanthropy.ingest has three donor-CRM gift bridges today: CiviCRM, Blackbaud Raiser's Edge, and Salesforce NPSP. Each is a thin, CRM-specific header-alias map plus a commitment/payment exclusion filter (a pledge and the payment against it must never both count toward total_gift_amount), delegating the actual roll-up to the shared aggregator in philanthropy/ingest/_civicrm.py. This issue adds the same bridge for Salesforce Nonprofit Cloud — Salesforce's newer nonprofit data model (Gift, Payment, and related objects), distinct from the older NPSP package this repo already supports.
Where to look
| File |
Purpose |
philanthropy/ingest/_npsp.py |
Closest existing example: same underlying platform (Salesforce), header aliasing, a documented default exclusion set, delegation to the shared aggregator |
philanthropy/ingest/_raisers_edge.py |
A second example of the same pattern |
philanthropy/ingest/_civicrm.py |
The shared aggregator (civicrm_contributions_to_features, read_civicrm_contributions) and the private helpers (_REQUIRED, _canonical, _normalise_headers, _to_frame, _empty_feature_frame) both examples import and reuse |
tests/test_npsp.py |
The test depth expected of your new tests/test_nonprofit_cloud.py |
philanthropy/cli.py |
_FEATURE_SOURCES and the features --source dispatch — wire your new source in the same shape, unless the read_gifts preset registry (a separate, in-flight PR extracting the shared aggregator into a source-keyed dispatcher) has already landed by the time you start, in which case register there instead |
What to build
philanthropy/ingest/_nonprofit_cloud.py: a header-alias map from Nonprofit Cloud's real export labels and API field names onto the canonical contact_id / receive_date / total_amount / gift_type names _civicrm.py already understands; a documented default exclusion parameter for whatever this data model's pledge/commitment-vs-payment split actually is (Nonprofit Cloud separates Gift and Payment as related objects rather than NPSP's stage-on-one-Opportunity approach — find out exactly how from the docs, don't assume it matches NPSP); a nonprofit_cloud_gifts_to_features(...) function; and a read_nonprofit_cloud_gifts(path) reader delegating to read_civicrm_contributions.
- Verify every field name and every commitment/payment vocabulary term against Salesforce's own public Nonprofit Cloud documentation before writing it down — do not guess or recall from memory. Cite the doc URLs you actually used in your PR body, the way PR #233 does for NPSP. Start from Salesforce's own help site and Trailhead, searching for "Nonprofit Cloud" data model / Gift / Payment objects — this issue does not assert any field name as fact, confirm everything yourself.
- Export the new names from
philanthropy/ingest/__init__.py (__all__, alphabetical) and add a stability-tier row to docs/reference/index.md (Tier 2/Beta, same reasoning as the other three bridges — a test enforces every public symbol has a row).
tests/test_nonprofit_cloud.py at the depth of tests/test_npsp.py: header aliasing from every spelling the docs name, the commitment/payment double-count trap and its exclusion, the exclusion set being documented/overridable/disabled by None, currency parsing, schema/dtypes, empty input, missing required columns.
CHANGELOG.md entry under ## [Unreleased], and add yourself to CONTRIBUTORS.md if you'd like.
Hard constraints (same as the rest of this ingest series)
- Dependencies stay scikit-learn/pandas/numpy/joblib only — no new package, no API client, no network code anywhere in
philanthropy/. This is a file-export reader (CSV in, DataFrame out), never a live API connector.
- No new estimator class.
- Build the exclusion logic from Salesforce's own public docs only — never from an institution's actual configuration or data.
- Follow AGENTS.md's order: implement, export, verify with
python -c "from philanthropy.ingest import nonprofit_cloud_gifts_to_features, read_nonprofit_cloud_gifts; print('OK')", then write tests, then make ci.
Verifying
python -m pip install -e ".[dev]"
python -c "from philanthropy.ingest import nonprofit_cloud_gifts_to_features, read_nonprofit_cloud_gifts; print('OK')"
make ci
make riskcov
Claim this issue with a comment before starting (see CONTRIBUTING.md); a maintainer assigns within 24 hours.
What's needed
philanthropy.ingesthas three donor-CRM gift bridges today: CiviCRM, Blackbaud Raiser's Edge, and Salesforce NPSP. Each is a thin, CRM-specific header-alias map plus a commitment/payment exclusion filter (a pledge and the payment against it must never both count towardtotal_gift_amount), delegating the actual roll-up to the shared aggregator inphilanthropy/ingest/_civicrm.py. This issue adds the same bridge for Salesforce Nonprofit Cloud — Salesforce's newer nonprofit data model (Gift, Payment, and related objects), distinct from the older NPSP package this repo already supports.Where to look
philanthropy/ingest/_npsp.pyphilanthropy/ingest/_raisers_edge.pyphilanthropy/ingest/_civicrm.pycivicrm_contributions_to_features,read_civicrm_contributions) and the private helpers (_REQUIRED,_canonical,_normalise_headers,_to_frame,_empty_feature_frame) both examples import and reusetests/test_npsp.pytests/test_nonprofit_cloud.pyphilanthropy/cli.py_FEATURE_SOURCESand thefeatures --sourcedispatch — wire your new source in the same shape, unless theread_giftspreset registry (a separate, in-flight PR extracting the shared aggregator into a source-keyed dispatcher) has already landed by the time you start, in which case register there insteadWhat to build
philanthropy/ingest/_nonprofit_cloud.py: a header-alias map from Nonprofit Cloud's real export labels and API field names onto the canonicalcontact_id/receive_date/total_amount/gift_typenames_civicrm.pyalready understands; a documented default exclusion parameter for whatever this data model's pledge/commitment-vs-payment split actually is (Nonprofit Cloud separates Gift and Payment as related objects rather than NPSP's stage-on-one-Opportunity approach — find out exactly how from the docs, don't assume it matches NPSP); anonprofit_cloud_gifts_to_features(...)function; and aread_nonprofit_cloud_gifts(path)reader delegating toread_civicrm_contributions.philanthropy/ingest/__init__.py(__all__, alphabetical) and add a stability-tier row todocs/reference/index.md(Tier 2/Beta, same reasoning as the other three bridges — a test enforces every public symbol has a row).tests/test_nonprofit_cloud.pyat the depth oftests/test_npsp.py: header aliasing from every spelling the docs name, the commitment/payment double-count trap and its exclusion, the exclusion set being documented/overridable/disabled byNone, currency parsing, schema/dtypes, empty input, missing required columns.CHANGELOG.mdentry under## [Unreleased], and add yourself toCONTRIBUTORS.mdif you'd like.Hard constraints (same as the rest of this ingest series)
philanthropy/. This is a file-export reader (CSV in, DataFrame out), never a live API connector.python -c "from philanthropy.ingest import nonprofit_cloud_gifts_to_features, read_nonprofit_cloud_gifts; print('OK')", then write tests, thenmake ci.Verifying
Claim this issue with a comment before starting (see CONTRIBUTING.md); a maintainer assigns within 24 hours.