/donation — Webhook & Command Spec¶
Not yet implemented. This page is a spec for a planned feature.
Overview¶
Donations come in via Open Collective. The flow is:
Open Collective webhook → bot ingest → write to Grist (Local_donations)
→ post to Discord channel → organizer links to $People → Grist updated
This is primarily a webhook-driven flow, not a slash command flow. The slash command surface is minimal — just enough for organizers to link and manage records that arrive via webhook.
Local_donations Schema (confirmed)¶
| Field | Type | Notes |
|---|---|---|
donation_id |
Text (UUID) | Auto-generated |
donation_type |
Choice | "open collective one time", "open collective recurring", "cashapp", "waiver" |
donation_name |
Text | Name as submitted with donation |
trans_date |
DateTime | Defaults to NOW() |
transaction_ID |
Text | OC transaction ID — write target for ingest |
amount |
Numeric | Defaults to 25 |
effective_date |
Date | Computed on ingest — see Effective Date Logic below |
person |
Reference("People") | Null until linked |
notes |
Text | |
refunded |
Bool | |
duration_days_ |
Numeric | Defaults to 365 — drives expires |
expires |
Formula (Date) | DATEADD(effective_date, days=duration_days_) — read-only |
contribution_ID |
Formula (Text) | Stub returning "" — do not write to this, use transaction_ID |
Open Collective Webhook Ingest¶
Webhook Configuration¶
Open Collective supports outbound webhooks on transaction events. Configure in OC dashboard to POST to bot's ingest endpoint on:
- transaction.created — new donation received
Endpoint: POST /webhooks/opencollective on the bot's HTTP server (separate from Discord gateway — bot runs both).
Note on OC email notifications: OC also sends email notifications to join@ on each transaction. These are redundant once the webhook is reliable — do not build a parallel email-parsing path. The webhook is the source of truth.
Payload¶
Open Collective webhook payload includes at minimum: - Transaction amount - Transaction date - Contributor name (as entered in OC) - Contributor email (if available) - Transaction type (one-time vs recurring) - Transaction ID
Ingest Flow¶
- Validate webhook signature (OC provides a secret for HMAC verification — store in DO secrets)
- Parse payload → extract name, email, amount, date, type
- Attempt member match:
- Query
$Peoplewhereinitial_email_addressorpreferred_email_addressmatches OC email (case-insensitive) - If no email match: fuzzy match
donation_nameagainst$People.all_aliases_no_PII - High-confidence single match → auto-link and note it
- Ambiguous or no match → leave unlinked
- Create new
Local_donationsrow: donation_name— from OC contributor nametrans_date— from OC transaction dateeffective_date— computed per Effective Date Logic below; leave null if member unmatcheddonation_type— map from OC transaction type: one-time → "open collective one time", recurring → "open collective recurring"person— linked$Peoplerow id if matched, null if notrefunded— False- Post notification to configurable donations channel:
💰 New donation received — Open Collective
Name: "Alice Smith"
Amount: $50
Type: One-time
Date: Jun 14 2025
Member match: ✅ @alice / ⚠️ unmatched
/donation link [id] @member — link to a member
/donation info [id] — view details
/donation link [id] @member¶
Signature¶
/donation link [donation_id] @member
Access¶
Representative+
Purpose¶
Links an unmatched (or incorrectly matched) donation to a $People row.
Parameters¶
donation_id— short identifier from the Discord notification (bot maintains a cache of recent donation row ids, or operator can look up via/donation list)@member— Discord mention, resolved to$People
Flow¶
- Fetch
Local_donationsrow by id - If already linked: confirm override —
⚠️ This donation is linked to @carol. Relink to @alice? [Yes] [Cancel] - On confirm: PATCH
Local_donations.person→ new$Peoplerow id - Ephemeral confirmation + update the original Discord notification message to show resolved state
/donation list [optional:unmatched]¶
Signature¶
/donation list
/donation list unmatched
Access¶
Representative+
Purpose¶
Lists recent donations. unmatched filter shows only rows where person is null.
Response¶
Ephemeral embed, most recent first:
💰 Recent donations
#1 Jun 14 Alice Smith $50 one-time ✅ @alice
#2 Jun 12 "bob jones" $25 recurring ⚠️ unmatched
#3 Jun 10 Carol Williams $100 one-time ✅ @carol
/donation link 2 @member to link unmatched entries
/donation info [id]¶
Signature¶
/donation info [id]
Access¶
Representative+
Purpose¶
Returns full detail on a single donation record including linked member's dues status.
Response¶
💰 Donation #2 — detail
Name submitted: "bob jones"
Amount: $25
Type: recurring
Transaction date: Jun 12 2025
Effective date: Jun 12 2025
Refunded: No
Linked member: ⚠️ unmatched
Run /donation link 2 @member to link.
If linked:
Linked member: @bob
Bob's dues expiration: Jun 12 2026
LC dues current: ✅ Yes
Pulls dues_expiration and LC_dues_current from $People — pre-computed, no bot logic needed.
Effective Date Logic¶
effective_date is not simply the transaction date. The bot applies this rule on every OC webhook ingest where the member is matched:
- If the donation is early (before
$People.dues_expiration):effective_date = $People.last_LC_effective_date + 365 daysExample: last donated 9/15/24, new donation arrives 9/1/25 → effective_date = 9/15/25 - If the donation is on or after
dues_expiration:effective_date = trans_dateExample: last donated 8/25/24, new donation arrives 9/1/25 → effective_date = 9/1/25
Purpose: members who renew slightly early don't get penalized — their next due date extends from the previous one, not from the early payment date.
If the member cannot be matched at ingest time, leave effective_date null. The organizer sets it manually after linking via /donation link.
Builder Notes¶
- The bot needs to run an HTTP server alongside the Discord gateway for webhook receipt. Use
aiohttpor FastAPI — both are compatible withdiscord.py's async event loop. - OC webhook secret stored in DO environment variables, same as Grist and Discord tokens.
- Donation IDs in Discord notifications: use the Grist row id (integer) as the short identifier — simple and stable.
/donationcommands should be run from#records-workstream— this channel serves as the operational home for secretary/treasurer commands. Notifications post there by default.effective_datevstrans_date: See Effective Date Logic section — the bot must compute this correctly on ingest. Do not default to trans_date.- Email matching uses
initial_email_addressandpreferred_email_address— both are PII fields. Confirm whether the no-PII Grist user can read these for matching purposes, or whether email matching must be done by a higher-privilege process.
Open Questions¶
- Email field access — can the no-PII bot user read
preferred_email_addressandinitial_email_addressfor donation matching? If not, email matching falls back to name-only fuzzy match, which is less reliable. - OC recurring donation events — does OC fire a webhook on each recurring charge, or only on initial signup? If only on signup, monthly charges won't be auto-logged. Confirm with OC documentation.
- Refund handling — should there be a
/donation refund [id]command to setrefunded=True? Or is that Grist-direct? - Cashapp donations — these don't go through OC. Is there a manual
/donation addentry flow needed, or is Grist-direct sufficient for cashapp?