Add notification preferences and alternate dues payment options

Notification preferences (migration 048):
- Per-member opt-out of muteable categories (bookings, classes, forms,
  admin alerts); billing is always-on. Opt-out model, default on.
- Dispatcher reads prefs and marks muted outbox rows 'skipped' instead
  of sending; pure isMuted logic, fail-open on a prefs lookup error.
- notification_preferences table (default-deny RLS); /me toggles and
  self-view via validated service-client actions.

Alternate dues payment methods (migration 049):
- Admin sets external pay-here URLs per platform (PayPal/Zeffy/Venmo)
  in /settings; members see active ones on /me and pay off-platform;
  the treasurer reconciles manually (platform pre-tagged). https-only.
- dues_payment_methods table (SELECT space member, write admin/board).

/me visual pass:
- Restore the chip/preset profile editor (skills/interests/willing-to/
  affiliations); promote ChipInput and presets to shared modules.
- Gate the Stripe pay-dues button on whether the space has Stripe
  configured; show external links or a contact-admin note otherwise.
This commit is contained in:
Moheeb Zara
2026-05-19 20:26:32 -07:00
parent addedfdb0f
commit ab8c22ae1d
29 changed files with 1281 additions and 124 deletions
+30
View File
@@ -1,5 +1,7 @@
import { z } from 'zod'
import { INVITE_ROLES } from './invite-logic'
import { MUTEABLE_CATEGORIES } from './notifications-prefs-logic'
import { DUES_LINK_PLATFORMS, isSafeDuesUrl } from './dues-payments-logic'
/**
* Accepts every date-ish string the app produces and normalizes to a
@@ -1021,6 +1023,34 @@ export const stripeSettingsSchema = z.object({
prices: z.record(z.string().trim().max(255)).optional().default({}),
})
// ─── Notification preferences ────────────────────────────────────────────────
// Only muteable categories are settable; billing is membership-critical and
// never user-controllable, so it is excluded from the enum (a request to set
// it is rejected at the boundary).
export const notificationPreferenceSchema = z.object({
category: z.enum(MUTEABLE_CATEGORIES as unknown as [string, ...string[]]),
enabled: z.boolean(),
})
// ─── Dues payment methods (admin-configured external links) ──────────────────
// One external pay-here URL per platform. url must be absolute https (no
// http downgrade, no javascript:/data: scheme) since it is rendered as a
// member-clickable anchor. instructions is an optional memo hint (e.g. "put
// your member name in the note") to help the treasurer reconcile later.
export const duesPaymentMethodSchema = z.object({
platform: z.enum(DUES_LINK_PLATFORMS as unknown as [string, ...string[]]),
url: z
.string()
.trim()
.max(500)
.refine(isSafeDuesUrl, 'Enter an absolute https:// URL'),
instructions: z.string().trim().max(300).optional().nullable(),
is_active: z.boolean().optional().default(true),
sort_order: z.number().int().min(0).max(999).optional().default(0),
})
// Helper type exports
export type CreateTaskInput = z.infer<typeof createTaskSchema>
export type CreateProjectInput = z.infer<typeof createProjectSchema>