Finalizes the docs section after an adversarial audit (code review + per-page fact-check + coherence critic) and two content requests. - Accuracy: fixed 10 invented cross-link slugs, the admin-vs-board gating claims the verifiers caught (permissions matrix, /settings, invites, onboarding, connect-payments, proposal withdraw, Stripe reconciliation, member fields, door.operate), and 2 issues in the new pages (project area-at-creation, forum thread-delete role). - Coverage: added 5 how-to pages so every module is documented (recurring Stripe dues, tasks & chores, projects, comms & forum, ops knowledge base + secrets vault). 34 pages total. - Screenshots: 12 captioned app screenshots under public/docs-media/, embedded as figures. Each tutorial now has 2-3 step screenshots. - Style: removed all em-dashes/en-dashes/ellipses from the docs (natural ASCII punctuation). - Handoff (docs/HANDOFF-audit-cleanup.md) updated with the current state. typecheck 0 · lint 0 errors · build ok (all 34 /docs pages prerendered)
94 lines
6.0 KiB
Markdown
94 lines
6.0 KiB
Markdown
The payments and dues subsystem records money that comes into your space and maps it to members. This page is the authoritative reference for the payment enums, the advance-only dues rule, admin-configured pay-here links, and the Stripe billing fields. For the day-to-day workflow, see [/payments](/payments) and the member self-view at [/me](/me).
|
|
|
|
## Payment platforms
|
|
|
|
The `payment_platform` enum tags every payment and every pay-here link with its origin. Only these five values exist.
|
|
|
|
| Value | Meaning | Pay-here link | In-app checkout |
|
|
| --- | --- | --- | --- |
|
|
| `paypal` | PayPal | Yes | No |
|
|
| `zeffy` | Zeffy | Yes | No |
|
|
| `venmo` | Venmo | Yes | No |
|
|
| `cash` | Cash logged by a treasurer | No | No |
|
|
| `stripe` | In-app recurring dues integration | No | Yes |
|
|
|
|
The url-based subset (`paypal`, `zeffy`, `venmo`) is the only set allowed for external pay-here links (`DUES_LINK_PLATFORMS`). `cash` and `stripe` cannot be configured as external dues links.
|
|
|
|
## Link status
|
|
|
|
Every row in `payments` carries a `payment_link_status`, which records whether the payment has been matched to a member.
|
|
|
|
| Value | Meaning |
|
|
| --- | --- |
|
|
| `unlinked` | Not yet matched to a member (the default) |
|
|
| `linked` | Matched to a member in this space |
|
|
|
|
Two columns hold this value: `link_status` is canonical, and `status` is kept for backward compatibility. Both default to `unlinked`. A cash payment logged with a member is inserted as `linked`; imported CSV rows and unmatched platform payments start `unlinked`. Linking via [/payments](/payments) sets `link_status` to `linked` and stamps `member_id`. The unlinked count drives the alert badge on the [/dashboard](/dashboard).
|
|
|
|
## Advance-only dues rule
|
|
|
|
When a linked payment updates a member's dues state, the state can only move forward in time. This is enforced by `resolveDuesAdvance`, which compares the member's existing `last_paid_at` against the incoming payment's `transaction_date`:
|
|
|
|
- If the incoming payment is the same as or newer than what is on file, `last_paid_at` advances to the incoming date and the member's `payment_status` is set to `current`.
|
|
- If the incoming payment is older than what is on file, `last_paid_at` is left unchanged and `payment_status` is not touched.
|
|
|
|
A backdated or historical payment therefore never moves a member's dues state backward. Comparison is by instant, not string order, so timezone offsets and `Z`-suffixed timestamps never disagree. The rule runs on both cash logging and payment linking.
|
|
|
|
## Pay-here links (`dues_payment_methods`)
|
|
|
|
Admins configure external pay-here links so members can pay dues off-platform; a treasurer reconciles the payment manually later through the [/payments](/payments) flow (the platform tag pre-types that reconcile). Configuration is link-only, no automated payment record is created.
|
|
|
|
| Column | Type | Notes |
|
|
| --- | --- | --- |
|
|
| `platform` | `payment_platform` | One of `paypal`, `zeffy`, `venmo`; unique per space |
|
|
| `url` | text | Must be an absolute `https://` URL (DB CHECK `url ~* '^https://'`) |
|
|
| `instructions` | text | Optional memo hint, e.g. "put your member name in the note" |
|
|
| `is_active` | boolean | Only active links render to members; defaults to `true` |
|
|
| `sort_order` | integer | Display order, `0`-`999`; defaults to `0` |
|
|
|
|
There is at most one link per `(space, platform)`; saving is an idempotent upsert on that pair. The [/me](/me) self-view shows only active links, in `sort_order`; the [/settings](/settings) admin UI shows every configured method, active and inactive.
|
|
|
|
### URL safety
|
|
|
|
Because `url` is rendered as a member-clickable anchor and admins can write the row directly through RLS, the `https://`-only rule is enforced at three layers: the DB CHECK constraint, the Zod schema (`isSafeDuesUrl`, max 500 chars), and the read boundary that feeds the clickable card. `http://`, `javascript:`, `data:`, and other non-https schemes are rejected everywhere.
|
|
|
|
### Permissions
|
|
|
|
| Operation | Roles |
|
|
| --- | --- |
|
|
| View active links ([/me](/me)) | Any space member |
|
|
| View all links, create, update, delete ([/settings](/settings)) | `admin`, `board` |
|
|
|
|
## Stripe billing fields (`member_billing`)
|
|
|
|
Stripe recurring dues uses per-space own keys (not Connect). Each member with a Stripe subscription has one row in `member_billing`.
|
|
|
|
| Column | Type | Notes |
|
|
| --- | --- | --- |
|
|
| `stripe_customer_id` | text | The member's Stripe customer |
|
|
| `stripe_subscription_id` | text | The active subscription |
|
|
| `subscription_status` | text | Raw Stripe subscription status |
|
|
| `current_period_end` | timestamptz | End of the paid period; drives the grace window |
|
|
|
|
Two service-client paths write this table: the Stripe webhook, and `startDuesCheckout`, which upserts `stripe_customer_id` when it first creates the member's Stripe customer. SELECT is limited to `admin`, `board`, and `treasurer` via RLS. A member reads their own row through a validated service-client action ([/me](/me)). The admin dues view lists every member's billing status.
|
|
|
|
### Status mapping
|
|
|
|
Raw Stripe `subscription.status` maps to a member status. Dues lapse tops out at `late`, it never auto-sets `inactive`; an admin decides inactivation manually.
|
|
|
|
| Stripe status | Member status |
|
|
| --- | --- |
|
|
| `active`, `trialing` | `current` |
|
|
| `past_due` (within grace) | `current` |
|
|
| `past_due` (grace exceeded) | `late` |
|
|
| `canceled`, `unpaid`, `incomplete`, `incomplete_expired` | `late` |
|
|
|
|
The grace window is `current_period_end` plus the configured `grace_days` (default `7`). Stripe settings live in `integrations.config`: `mode` (`test`/`live`), publishable key, the tier-to-price map, `grace_days`, and the secret/webhook refs. The secret key and webhook signing secret are stored in the encrypted [secrets vault](/docs/explanation/security-model), not in `config`. See [/settings](/settings) to configure Stripe.
|
|
|
|
## Related pages
|
|
|
|
- [/payments](/payments), log, import, link, and reconcile payments
|
|
- [/me](/me), member dues self-view, pay-here links, and Stripe checkout
|
|
- [/settings](/settings), configure Stripe and pay-here links
|
|
- [Members: statuses, tiers & fields](/docs/reference/members), member statuses, tiers, and fields
|