Data Model Reference
Recurring Donation#
Represents a donor's ongoing recurring giving schedule (subscription profile) supporting a campaign. This package self-manages the billing schedule: a daily scheduler charges each installment off-session against a saved Stripe payment method, so there is no native Stripe subscription object involved. Related to the donor via the dual Contact/Person-Account lookup pattern and to Designation__c/Recurring_Designation__c for fund allocation; each state change or payment attempt is logged to a child Recurring_Donation_History__c record.
custom · 39 fields · ReadWrite · Recurring Giving
Why it exists#
Stripe Billing/Subscriptions is deliberately not used here — instead a daily batch job walks
due Recurring_Donation__c records and charges each installment off-session against the
donor's saved Payment_Method__c, inserting a normal Opportunity per installment via
Recurring_Donation__c (so every past charge is still a first-class, reportable donation, not
a line on an external subscription object). Every state change or charge attempt — success,
decline, retry — is appended to a child Recurring_Donation_History__c record, which is what
powers dunning and renewal reporting without needing to replay Stripe's event log.
Fund allocation is split by design/reuse: Designation__c sets the default allocation for the
schedule, while individual installments can override it via Recurring_Designation__c.
Relationships#
| Direction | Field | Related object | Relationship type |
|---|---|---|---|
| → out | Campaign__c |
Campaign | Lookup |
| → out | Contact__c |
Contact | Lookup |
| → out | Designation__c |
Designation | Lookup |
| → out | Donor_Account__c |
Account | Lookup |
| → out | Payment_Account__c |
Payment Account | Lookup |
| → out | Payment_Method__c |
Payment Method | Lookup |
| ← in | Recurring_Donation__c on Opportunity |
Opportunity | Lookup |
| ← in | Recurring_Donation__c on Recurring Designation |
Recurring Designation | MasterDetail |
| ← in | Recurring_Donation__c on Recurring Donation History |
Recurring Donation History | MasterDetail |
| ← in | Recurring_Donation__c on Transaction Log |
Transaction Log | Lookup |
Fields#
| Field | Label | Type | Notes |
|---|---|---|---|
Amount__c |
Amount | Currency | The amount charged to the donor's card each billing cycle. |
Campaign__c |
Campaign | Lookup → Campaign |
The campaign this recurring gift supports. Docs: Also the field CancelGoalReachedRecurringQueueable filters on when a campaign's goal is newly crossed with Recurring_Behaviour_At_Goal__c set to Cancel — every Active schedule pointing at that campaign is a candidate for bulk auto-cancellation, chunked at 200 with Transaction Finalizer retry. |
Cancellation_Reason__c |
Cancellation Reason | Text | Why this recurring donation was cancelled, if known. Docs: Also populated by system-initiated cancellations, not just donor/admin ones — CancelGoalReachedRecurringQueueable calls RecurringDonationService.cancelSubscriptions() with a fixed reason string when Campaign.Recurring_Behaviour_At_Goal__c is Cancel and the campaign's goal is newly crossed, so a blank-looking manual cancel and an automatic goal-triggered one are both represented here, distinguishable only by the reason text. |
Contact__c |
Contact | Lookup → Contact |
The donor for this recurring gift (standard Account orgs). |
Currency_ISO_Code__c |
Currency ISO Code | Text | Three-letter currency code for this recurring gift, e.g. USD. |
Designation__c |
Designation | Lookup → Designation__c |
The fund or program this recurring gift supports, if not split across multiple funds. |
Donor_Account__c |
Donor Account | Lookup → Account |
The donor for this recurring gift (Person Account orgs). |
Donor_Covered_Fees__c |
Donor Covered Fees | Checkbox | Checked when the donor agreed to cover the processing fee. Each renewal then charges their gift amount plus the current fee, so the amount taken is slightly more than the Amount field. Uncheck it to bill only the gift amount from the next renewal onward. Docs: Amount__c on a schedule is always the GIFT, never the grossed-up charge — this flag is what tells RecurringRenewalBatch to add a fee on top at charge time. Storing the gross in Amount__c instead would have been simpler but wrong twice over: the designation splits that every installment inherits would allocate the fee to a fund, and the fee would freeze at whatever the rate was on signup day, so a later Settings__c rate change would never reach existing donors. Recomputing per cycle from FeeCoverageService.computeFee also means the donor's coverage does not silently lapse after month 1, which is what happens if the fee is only ever applied to the signup charge. |
End_Date__c |
End Date | Date | The date after which this recurring gift should stop being charged. Leave blank for a gift that continues until the donor stops it. Docs: Unlike Installment_Cap__c (enforced after a charge), this one is enforced BEFORE any charge: it is part of RecurringDonationsSelector.dueCondition(), so a schedule past its end date is never even selected, and RecurringRenewalBatch.isStillDue() re-checks it against a freshly read row. Nothing marks such a schedule Completed on the day it expires — it simply stops being due, and processRenewal marks it Completed the next time it would otherwise have renewed. |
Failed_At__c |
Failed At | DateTime | When this recurring gift stopped retrying and moved to Failed. Used to auto-cancel it after the configured grace period — do not edit directly. |
Failed_Payment_Count__c |
Failed Payment Count | Number | Number of renewal charges in a row that have failed. |
Frequency__c |
Frequency | Picklist | How often the donor is charged for this recurring gift. |
Installment_Cap__c |
Installment Cap | Number | Set this to end the gift automatically after a fixed number of payments — enter 12 for a "$50 a month for a year" pledge. Leave blank for a gift that continues until the donor stops it. Docs: The cap is enforced AFTER a charge, not before one: RecurringDonationService.processRenewal increments Installments_Paid__c on a successful capture and, when the incremented count reaches this cap, flips Status__c to Completed and clears Next_Payment_Date__c. That is why the due-selector needs no field-to-field comparison (SOQL cannot express one) — a completed schedule simply stops matching the Status__c = 'Active' predicate. A cap lowered below the count already paid therefore ends the schedule at the NEXT successful charge, not retroactively. |
Installments_Paid__c |
Installments Paid | Number | A running count of payments successfully collected on this recurring gift. The system maintains this — do not edit it. Docs: Maintained by Apex, not a roll-up summary: Recurring_Donation__c ↔ Opportunity is a lookup, not a master-detail, so the platform cannot roll up the installment count. Gift #1 is counted at signup (RecurringDonationService.createScheduleFromStaging sets it to 1) and each later successful renewal increments it in processRenewal's success branch. A schedule created before this field existed starts at 0 and counts forward from its next renewal, so a cap applied to a long-running legacy schedule grants it a full fresh term — deliberate, since the historical count cannot be reconstructed for schedules whose installments predate the field. |
Last_Dunning_Email_Sent_At__c |
Last Dunning Email Sent At | DateTime | When a payment-failure email was last sent to the donor. |
Last_Error__c |
Last Error | LongTextArea | The last error encountered trying to charge this recurring gift. |
Last_Payment_Amount__c |
Last Payment Amount | Currency | The amount of the most recent successful payment. |
Last_Payment_Date__c |
Last Payment Date | Date | The date the most recent payment was successfully charged. |
Mandate_Accepted__c |
Mandate Accepted | Checkbox | Whether the donor has agreed to the recurring-charge authorization. Docs: Actively enforced, not just a record of consent — RecurringDonationsSelector's active-batch query requires Mandate_Accepted__c = true, so a schedule created without it set is silently excluded from every renewal charge, not just flagged for audit. |
Mandate_Timestamp__c |
Mandate Timestamp | DateTime | When the donor agreed to the recurring-charge authorization. |
Mandate_Version__c |
Mandate Version | Text | Which version of the authorization terms the donor accepted. |
NPSP_Recurring_Donation_Id__c |
NPSP Recurring Donation Id | Text | Internal reference to the mirrored NPSP recurring donation record; not for manual entry. Docs: Part of the optional NPSP extension, detected at runtime via Schema.describeSObjects() rather than a hard package dependency — the core package works whether or not NPSP is installed. When present, this schedule maps to npe03__Recurring_Donation__c as a mirror, not a replacement; Designation__c/Recurring_Designation__c map to NPSP's GAU allocations separately. |
Next_Payment_Date__c |
Next Payment Date | Date | The next date this recurring gift is scheduled to be charged. Docs: Frozen on the missed date once a charge declines, not advanced day to day. A decline sets Next_Retry_Date__c and leaves this field alone; RecurringDonationsSelector's due-schedule query is gated so the normal branch only fires when Next_Retry_Date__c is null, otherwise a stale Next_Payment_Date__c would re-select the schedule every run and defeat the dunning cadence. Only a successful charge advances this field, via advanceScheduleDate. |
Next_Retry_Date__c |
Next Retry Date | Date | The next date a failed payment will automatically be retried. Docs: Computed as today plus SettingsService.retryIntervalDays(), the same flat interval for every attempt — see the doc note on Retry_Attempt__c about the stale fixed 1/3/5-day cadence described elsewhere. |
Paused_Until__c |
Paused Until | Date | The date this recurring gift will automatically resume, if paused. |
Payment_Account__c |
Payment Account | Lookup → Payment_Account__c |
Which of your configured Stripe accounts renewals for this schedule are charged to. Set automatically when the schedule starts — changing the fundraiser's payment account later does not, and must not, move an existing schedule. Docs: Second rung of the renewal resolution chain: Payment_Method__r.Payment_Account__c, then this field, then Campaign.Payment_Account__c for rows predating both (see RecurringRenewalBatch). Stamped from Donation_Staging__c.Payment_Account__c, which is already in hand at RecurringDonationService.createScheduleFromStaging — the account was decided at intent time, so this is a snapshot of a known fact rather than a fresh resolution. deleteConstraint omitted (SetNull) for the same reason as Payment_Method__c.Payment_Account__c. |
Payment_Method__c |
Payment Method | Lookup → Payment_Method__c |
The saved payment method record associated with this recurring gift, if tracked. Docs: Kept in sync automatically, not just set once at signup. RecurringDonationService. updatePaymentMethod calls PaymentMethodService.saveWalletCard and registers the returned Payment_Method__c on this schedule via uow.registerRelationship, so a card swap updates both this lookup and Stripe_Payment_Method_Id__c together in the same unit of work. |
Re_Auth_Link_Sent_At__c |
Re-Auth Link Sent At | DateTime | When a re-authentication email was last sent to the donor. |
Re_Auth_Required__c |
Re-Auth Required | Checkbox | Whether the donor needs to re-authenticate their card before the next charge can go through. Docs: Set by RecurringDonationService.processRenewal's requiresAction branch and excluded from the renewal batch's selection query (RecurringDonationsSelector) alongside Mandate_Accepted__c and Status__c — a schedule needing re-auth is simply skipped by the batch, not retried, until a donor completes 3-D Secure and this is cleared. |
Retry_Attempt__c |
Retry Attempt | Number | How many retry attempts have been made after a failed payment. Docs: The description above states a fixed 1/3/5-day retry schedule; that is stale. The current implementation in RecurringDonationService.processRenewal uses a flat interval read from SettingsService.retryIntervalDays(), capped by SettingsService.maxFailedPaymentRetries(), not a hardcoded per-attempt day count. Trust the Apex over this description until it is corrected. |
Status__c |
Status | Picklist | The current state of this recurring gift. Docs: Pending is the picklist's UI default, but RecurringDonations.onBeforeInsert (setDefaults) assigns Active to any record inserted with a null Status__c, so a schedule created through normal Apex flows practically never lands on Pending — that value only shows up if something explicitly sets it. |
Stripe_Customer_Id__c |
Stripe Customer Id | Text | Internal Stripe reference for this donor's saved payment method; not for manual entry. |
Stripe_Payment_Method_Id__c |
Stripe Payment Method Id | Text | Internal Stripe reference for the saved card charged each cycle; not for manual entry. |
URL_Parameters__c |
URL Parameters | LongTextArea | The full set of tracking parameters from the link the donor clicked, stored as JSON. Reference only — report on the UTM fields instead. Docs: Set once at signup by RecurringDonationService from Donation_Staging__c.Options_JSON__c -> attribution, and is the template every renewal Opportunity copies its UTM fields from. Deliberately never refreshed — it records how the donor was acquired, not how they last interacted. |
UTM_Campaign__c |
UTM Campaign | Text | The marketing appeal name from the link, e.g. year-end-2026. This is the marketer's label, not the Salesforce Campaign — filled in automatically. Docs: Set once at signup by RecurringDonationService from Donation_Staging__c.Options_JSON__c -> attribution, and is the template every renewal Opportunity copies its UTM fields from. Deliberately never refreshed — it records how the donor was acquired, not how they last interacted. |
UTM_Content__c |
UTM Content | Text | Which link or creative variant was clicked, for A/B tests. Filled in automatically — usually blank. Docs: Set once at signup by RecurringDonationService from Donation_Staging__c.Options_JSON__c -> attribution, and is the template every renewal Opportunity copies its UTM fields from. Deliberately never refreshed — it records how the donor was acquired, not how they last interacted. |
UTM_Medium__c |
UTM Medium | Text | The channel that carried the link, e.g. email, cpc or social. Filled in automatically — leave it alone. Docs: Set once at signup by RecurringDonationService from Donation_Staging__c.Options_JSON__c -> attribution, and is the template every renewal Opportunity copies its UTM fields from. Deliberately never refreshed — it records how the donor was acquired, not how they last interacted. |
UTM_Source__c |
UTM Source | Text | Where this gift came from, e.g. newsletter or facebook. Filled in automatically from the link the donor clicked — leave it alone. Docs: Set once at signup by RecurringDonationService from Donation_Staging__c.Options_JSON__c -> attribution, and is the template every renewal Opportunity copies its UTM fields from. Deliberately never refreshed — it records how the donor was acquired, not how they last interacted. |
UTM_Term__c |
UTM Term | Text | The paid-search keyword that led here, if any. Filled in automatically — usually blank. Docs: Set once at signup by RecurringDonationService from Donation_Staging__c.Options_JSON__c -> attribution, and is the template every renewal Opportunity copies its UTM fields from. Deliberately never refreshed — it records how the donor was acquired, not how they last interacted. |