Donations Manager
Donations Manager
Component File: src/features/donations/components/DonationsManager.tsx Route / navigation: Path /donors, Zustand donorTool = donations. See 00-DONOR-HUB.md. Access Level: Parent Org and Fund Users with Donor Hub access (position-based) Last Updated: April 13, 2026
> Data Layer: This component uses Supabase client functions in src/lib/db.ts (e.g., fetchDonations(), createDonation(), updateDonation(), softDeleteDonation(), checkForDuplicateDonation()). There is no internal REST API; use Supabase clients, RPC, and edge functions.
Recent Updates (April 13, 2026)
DAF sponsor enforcement hardening
- Manual Record mode: Add Donation now hard-blocks save when DAF gift is on and no sponsor is selected (create path).
- Edit Donation dialog: Save now hard-blocks when DAF is on and sponsor is missing (update path).
- DB helper guards:
createDonation()andupdateDonation()now throw explicit errors when a non-interfund DAF gift has nodaf_sponsor_id. - Schema enforcement + legacy backfill: migration
20260615120000_enforce_daf_sponsor_and_memtx_deposit_requirements.sqlbackfills unresolved historical DAF gifts to an org-scoped placeholder sponsor (Unknown DAF (Needs Review)) and addschk_donations_daf_sponsor_required. - Reporting impact: Schedule B and DAF summary flows no longer accept new sponsorless DAF gift writes.
Duplicate guard donor lookup contract fix
- Problem:
checkForDuplicateDonation()was selectingdonors!left(name), but donors are stored asfirst_name+last_namein schema. - Fix: Query now selects
donors!left(first_name,last_name)and composesdonor_namefrom those fields. - Impact: same-day duplicate warning now reliably includes donor names instead of silently degrading when the join shape is invalid.
Add Donation integrity hardening
- Purpose reset parity: In Record Donation mode, changing nonprofit now clears
purposeId(matching Process mode), so a purpose from Fund A cannot be submitted under Fund B. - Stale purpose self-heal: If a previously selected purpose is no longer valid for the selected nonprofit, the form now auto-recovers to that fund's General purpose (or blank if none).
- Notes persistence: Manual Record Donation now writes
newDonation.notestodonations.notesthroughcreateDonation(). - No placeholder purpose writes: The UI no longer submits a synthetic fallback purpose value;
createDonation()resolves the organization default purpose when no explicit purpose is chosen. - Post-commit sync is non-fatal:
syncDonorDonationTypeFromHistory()failures no longer surface as a failed add-donation action after the donation row has already committed.
Recent Updates (April 12, 2026)
Deposit-linked donation void behavior
- Standalone deposits: If a posted deposit has exactly one active
deposit_item, the linked donation now behaves like a direct donation entry for edit/void guard purposes. - Posted multi-item deposits: Donation-level void now supports check deposits and newer regular deposits that stored per-item posting context on
deposit_items, using an additive reversing JE rather than voiding the original batch lines. - Historical regular deposits: Older multi-item regular deposits without stored posting context remain whole-batch only for safe accounting; the UI now surfaces an explicit ambiguity error instead of guessing.
- Donation-context JE drawer: Donation-linked JE drawer void no longer routes through the generic batch-void path for one-donation actions.
Recent Updates (April 7, 2026)
Fund-scoped unassigned gifts + assign permission
- Visibility: Gifts with no linked donor (
donations.donor_idnull) appear in the receiving fund whendonations.organization_idmatches the scoped fund (single-fund entity switcher or parent user viewing one child fund). Parent aggregate (isAdminView) behavior is unchanged. Server: `get_donations_page` and `get_donation_table_stats` include unassigned rows/counts whenp_org_idis set (supabase/migrations/20260510120000_fund_scope_show_unassigned_donations.sql, supersedes the hide rule in20260506120000_hide_unassigned_donations_fund_scope.sql). - Client list query: Donations Manager passes through the assignment filter to the RPC for all scopes (no more forced `assigned`-only fetch in single-fund view).
- Who can assign: `canAssignDonations` in
src/lib/permissions.ts(exposed onusePermissions). `canManageDonations` implies assign when true (parent org and non-enterprise fund users withread_write). If that is false, assign is allowed only when `tier === 'enterprise'` and `fund_user` + `director` + `read_write`. Bookkeeper/assistant and `read_only` directors on enterprise see unassigned rows but cannot assign. - Unassigned tile vs list: The summary tile’s `unassignedCount` (RPC) counts only `payment_status = 'completed'` gifts with no donor, matching historical stats semantics; the paginated list still includes refunded / partially_refunded rows, so counts can differ slightly from row counts when those states appear.
- Assign dialog: Add New donor and prospect rows are hidden when `canManageDonors` is false (enterprise fund directors can assign to existing donors only). i18n:
donations.donationErrors.assignNotPermitted.
Recent Updates (April 13, 2026)
Shared donor picker adoption (Add Donation modal)
- Record + Process donor fields: Add Donation now uses shared `DonorSearch` (same component used in accounting deposit flows) instead of local
Popover + Commandimplementations. - Fund scope: Search context remains based on `newDonation.entityId` / selected receiving fund.
- Metadata: Selection now captures donor linkage (
donorId,donorSource) and known email via shared picker callback details. - Assign/Edit flows unchanged: Standalone Assign dialog and Edit dialog donor assignment continue using shared
AssignDonorSearchItemsbehavior.
Recent Updates (April 6, 2026)
Tax-deductible defaults (donations.is_taxable)
- Rule: Gifts default to tax-deductible (
is_taxable = true) unless the staff explicitly unchecks tax-deductible, the gift is a DAF (is_daf_gift), or the payment is not a charitable donation (e.g. Stripetransaction_typeof service/product/fee, orsource=event_registrationin metadata — event revenue is separate from donations). - Implementation:
resolveDonationIsTaxable/isTaxDeductibleFromDonationRowinsrc/lib/donationDafNormalization.ts; Stripe webhook usessupabase/functions/_shared/donation-tax-deductible.ts; DB backfill + triggersupabase/migrations/20260406183000_donation_tax_deductible_daf_backfill.sql.
Assign Donor flow (donors + prospects; fund-scoped visibility — see April 7, 2026)
- Assign Donor + Edit assign block: Search uses `searchDonorsAndProspects` (same API as record donation), scoped to the gift's receiving fund when known. Results show donors and prospects with shared `AssignDonorSearchItems` cmdk rows (donor vs prospect Badge parity with record mode). Selecting a prospect runs `convertProspectToDonor(..., { targetOrganizationId: donation.organization_id })` then `updateDonation(donationId, { donorId })` (replaces raw
assignDonorToDonationso voided donations and shared update paths stay consistent). Users without `canManageDonors` see donors only in the standalone Assign dialog. - Add New Donor from assign: `AddDonorDialog` receives `defaultEntityId` derived from the gift’s
organization_id(viafetchDonationsPage→organizationIdon list rows) so staff on parent view create donors under the receiving fund, not the parent slug. Dialog syncsentityIdwhen it opens with a non-emptydefaultEntityId. - Record / Process Donation search: donor/prospect search now follows `newDonation.entityId` (the selected receiving fund) instead of the parent aggregate view, so prospect conversion and donor creation stay aligned to the donation's fund.
- Journal entries: Donor-only updates do not run
updateDonationaccounting sync (needsAccountingSyncis false when onlydonorIdchanges). The DB trigger `donations_try_journal_line_link` still runs ondonor_idupdates and may attach `journal_entry_line_id` when it was null. Existing JEs are not rewritten for name/description when a donor is linked later.
Recent Updates (March 27, 2026)
Recent Updates (April 5, 2026)
DAF / payment_method consistency (edit + insert)
- Edit dialog open: If
payment_methodisdafbutis_daf_giftwas false (legacy rows), the form treats the row as a DAF gift and restores sponsor fields. The payment method select is disabled while DAF is on and shows a non-dafplaceholder (bank-transfer) so the list never needs adafoption (Donors CRM edit uses a DAF item in the method list instead—both UIs save the same way). - Edit save:
daf_sponsor_*are cleared when DAF is unchecked;methodis coerced sodafis never saved withoutisDafGift. `updateDonation()` applies the same column alignment ascreateDonation()whenever any DAF-related field is updated (safety net for partial callers). - App + DB safety net:
createDonation()/updateDonation()normalize non-interfund rows insrc/lib/db/donations.ts, andsupabase/migrations/20260505123000_normalize_donation_daf_invariants.sqladds a one-time backfill for legacypayment_method='daf'rows plus a DB trigger/check constraint so direct writes stay aligned too. Interfund transfers are unchanged. - DAF checkbox: Toggling DAF on sets the form method to
bank-transferfor display; a ref stores the prior method so toggling off restores unsaved method changes.
Edit Donation footer — void control
- Void donation uses
variant="destructive"size="sm"in the footer row (left), with Cancel / Save on the right; confirm UI stays inDialogFooterabove that row (see Developer Playbook § Drawer Footer / donation dialog note).
Assigned / Unassigned donation filter now scopes the query set
- Issue: The Donations Manager assignment filter was only applied client-side after
get_donations_pagehad already returned one page of rows. That produced empty table states such as "Showing 0 of 20141" when page 1 contained no matching rows even though later pages did. - Fix:
get_donations_pagenow acceptsp_donor_assignmentso assignment filtering happens server-side before count, sort, and pagination.- Assignment semantics are now based on
donations.donor_id, not the rendered donor label. A donation linked to a donor record is treated as Assigned even if that donor currently renders as Anonymous because first/last name is blank. - Donations Manager now uses a shared donor-display helper so linked anonymous donors render as a normal assigned row instead of showing the Unassigned badge / assign action.
- Files changed:
supabase/migrations/20260505120000_get_donations_page_donor_assignment_filter.sql,supabase/migrations/20260505121000_get_donations_page_assignment_by_donor_id.sql,src/lib/donationAssignment.ts,src/features/donations/components/DonationsManager.tsx,src/lib/db/donations.ts
Recent Updates (April 2, 2026)
Standardized duplicate guard with deposit flows
- Manual Record Donation now uses the same shared guard logic as Check/Regular Deposit via
src/lib/donationEntryGuards.ts. - High-confidence duplicate deposit matches are now hard-blocked (no override), instead of warning-only confirmation.
- Lower-confidence same-day/same-amount collisions still use the confirmation dialog.
- Files changed:
DonationsManager.tsx,donationEntryGuards.ts
Stripe checkout errors (Process mode redirect path)
- `createCheckoutSession` now throws `PaymentInitError` with server codes (`connect_not_ready`, `charges_not_enabled`, etc.) like `createPaymentIntent`. Donations Manager shows `getPaymentInitErrorMessage` in the checkout failure toast. Backend routing matches `resolvePaymentTargetForOrg` (see `STRIPE-SETUP-GUIDE.md`).
Recent Updates (March 22, 2026)
Donation List table — Safari column overlap (P2)
- Issue: On Safari (reported macOS), the desktop Donation List table (
table-fixed) could compress columns so ID and Donor text appeared merged or overlapping; Amount sat too close to the donor column. - Fix: Desktop table uses
min-w-[1080px]inside the horizontal scroll wrapper so layout does not over-compress. Wider min width on Donor / Amount / Status headers;whitespace-nowrapon ID, amount, date, type, method, and status cells; donor cell usesmin-w-0 max-w-[240px]with existing truncate for names. - Files changed:
src/features/donations/components/DonationsManager.tsx
Recent Updates (March 20, 2026)
Record-mode donor linkage guard + cmdk parity (P0)
- Issue: Staff could type a donor name in Record Donation mode and submit without actually linking a donor record, creating rows that appeared as Unassigned later. Add Donation donor pickers also diverged from the
cmdkpattern documented in the Developer Playbook. - Fix:
- Record guard: Record Donation now blocks submit when donor text exists but no linked donor ID is selected. Users must choose a donor from search results, use Add New, or clear the donor field.
- UX parity: Record mode now shows the same donor guidance hints already used in Process mode (
selectDonorHint/searchDonorHint). - cmdk parity: Added
shouldFilter={false}to both Add Donation donorCommanddropdowns (Process + Record) so API-driven search behavior matches the Assign/Edit donor popovers and Playbook §20. - Assigning anonymous/unassigned rows: Existing Assign Donor actions (table row + Edit Donation dialog) remain the supported path to link unassigned/anonymous donations to a donor record after entry.
- Files changed:
src/features/donations/components/DonationsManager.tsx,src/i18n/locales/*/donations.json
Edit Donation dialog — donor assignment + footer layout (P1)
- Issue: The donations table Edit Donation dialog felt unbalanced: void used a full-width destructive button between the form and the footer, and there was no way to assign or change the donor from edit (only the separate Assign Donor flow from the row / unassigned badge).
- Fix:
- Donor block (gated on
canManageDonations): shows current donor or “Unassigned”; same API-driven donor/prospect search pattern as Assign Donor (shouldFilter={false}, uniqueCommandItemvalues, sharedAssignDonorSearchItems); Assign Donor applies via `updateDonation(id, { donorId })` (prospects converted first) for the open donation (handleAssignDonorFromEdit). Uses a dedicated popover open flag (editDonorAssignPopoverOpen) so the edit dialog does not share popover state with the standalone Assign dialog or Add Donation donor pickers. - Void: Void donation is
variant="destructive"size="sm"in the footer row, left-aligned; Cancel / Save stay right-aligned. Confirm step is a compact bordered panel above that row (reason textarea + cancel / confirm void). Dialog content usesmax-h-[90vh] overflow-y-autofor long forms. - Files changed:
src/features/donations/components/DonationsManager.tsx
Recent Updates (March 13, 2026)
Assign Donor Dialog Fix (P0)
1. `cmdk` duplicate `value` collision — Two donor records named "Bethany Community Church" existed. CommandItem used value={donor.name} (not unique). cmdk v1.x deduplicates items by value, collapsing both into one non-functional item. 2. Missing `shouldFilter={false}` — The Command component had no CommandInput, but cmdk's built-in filter was still active (default shouldFilter: true), interfering with the external API-driven search. 3. Dual-purpose state variable — selectedDonorForAssignment was used as both the search input value AND the selected donor display name. After selecting a donor, the debounce timer fired again with the full name, triggering a new search and causing re-render/popover timing issues.
- Issue: Mike Labrum (Deeper Walk) reported that the "Assign Donor" dialog allowed searching for donors but clicking a result did not select them. "Bethany Community Church" was searchable but not selectable.
- Root cause (3 compounding bugs):
- Fix:
- Separated search term (
assignSearchTerm) from display name (selectedDonorForAssignment), matching the working Add Donation flow pattern. - Added
shouldFilter={false}to<Command>since search is API-driven. - Changed
CommandItemvalueto{donor.name}-{donor.id}(unique, prevents deduplication). - Fixed Popover open condition:
assignSearchTerm.length >= 2instead of!!selectedDonorForAssignment. - Added
__none__clear selection item for UX parity with Add Donation. - Added email display below donor names to help disambiguate duplicate names.
- Replaced mojibake
✕character with<XCircle>Lucide icon on the clear button. - Added
disabled={!selectedDonorIdForAssignment}to the Assign button. - Added
onFocushandler to re-open results when clicking back into the input. - Pattern rule: When using
cmdkCommandwith API-driven search (noCommandInput), ALWAYS: (a) setshouldFilter={false}, (b) use uniquevalueprops containing IDs, (c) keep search term and display name in separate state variables. - Files changed:
src/features/donations/components/DonationsManager.tsx
Recent Updates (March 8, 2026)
Donation Void Standardization
- Void reason is now required in both donation management entry points.
softDeleteDonation()now rejects blank reasons instead of treating them as optional audit data.- Donation notes are preserved on void and now append
VOIDED: ...instead of overwriting the prior note history. - Direct donation voids still reverse the linked JE first; deposit-driven donation voids still use
skipJeVoid = trueto avoid double-voiding.
Recent Updates (March 6, 2026)
Robust Donation List Search (P1)
- Issue: Donation list search mainly matched donor name and purpose, so amount/date searches and other donation metadata were easy to miss.
- Fix:
fetchDonationsPaginated()now uses shared search helpers fromsrc/lib/searchUtils.tsplus a donation-specific matcher insrc/lib/db/donations.ts. - Search now matches: donation amount, donation date, donor name, donation ID, fund name, purpose, effective donation type, payment method, and payment status.
- Architecture: This reuses the same shared matcher core as other richer search surfaces, but keeps a donation-specific field adapter instead of forcing one universal search function across donors and fund accounting.
- Files Changed:
src/lib/searchUtils.ts,src/lib/db/donations.ts
Donor + Prospect Entry Search (P1)
- Issue: The add-donation donor picker still behaved like a narrow name-only search, which made it harder to find existing donors or prospects by email, phone, spouse name, or source.
- Fix:
searchDonorsAndProspects()insrc/lib/db.tsnow uses the same shared multi-word search core for donor/prospect entry search. - Search now matches: donor first/last name, spouse first name, email, phone; plus prospect name, email, phone, and source.
- Files Changed:
src/lib/db.ts,src/lib/searchUtils.ts
Recent Updates (February 28, 2026)
Donation Void & Safety Audit
- Void Donation UI: Added inline void section to the Edit Donation dialog, gated behind
canWriteAccountingpermission. Includes reason field and two-step confirmation. CallssoftDeleteDonation(). - `softDeleteDonation()` now auto-reverses JE lines: If the donation has a linked
journal_entry_line_id, the function automatically callsvoidJournalEntry()to create reversing entries. NewskipJeVoidparameter for callers that already handle JE voiding (deposit batch flows,voidJournalEntrycascade). - Duplicate detection:
handleAddDonation()in Record mode now callscheckForDuplicateDonation(orgId, amount, date)before inserting. Shows a warning dialog with matching donor names if duplicates are found. - Improved JE failure error UX: If
createDonation()succeeds but JE creation fails, the catch block now shows a specific 8-second toast explaining the donation exists but won't appear on financial reports, instead of a generic "Record failed" message. - Files changed:
src/lib/db/donations.ts,src/lib/db/deposits.ts,src/lib/db/journal-entries.ts,src/features/donations/components/DonationsManager.tsx, 6 locale files (donations.json) - DB fix: Voided duplicate Terry Benware $82.62 donation (God's Grace, 2026-02-03) created via deposit batch duplicating a Stripe webhook donation. Corrected deposit batch JE from $134.37 → $51.75.
Previous Updates (February 20, 2026)
Donation → Journal Entry FK Linkage
- Schema change: Added
journal_entry_line_id UUIDFK column ondonations→journal_entry_lines(id)with partial index. Links each donation directly to its debit-side JE line. - Postgres (Mar–Apr 2026): Trigger `donations_try_journal_line_link` attempts heuristic
journal_entry_line_idfor completed non-Stripe-rail donations (donation_try_link_journal_entry). Rows with any ofstripe_payment_intent_id,stripe_session_id,stripe_charge_id, orstripe_invoice_idset are skipped so Stripe webhook paths own the FK. See DEVELOPER-PLAYBOOK §45. - Backfill: 19,390 / 19,432 active donations linked (99.8%) using three strategies:
- A) Stripe PI →
reference_id - B) org/fund + date + amount on asset-type debit lines
- C) org/fund + date on revenue credit lines (Aplos aggregate entries)
- `createDonation()` now captures the debit line ID from
createJournalEntry()return and saves it back to the donation. - `fetchJournalEntryForDonation()` uses FK as primary path (single query, instant). Falls back to Strategy A/B/C matching for the 42 unlinked donations, and auto-backfills the FK on first hit.
- Void tracking: Added
voided_by(UUID → users) andvoided_at(timestamp) columns to donations. - Files changed:
src/lib/db/donations.ts,documentation/frontend/DEVELOPER-PLAYBOOK.md§16 - Migration:
backend/migrations/20260220_donation_journal_entry_line_fk.sql
Previous Updates (February 12, 2026)
Bug Fix: Donation Journal Entry Creation Failure
- Root cause:
createDonation()insrc/lib/db/donations.tssilently failed to create journal entries for child org donations. ThegetEntityId(parentOrgUUID)call returnednullfor the parent org UUID, which was unsafely cast viaas EntityId. The resultingcreateJournalEntry()call threw inside atry/catchthat swallowed the error withconsole.warn. - Impact: ~19,557 donations existed without corresponding
journal_entry_lines, making them invisible on the Income Statement by Fund report. Reported by Maribeth Carlton for Jenny Torgerson (Deeper Walk), Anita Seivert (Breathe Pray Worship), and Kelly Majewski (Awakenings). - Frontend fix (`src/lib/db/donations.ts`):
- Line 697: Replaced
getEntityId(journalOrgId) as EntityIdwithgetEntityId(journalOrgId) || journalOrgId— falls back to raw UUID when slug mapping unavailable - Changed
catchblock from silentconsole.warntothrowso JE failures surface to the user - Database migration (`backfill_donation_journal_entries`):
- Created 39,114 journal entry lines (19,557 balanced pairs: debit Cash 1000, credit Revenue 4000) for all completed donations missing JEs
- Uses parent org's accounts (InFocus Ministries) since child orgs don't have their own
- Sets
organization_idandfund_idto the donation's org (matching existing pattern) - Database migration (`backfill_null_purpose_ids`):
- Fixed NULL
purpose_idon donations by inferring from the donor's most common purpose for the same organization - Updated corresponding JE lines with the inferred purpose
Previous Updates (January 31, 2026)
Automatic Receipt Emails for Manual Donations
- Manual donations now trigger receipt emails - When recording a donation via "Add Donation" (Record Donation mode), a donation receipt email is automatically sent to the donor
- Receipt is only sent if the donation has an associated donor with an email address
- Uses the same
send-donation-receiptedge function as Stripe payments
Stripe card refunds vs void (accounting + donor email)
- Stripe refunds (Donor Payment Management, donor profile refund dialog, Donations Manager where applicable, or Stripe Dashboard) go through Stripe → `charge.refunded` in `stripe-webhook`. That path inserts a `donation_type: refund` row, updates the original gift’s `payment_status`, and appends new
journal_entry_lines(DR donations revenue, CR stripe receivable) for the refunded amount — it does not void or edit the original payment JE lines (audit trail). Donors receive transactional `send-donation-refund-notice` (same fund / fiscal-sponsor branding as receipts). See DEVELOPER-PLAYBOOK.md §13 (*Stripe refunds*). - Void donation (
softDeleteDonation, often check/cash or corrections) follows separate rules and may reverse linked JEs via the void pipeline — do not conflate with Stripe refund.
Previous Updates (January 27, 2026)
Date-Filtered Stats
- Total Amount stat card now reflects the selected time period filter
- When a date filter is selected (Month, YTD, Q1-Q4, Last Year), the Total Amount shows donations for that period only
- The stat card label displays the selected period (e.g., "Total Amount (Month)")
- Recurring count and Unassigned count also respect the date filter
Filter Bar Reordering
- Timeframe dropdown moved to the right of Filter button for better UX
- New order: Search → Sort → Filter → Timeframe → Page Size
Overview
The Donations Manager is a comprehensive donation tracking and management system. It displays all donations across the organization with advanced filtering, sorting, and search capabilities. Users can add new donations, assign unassigned donations to donors, process refunds, and send receipts.
UI Features
Main Features
- Search: Search by donation amount, donation date, donor name, donation ID, fund, purpose, type, payment method, or payment status
- Sort Options:
- Date (Newest, Oldest)
- Amount (High to Low, Low to High)
- Donor name (A-Z, Z-A)
- Filters:
- Status (All, Completed, Pending, Failed, Refunded)
- Type (All, One-time, Recurring, Event)
- Assignment (All, Assigned to donor, Unassigned)
- Actions:
- Add new donation
- Assign donation to donor
- Add new donor (from donation flow)
- View donor profile (click donor name)
- Send receipt
- Process refund
Donation Table Columns
- Status badge (Completed, Pending, Failed, Refunded)
- Donor name (clickable to view profile)
- Amount
- Date
- Type (One-time, Recurring, Event)
- Payment method
- Cause/Campaign
- Actions dropdown
Add Donation Dialog
Two modes are available:
#### Record Donation Mode For manually recording donations received outside the platform:
- Donor selection (searchable dropdown across donors and prospects by name, spouse name, email, phone, or prospect source, or "Add New Donor")
- Amount
- Date
- Type (One-time, Recurring)
- Payment method (Credit Card, Check, Cash, ACH, Wire)
- Purpose (from Purpose Manager - fund-specific + global purposes; organization default is used when none is selected)
- Notes (persisted to
donations.notes) - Receipt sent checkbox
- Tax deductible checkbox
Accounting Behavior: Manual donations now automatically create journal entry lines for GL posting using purpose-based account defaults (typically DR `default_checking` for non-Stripe/manual methods or DR `stripe_receivable` for Stripe-backed flows, CR `donations_revenue`). The posting code resolves account IDs through the account-default mapping system / get_default_account_id RPC with parent-org fallback; it does not hard-code a single cash/revenue account UUID or account code. Deposit batch flows still post a single batch journal entry and skip per-donation GL entries. Deposit intake (Check Deposit + Regular Deposit) uses that same effective defaulting model for donations_revenue: a fund can rely on its own override or inherit the parent mapping, but the app still refuses to fall back to the first revenue account in the chart when no default resolves.
Process Payment Mode
For processing live payments via Stripe (`create-payment-intent` embedded flow). Charge destination follows `resolvePaymentTargetForOrg`: Connect on the selected org’s ancestor chain, or `legacy_direct` platform charges when the org is under `DIRECT_STRIPE_PAYOUT_ORG_ID`. If neither applies, the UI shows a payment-init error (`connect_not_ready` / `charges_not_enabled` via `getPaymentInitErrorMessage`).
Redirect checkout (Process flow alternate): When the flow uses `createCheckoutSession`, the same resolver and error codes apply.
- Step 1 - Donation Details:
- Entity/Organization selection (pre-selected based on current filter, overridable by parent org)
- Donor selection (required - searchable dropdown or "Add New Donor")
- Amount selection (suggested amounts: $25, $50, $100, $250, $500, $1000 or custom)
- Cover processing fee option (2.9% + $0.30)
- Recurring donation toggle with frequency selection (weekly, monthly, quarterly, annually)
- Total amount display with fee breakdown
- Step 2 - Payment:
- Stripe PaymentElement for secure card entry
- Submit payment button
- Cancel option to return to Step 1
Note: Donor is required for Process Payment mode to ensure proper donation tracking and receipt generation.
Assign Donor Dialog
- Search and select existing donor (API-driven search via
searchDonorsAndProspects, donors only) - Results show donor name + email for disambiguation of duplicate names
- Clear selection item at top of dropdown
- Or create new donor inline via "Add New" button
- Assign button disabled until a donor is selected
- Uses
shouldFilter={false}onCommand+ uniquevalueprops (name-id) — see pattern rule in March 13 2026 update
Edit Donation Dialog (table pencil)
- Opens from the per-row edit control when the user has donation management permission (
canManageDonations). - Fields: Amount, date, type, payment method, purpose (when purposes loaded), tax-deductible, DAF gift + sponsor (same patterns as add/edit elsewhere).
- Donor: Dedicated section to view current donor and assign or change the linked donor (March 20, 2026) — same search UX as Assign Donor; does not require closing the dialog to open the standalone Assign dialog.
- Void: Requires accounting write permission (
canWriteAccounting); two-step flow with required reason; destructive actions aligned with footer buttons (March 20, 2026 layout).
Data Requirements
Donation List Data
- id (uuid) - Unique identifier
- donor_id (uuid, nullable) - Linked donor
- donor_name (string, nullable) - Donor name (if assigned)
- amount (decimal) - Donation amount
- currency (string) - Currency code (default: USD)
- donation_date (date) - Date of donation
- donation_type (string) - 'one-time', 'recurring', 'pledge'
- payment_method (string) - 'credit_card', 'check', 'cash', 'ach', 'wire'
- payment_status (string) - 'completed', 'pending', 'failed', 'refunded'
- transaction_id (string, nullable) - External payment processor ID
- fund_id (uuid, nullable) - Designated fund
- fund_name (string, nullable) - Fund name
- campaign_id (uuid, nullable) - Associated campaign
- campaign_name (string, nullable) - Campaign name
- designation (string, nullable) - Specific purpose/cause
- is_anonymous (boolean) - Anonymous donation flag
- is_recurring (boolean) - Recurring donation flag
- recurring_frequency (string, nullable) - 'weekly', 'monthly', 'quarterly', 'yearly'
- check_number (string, nullable) - Check number if applicable
- deposited_at (datetime, nullable) - When deposited
- receipt_sent (boolean) - Receipt sent flag
- receipt_sent_at (datetime, nullable) - When receipt was sent
- notes (text, nullable) - Internal notes
- created_at (datetime) - When record was created
- updated_at (datetime) - When record was updated
Data Mutations
- Create Donation: Add new donation record
- Update Donation: Edit donation details
- Delete Donation: Remove donation (soft delete)
- Assign Donor: Link donation to existing or new donor
- Send Receipt: Email receipt to donor
- Process Refund: Refund a donation
Request/Response Schemas
Donation Schema
interface Donation {
id: string;
organization_id: string;
donor_id?: string;
donor_name?: string;
amount: number;
currency: string;
donation_date: string;
donation_type: 'one-time' | 'recurring' | 'pledge';
payment_method: 'credit_card' | 'check' | 'cash' | 'ach' | 'wire';
payment_status: 'completed' | 'pending' | 'failed' | 'refunded';
transaction_id?: string;
fund_id?: string;
fund_name?: string;
campaign_id?: string;
campaign_name?: string;
designation?: string;
is_anonymous: boolean;
is_recurring: boolean;
recurring_frequency?: 'weekly' | 'monthly' | 'quarterly' | 'yearly';
recurring_day?: number;
recurring_end_date?: string;
check_number?: string;
deposited_at?: string;
receipt_sent: boolean;
receipt_sent_at?: string;
notes?: string;
refund_amount?: number;
refund_reason?: string;
refunded_at?: string;
created_at: string;
updated_at: string;
}
interface RecurringDonation {
id: string;
donor_id: string;
donor_name: string;
amount: number;
recurring_frequency: string;
recurring_day: number;
next_charge_date: string;
recurring_end_date?: string;
status: 'active' | 'cancelled';
created_at: string;
}Authentication & Authorization
Required Permissions
donations:read- View donationsdonations:write- Create and update donationsdonations:delete- Delete donationsdonations:refund- Process refundsdonations:send_receipt- Send receipts
Role-Based Access
- Admin: Full access to all donation operations
- Manager: Can view, create, update, refund, send receipts
- Staff: Can view, create, update; cannot delete or refund
- Volunteer: No access to donations manager
Business Logic & Validations
Frontend Validations
- Amount must be greater than 0
- Date cannot be in the future
- Donor required (either existing or new)
- Payment method required
- Organization must be selected
- DAF gifts require a selected DAF sponsor
- Refund amount cannot exceed original donation amount
- Email required for sending receipt
Backend Validations (Rails)
- Amount must be positive decimal
- Valid donation date
- Valid payment method
- Valid payment status
- Valid donation type
- Organization exists and user has access
- Donor exists (if donor_id provided)
- Non-interfund DAF gifts require
daf_sponsor_id(createDonation/updateDonationguards +chk_donations_daf_sponsor_required) - Fund exists (if fund_id provided)
- Campaign exists (if campaign_id provided)
- Cannot refund already refunded donation
- Cannot refund more than original amount
Business Rules
- Donations are organization-specific (multi-tenant)
- Unassigned donations can be assigned to donors later
- Refunds create negative entries or update status to 'refunded'
- Recurring donations automatically create new donation records
- Receipt emails use organization's email templates
- Anonymous donations hide donor information in public reports
- Deposited donations cannot be deleted (only voided)
- Total donated amount on donor profile updates automatically
State Management
Local State
searchQuery- Search inputsortBy- Current sort optionstatusFilter- Status filtertypeFilter- Type filterassignmentFilter- Assignment filteraddDonationOpen- Add dialog stateassignDonorOpen- Assign dialog stateaddNewDonorOpen- New donor dialog stateselectedDonationId- Selected donation for actionsnewDonation- Form state for new donationnewDonor- Form state for new donor
Global State (AppContext)
selectedEntity- Current organizationsetSelectedDonor- Navigate to donor profilesetDonorTool- Switch to donors view
Dependencies
Internal Dependencies
AppContext- Global statemockData.ts- TO BE REMOVED -getAllDonationRecords(),getAllDonors()- UI components (Card, Button, Table, Dialog, etc.)
External Libraries
lucide-react- Iconssonner- Toast notifications
Error Handling
Error Scenarios
1. Network Error: Show toast "Unable to load donations. Please try again.", retry button 2. Validation Error: Show inline field errors 3. Permission Error: Show toast "You don't have permission to perform this action" 4. Not Found: Show "Donation not found" message 5. Refund Failed: Show toast "Refund failed: [reason from API]" 6. Receipt Send Failed: Show toast "Failed to send receipt. Please try again." 7. Assign Donor Failed: Show toast "Failed to assign donor"
Loading States
- Initial load: Skeleton table with 10 rows
- Pagination: Loading spinner overlay on table
- Form submission: Disable submit button, show spinner
- Refund processing: Disable button, show spinner
- Receipt sending: Disable button, show spinner
Mock Data to Remove
mockData.ts-getAllDonationRecords()functionmockData.ts-getAllDonors()function (for donor selection)mockData.ts-DonationRecordinterface (move to types)
Migration Notes
Phase 1: Data Layer Integration
1. Create/expand Supabase helpers in src/lib/db.ts (or scoped donations modules) 2. Create src/types/donation.ts with TypeScript interfaces 3. Replace getAllDonationRecords() with API call 4. Add loading and error states
Phase 2: CRUD Operations
1. Implement create donation flow 2. Implement update donation flow 3. Implement delete donation flow 4. Test all operations
Phase 3: Advanced Features
1. Implement assign donor flow 2. Implement send receipt flow 3. Implement refund flow 4. Implement recurring donation management
Phase 4: Integration with Donors
1. Ensure clicking donor name navigates to donor profile 2. Ensure donor selection dropdown loads from API 3. Test create new donor from donation flow
Related Documentation
- 01-DONORS-CRM.md - Related donor management
- 04-DONOR-PORTAL.md - Public donation portal
- 08-CHECK-DEPOSIT-MANAGER.md - Deposit processing
- 01-DATA-SCHEMA.md - Historical donation data model
Synced from IFMmvp-Frontend documentation: pages/donor-hub/02-DONATIONS-MANAGER.md
Ready to Get Started?
See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.
Questions? Email us at steven@getalignmint.org