Wave-based onboarding makes the auto-invite + polling path obsolete.
- Removes SlackService.inviteUserToSlack — admins now send invites
through Slack's UI and flip the flag in our admin endpoint.
- Removes the slack_invite_failed admin alert + its detector. The
alert no longer has a meaningful trigger (we don't attempt invites).
- Archives server/utils/checkSlackJoins.js (and its test) under
_archive/ in case the polling pattern is needed again post-pilot.
- Deletes the Nitro plugin that scheduled checkSlackJoins on boot
+ hourly. Nothing in nitro.config / nuxt.config / package.json
registered it elsewhere.
- Drops the slack_invite_failed branch from adminAlerts.test; the
enum slug stays in adminAlertDismissal so historical dismissal
rows continue to validate.
notifyNewMember (vetting-channel notification) and findUserByEmail
(used by the auto-flag helper) are retained.
Best-effort lookup of an activating member's email in the Slack
workspace. On a hit, flips slackInvited:true and stamps slackInvitedAt
without sending a fresh invite. Races against a 3s timeout and swallows
all errors so activation never blocks on Slack.
- Promotes SlackService.findUserByEmail from private to public so the
helper can call it without a wrapper.
- New activity-log action: slack_access_auto_detected (actor = subject).
- Idempotent: short-circuits when slackInvited is already true.
Callers wired in next commit.
Legacy events (tickets.enabled=false) with membersOnly=true were
returning a free guest ticket for unauthenticated callers, causing
GET /api/events/[id]/tickets/available to report available:true. The
UI then rendered the registration form and register.post.js 403'd on
submit. Short-circuit early when membersOnly && !hasMemberAccess so the
availability endpoint's existing null-ticketInfo branch surfaces the
correct "members only" reason.
Pre-launch P0 fixes surfaced by docs/specs/events-functional-test-matrix.md
(Findings 1, 2, 3).
1. Series-pass bypass (Finding 1 / matrix S1 P3): register.post.js now
loads the linked Series when tickets.requiresSeriesTicket is set and
rejects drop-in registration unless series.allowIndividualEventTickets
is true or the user has a valid pass. Data-integrity 500 if the
referenced series is missing.
2. Hidden-event leak (Finding 2 / matrix E11): extract loadPublicEvent
into server/utils/loadEvent.js. All five public event endpoints
([id].get, register, tickets/available, tickets/reserve,
tickets/purchase) now go through the helper, which 404s when
isVisible === false and the requester is not an admin. Admin detection
uses a new non-throwing getOptionalMember() in server/utils/auth.js
(extracted from the pattern already inlined in api/auth/status.get.js).
3. Deadline enforcement + legacy pricing retirement (Finding 3 / matrix
E8): register.post.js and tickets/reserve.post.js delegate gating to
validateTicketPurchase (which already covers deadline, cancelled,
started, members-only, sold-out, and already-registered);
tickets/available.get.js gets an explicit registrationDeadline check.
Legacy pricing.paymentRequired 402 branch removed from register.post.js.
Takes a Member doc + a normalized Helcim transaction and inserts a
Payment row if helcimTransactionId is unseen. Maps helcim status
paid→success, refunded→refunded, failed→failed; skips 'other'.
opts.paymentType overrides the cadence fallback for mid-flight cadence
changes. opts.sendConfirmation triggers a Resend payment-confirmation
email ONLY on new inserts — swallows send failures so email trouble
cannot break the upstream payment flow.
The Resend template lives in server/emails/paymentConfirmation.js. It
is CRA-safe (charity name + 'not an official donation receipt / tax
receipts available later in 2026' disclaimer) so it can be used in
either Task 8 branch without copy changes.
- listHelcimCustomerTransactions(customerCode): GET /card-transactions/
with customerCode filter, sorts newest-first, caps at 50, normalizes
Helcim status (APPROVED/DECLINED) + type (refund) into
paid/refunded/failed/other.
- updateHelcimCustomerDefaultPaymentMethod(customerId, cardToken):
resolves cardToken -> cardId via /customers/{id}/cards, then PATCHes
/customers/{id}/cards/{cardId}/default.
- updateHelcimSubscriptionPaymentMethod(subscriptionId, cardToken):
wraps updateHelcimSubscription with a cardToken payload.
- helcimUpdateCardSchema: Zod schema { cardToken: string } for the
upcoming /api/helcim/update-card route.
- Unit tests for all three helpers (success + error paths).
Extracts hasMemberAccess(member) in tickets.js and uses it across event
registration, ticket purchase, and series purchase flows so guest, suspended,
and cancelled records no longer count as members while pending_payment still
does.
Set up Vitest with server (node) and client (jsdom) test projects.
79 tests across 8 files verify all Phase 0-1 security controls:
escapeHtml sanitization, DOMPurify markdown XSS prevention, CSRF
enforcement, security headers, rate limiting, auth guards, profile
field allowlist, and login anti-enumeration. Updated SECURITY_EVALUATION.md
with remediation status, implementation summary, and automated test
coverage details.