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.
37 lines
1.5 KiB
TypeScript
37 lines
1.5 KiB
TypeScript
// Pure logic for admin-configured alternate dues payment methods. These are
|
|
// external pay-here links (PayPal, Zeffy, Venmo) a member clicks to pay dues
|
|
// off-platform; a treasurer reconciles the payment manually later through the
|
|
// existing payments flow, which is why each method carries a payment_platform
|
|
// tag. No I/O here: the platform set + labels are shared by the admin settings
|
|
// UI and the member /me UI; URL validation is exercised by the Zod schema.
|
|
|
|
// The url-based subset of the payment_platform enum. 'cash' has no link and
|
|
// 'stripe' is the in-app integration, so neither is an external dues link.
|
|
export const DUES_LINK_PLATFORMS = ['paypal', 'zeffy', 'venmo'] as const
|
|
|
|
export type DuesLinkPlatform = (typeof DUES_LINK_PLATFORMS)[number]
|
|
|
|
export const DUES_PLATFORM_LABEL: Record<DuesLinkPlatform, string> = {
|
|
paypal: 'PayPal',
|
|
zeffy: 'Zeffy',
|
|
venmo: 'Venmo',
|
|
}
|
|
|
|
export function isDuesLinkPlatform(p: string): p is DuesLinkPlatform {
|
|
return (DUES_LINK_PLATFORMS as readonly string[]).includes(p)
|
|
}
|
|
|
|
// A payment link is only safe to render as a member-clickable anchor if it is
|
|
// an absolute https URL. http (plaintext) and any non-web scheme
|
|
// (javascript:, data:, mailto:, etc.) are rejected so an admin-entered value
|
|
// cannot become an XSS or a downgrade vector when shown to members.
|
|
export function isSafeDuesUrl(url: string): boolean {
|
|
let u: URL
|
|
try {
|
|
u = new URL(url)
|
|
} catch {
|
|
return false
|
|
}
|
|
return u.protocol === 'https:'
|
|
}
|