> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wegive.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Donation Mapping

> Detailed mapping between WeGive transactions and Planning Center donation records with batch processing

# Donation Mapping

This document details the real mapping between WeGive Transaction objects and Planning Center Donation records. Transactions sync **exclusively through the once-daily batch job** — there is no real-time transaction push (`pushTransaction()` is a no-op stub) — and each donation gets exactly one fund designation; split/multi-fund gifts aren't supported.

## Core Donation Mapping

### Primary Fields

| WeGive Field | Planning Center Field | Direction | Notes |
| - | - | - | - |
| `planning_center_id` | `id` | Both | Correlation ID — the only matching key |
| `amount` | `amount_cents` | Both | Direct mapping |
| `fee` | `fee_cents` | Push only | Sent as `fee * -1` |
| `source_type` | `payment_method` | Push only | Only `card` or `ach` — no check/cash/other mapping |
| `created_at`/`succeeded_at` | `received_at` | Both | |
| `owner` | `person_id` / `person` relationship | Both | Pull requires a matching donor by `planning_center_id`; if none exists, the pull throws `'No owner found'` rather than creating one |

<Note>
  There's no `currency`, `memo`, or generic `reference` field mapping — this integration doesn't track or send those. Currency is implicitly USD; there's no multi-currency support.
</Note>

## Batch Processing Integration

### Daily Batch Organization

**Real batch mechanics** (confirmed against `GeneratePlanningCenterBatchesJob`):

* One scheduled job, once per day at 8:01 AM, per enabled integration
* Selects WeGive transactions by `succeeded_at` falling within the previous calendar day (in the organization's own timezone), with `status = success`, `amount > 0`, `integration_enabled = 1`, and no `planning_center_id` yet
* Every run also does a second "missing" catch-up pass with no date bound, covering any transaction that still lacks a `planning_center_id` for any reason
* Batch name format: `WeGive Import MM/DD/YYYY`
* A day with zero eligible transactions logs the batch as `ignored` — no Planning Center batch is created for it at all

**Batch commitment:**

* The batch is looked up by matching `description` in Planning Center, or created if not found
* After all donations in the run are processed, the batch is committed automatically if its status is `in_progress` — there's no manual/uncommitted state exposed anywhere

### Batch Rollback (Error Handling)

If anything in `createTransactionsBatch()` throws (outside the per-transaction catch described below — e.g. the initial batch/payment-source lookup fails), the **entire run** is rolled back:

* Any `planning_center_id`s set on transactions during that run are cleared back to `null`
* The partial batch itself is deleted in Planning Center

This is a genuine all-or-nothing rollback at the run level, but doesn't apply to individual transaction failures within a successful run (see below).

## Fund Designation Mapping

### Single Fund Designation (the only kind supported)

Each donation includes **exactly one** `Designation` object, resolved in this fixed priority order:

1. The transaction's own linked fund, if it has a `planning_center_id`
2. The integration's configured `default_fund_id`
3. The destination organization's oldest fund that has a `planning_center_id`
4. If none resolve: the transaction throws `Unknown Fund Source` and is **skipped** (not the whole batch)

<Note>
  There is no split/multi-fund designation support in this integration — a WeGive transaction with multiple fund allocations still produces exactly one Planning Center Designation, tied to whichever fund resolves from the priority order above. The full transaction amount goes to that one fund.
</Note>

## Payment Source Management

### WeGive Payment Source

* A single Planning Center payment source named "WeGive" is found by name or created automatically, once per batch run, and reused for every donation in that run
* The original payment method is preserved only via the donation's own `payment_method` attribute (`card` or `ach`) — there's no separate metadata field storing card type, last-4, bank name, or check number

<Note>
  There is no per-payment-method breakdown ("Bank/Check/Cash" mapping) — every WeGive source type other than card resolves to `ach` in the `payment_method` field sent to Planning Center. Check and cash transactions aren't distinguished.
</Note>

## Data Transformation

### Donation Creation Processing (Push, via daily batch)

1. Find-or-create the day's batch and the "WeGive" payment source (once per run)
2. For each eligible transaction: push the donor (if not already linked) and the fund (if not already linked)
3. Resolve the fund via the priority order above; skip (catch + report to Sentry) if none resolves
4. POST the donation with `amount_cents`, `payment_method`, `payment_source_id`, `person_id`, `received_at`, plus one nested `Designation`
5. Store the returned donation ID as `planning_center_id` on the transaction
6. Commit the batch once all transactions are processed

### Pull Operation (Planning Center → WeGive)

1. Page through `giving/v2/donations` (100 per page), including `designations`
2. For each donation, resolve its fund from the included `designations`/`fund` relationship (first designation only)
3. Match the donor by `planning_center_id`; throw `'No owner found'` if none exists (this transaction is skipped, not created with a null owner)
4. Match the fund by `planning_center_id` if present
5. Match a scheduled donation by the pulled donation's `recurring_donation` relationship, if present (read-only correlation — see below)
6. **Guard against CRM-owned-field overwrite**: if the local transaction already has a `correlation_id` (it was actually processed by WeGive) or is still future-dated-pending, its `amount` and fund allocations are preserved as-is — a Planning Center-side edit does not overwrite the real payment data. Otherwise, `amount` and (if fund allocations are enabled for the org) a single fund allocation are rebuilt from the pulled data.

## Recurring Donation Integration

<Note>
  There is no independent recurring-gift sync in this integration. A pulled donation's relationship to a Planning Center recurring donation is read and used only to link the resulting WeGive Transaction to an existing `ScheduledDonation` record by `planning_center_id` — WeGive never creates, updates, or pushes a recurring-gift schedule to Planning Center. If no matching `ScheduledDonation` exists locally, the link is simply not made; nothing fails because of it.
</Note>

## Validation and Error Handling

### Donation Validation

* Non-positive transaction amounts are silently skipped on push (Planning Center's API requires positive amounts) — not an error, not logged
* A transaction whose fund can't resolve throws `Unknown Fund Source`, caught per-transaction and reported to Sentry — the transaction stays unlinked (no `planning_center_id`) and is re-attempted on the next run
* A pulled donation with no matching donor throws `'No owner found'` — that donation import fails; there's no "create the donor from the donation" fallback

### Batch Error Handling

* A single transaction's push failure (thrown inside the per-transaction try/catch) is caught, reported to Sentry, and the loop continues — it does **not** fail the whole batch
* A failure in batch-level setup (the batch lookup, the payment-source lookup/creation) is **not** caught per-transaction — it propagates up and triggers the full run-level rollback described above

## Performance Considerations

* All transaction sync happens through the once-daily batch job — there's no way to force an immediate/real-time transaction push
* API calls are throttled to Planning Center's real limit (70 requests / 20 seconds) via a hardcoded internal counter
* GET requests (fund/payment-source lookups, donation/fund pulls) are retried up to 3 times on transient failures; the mutating POST/PATCH/DELETE calls that create the actual records are not automatically retried on failure

## Known Gaps (not implemented)

* Real-time/immediate transaction push
* Split/multi-fund designations per donation
* Currency field beyond implicit USD
* Memo/reference field sync
* Distinct payment-method categories beyond card/ach
* Independent recurring-gift creation or schedule sync


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.