ops: document the member provisioning boundary (org-backed, credit-pack, manual, hidden-email) and the daily paid-but-unprovisioned watchdog
This commit is contained in:
1 parent
0f40dcc519
commit
5a4492e725
2 files changed
+76
No files matched your search
@@ -0,0 +1,75 @@
|
||||
# Member provisioning boundary — who auto-provisions from Open Collective
|
||||
|
||||
Which Open Collective contributors become portal members automatically, and
|
||||
which are deliberately excluded. The portal defines the member set in
|
||||
`fetch_members()` (member-portal repo, `app/main.py`), acts on it in the
|
||||
webhook and the nightly `reconcile_memberships()` sweep, and exposes two admin
|
||||
overrides: `POST /admin/org-link` and `POST /admin/provision`. A daily
|
||||
watchdog (below) is the net for anything that slips past both.
|
||||
|
||||
## Auto-provisioned
|
||||
|
||||
**Individual OC accounts holding BACKER/ADMIN on the Membership tier (or no
|
||||
tier — pre-tier contributors are grandfathered) whose email is visible to the
|
||||
admin token.** The webhook provisions on first payment; the nightly sweep
|
||||
(03:00 scheduler task) provisions anything missed within 24h and deactivates
|
||||
lapsed members. `fetch_members()` matches the tier by NAME
|
||||
(`"membership"`, case-insensitive): renaming the tier on Open Collective
|
||||
silently empties the member set — the sweep's fail-safe skips reconciliation
|
||||
rather than deactivating everyone, but provisioning also stops.
|
||||
|
||||
## Deliberately excluded
|
||||
|
||||
1. **Org-backed memberships.** An ORGANIZATION account never exposes an email
|
||||
(the `... on Individual { email }` fragment matches nothing, and OC returns
|
||||
org emails as null even to collective admins), so reconcile cannot map one
|
||||
and does not try. Org-backed memberships become members only when LINKED:
|
||||
`POST /admin/org-link {"org_slug", "email", "name"}` runs the full
|
||||
provisioning pipeline and stores `members.org_slug`; the sweep keeps linked
|
||||
accounts active while the org holds BACKER/ADMIN on the Membership tier and
|
||||
deactivates them when the org's dues lapse (fail-safe: if the org fetch
|
||||
itself errors, linked accounts stay active). An unlinked org's first
|
||||
payment emails info@ with the exact linking command.
|
||||
|
||||
2. **Non-membership tiers (Credit pack).** A one-time purchase grants BACKER
|
||||
role but is NOT a membership — `fetch_members()` filters it out. This is
|
||||
why a contributor can pay, appear in the public backer list, and still have
|
||||
no portal member row: it is a credit-pack purchase, not a lapsed
|
||||
provisioning. The money is not lost: the webhook's held-credit-pack flow
|
||||
records a hold, notifies the buyer, and the operator grants, refunds or
|
||||
transfers it via `/admin/credit-packs-held`; a hold granted to a named
|
||||
member auto-applies if that person later becomes a member.
|
||||
|
||||
3. **Manual members.** The `MANUAL_MEMBERS` env allowlist (founder/free tier,
|
||||
the co-op's own accounts) holds members with no OC membership BY DESIGN:
|
||||
never auto-provisioned from OC, and exempt from lapsed-membership
|
||||
deactivation. Check for orphaned LiteLLM `member:*` teams after any
|
||||
`MANUAL_MEMBERS` change.
|
||||
|
||||
4. **Hidden-email / anonymous contributors.** An individual whose email is
|
||||
null even in the ADMIN view cannot be mapped by reconcile. They are not
|
||||
silently dropped — the daily watchdog reports them with their public dues,
|
||||
and provisioning is manual: `POST /admin/provision {"email", "name"}` on
|
||||
the operator's instruction. Incognito accounts publish an
|
||||
`incognito-xxxxxxxx` slug, but the ADMIN member query usually still reveals
|
||||
their email — always check the admin view before treating one as
|
||||
unmappable (that is what resolved the mmoya case).
|
||||
|
||||
## The watchdog: `oc_unmapped_check.py`
|
||||
|
||||
Daily no-agent cron on the agent host (07:20, job `d0fd1126cc80`), running
|
||||
`~/.hermes/scripts/oc_unmapped_check.py` — the same silent-until-findings
|
||||
pattern as the spend-consistency guard. It reads the collective's backer list
|
||||
through the portal env's admin token and the portal rows read-only via
|
||||
`cloudron exec`, maps them by email, slug or org_slug, and prints ONLY
|
||||
member-ish backers (the exact set `fetch_members()` treats as members) with no
|
||||
portal row at all: slug, display name, whether the admin view sees an email,
|
||||
dues (amount/frequency/status from the public order list), days as a backer,
|
||||
and the matching action (org-link vs /admin/provision vs investigate-the-sweep).
|
||||
Backer nodes with no account object (deleted/incognito-hidden accounts) are
|
||||
attributed via same-day public orders and stay silent when every same-day
|
||||
payer is already mapped. Excluded from findings: credit-pack-only backers
|
||||
(the held-credits flow owns them) and backers matching an inactive portal row
|
||||
(the sweep re-provisions/reactivates those within 24h). Empty output = clean;
|
||||
any fetch failure prints FAILED — a check that cannot look never reads as
|
||||
clean.
|
||||
Reference in new issue
Block a user