You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Track financial contributions from organizations across time, separate from the recurring-membership tier system. Donations are one-time or campaign-attributed contributions that don't fit org_memberships (annual recurring) or event_sponsorships (event-tied).
Requirements
New org_donations table: id, organization_id (FK → organizations.id ON DELETE restrict), amount_cents (bigint), currency (text, ISO 4217, default 'USD'), donation_date (date NOT NULL), campaign (text, nullable — free-form for now), fiscal_year (int, nullable), notes (text), created_at/updated_at/recorded_by_user_id (audit columns)
Migration adds the table + index on (organization_id, donation_date DESC) for fast per-org history queries
API: GET/POST/PATCH/DELETE /admin/organizations/:id/donations; list returns rows sorted by donation_date desc with per-currency totals as aggregates
Org detail page: new "Donations" tab listing the org's contribution history with running totals by year
Permissions: donation writes gated on staff tier (canEditOrganizations); consider a canRecordDonations policy if we want to lock this down further later
Audit middleware captures all donation mutations (organizations.donation.create, .update, .delete)
Context
Why not extend org_memberships? Memberships are recurring annual tiers with one active row per org; donations are arbitrarily many transactions across time, often campaign-attributed. Why not extend event_sponsorships? Donations aren't event-bound. The clean split is three independent tables sharing only the organization_id FK.
Implementation Notes
amount_cents as bigint keeps us currency-precision-safe without a numeric type. Storing currency per-row instead of org-level lets a single org's history span currency changes (e.g., GBP → USD after a UK org's US subsidiary takes over donations).
Fiscal year is nullable because US-RSE doesn't have one defined yet — leaving the column means we can backfill it later via a single UPDATE keyed off donation_date once the policy is set.
Summary
Track financial contributions from organizations across time, separate from the recurring-membership tier system. Donations are one-time or campaign-attributed contributions that don't fit
org_memberships(annual recurring) orevent_sponsorships(event-tied).Requirements
org_donationstable:id,organization_id(FK → organizations.id ON DELETE restrict),amount_cents(bigint),currency(text, ISO 4217, default 'USD'),donation_date(date NOT NULL),campaign(text, nullable — free-form for now),fiscal_year(int, nullable),notes(text),created_at/updated_at/recorded_by_user_id(audit columns)(organization_id, donation_date DESC)for fast per-org history queriesGET/POST/PATCH/DELETE /admin/organizations/:id/donations; list returns rows sorted bydonation_datedesc with per-currency totals as aggregatescanEditOrganizations); consider acanRecordDonationspolicy if we want to lock this down further laterorganizations.donation.create,.update,.delete)Context
Why not extend
org_memberships? Memberships are recurring annual tiers with one active row per org; donations are arbitrarily many transactions across time, often campaign-attributed. Why not extendevent_sponsorships? Donations aren't event-bound. The clean split is three independent tables sharing only theorganization_idFK.Implementation Notes
amount_centsas bigint keeps us currency-precision-safe without a numeric type. Storing currency per-row instead of org-level lets a single org's history span currency changes (e.g., GBP → USD after a UK org's US subsidiary takes over donations).Fiscal year is nullable because US-RSE doesn't have one defined yet — leaving the column means we can backfill it later via a single UPDATE keyed off
donation_dateonce the policy is set.