Release Notes — Jul 6, 2026 — on stage (dev accumulation since the Jul 1 prod release)
dev (→ staging) since the Jul 1 prod promotion (!264) — MRs membervu/membervu-saas!267 through !281 (≈15 MRs, ~45 issues). This is a stage/dev accumulation, not yet a prod promotion — verify on staging (https://stg-rcme.membervu.com; admin portal https://stg-admin.membervu.com). Headlines: a Fundraising module preview (campaigns + donations — partially landed, see the ⚠ note), a large billing / invoice-export wave (CSV exports, bulk actions, Download-PDF, dues-run fix, concurrency-safe invoice numbers), scheduled broadcasts now actually send, and two security hardenings (single-use verification tokens, forgot-password user-enumeration).
🧭 New to MemberVu? Start here
MemberVu is a multi-tenant membership platform with two web apps: the org portal (stg-rcme.membervu.com — admins + members of a club) and the super-admin portal (stg-admin.membervu.com, separate login — Zeniark platform operators).
This window is billing-heavy: many invoice/statement screens gained Export CSV controls, bulk actions, and a working Download PDF, and several money-total refresh bugs were fixed. There is also a brand-new Fundraising area (campaigns + donations) that is only partially wired up — treat it as a preview.
Recommended path: (1) read the env + login box and the ⚠ scope notes; (2) work §💳 Billing & exports top-to-bottom (that's where most of the change is); (3) test §📣 Scheduled broadcasts (#579/#596); (4) test §🔐 Security (#582/#593, #581); (5) sanity-check the §💝 Fundraising preview; (6) finish with events + UI polish.
⚙️ Before you start — environment & logins
| Surface | URL | Login to use |
|---|---|---|
| Org portal (tenant) | stg-rcme.membervu.com | Admin admin@rcme.membervu.com / Admin123!; Treasurer treasurer@demo.membervu.com / Treasurer123! |
| Member view | stg-rcme.membervu.com | Member testmember@rcme.membervu.com / Member123!; Expired expired@demo.membervu.com / Expired123! |
| Super-admin portal | stg-admin.membervu.com | Super Admin superadmin@zeniark.com / SuperAdmin123! |
| Email inbox (MailHog) | stg-webmail.membervu.com | — |
- Exports are a big theme. New Export CSV controls landed on the admin Invoices list, the member "My Statements" page, and the registrant CSV import gained a "Referred By" column. When you export, confirm the file honors any search / status / source filters you had applied and covers all pages, not just page 1.
- Scheduled broadcasts now actually send. The Communications compose screen has a new schedule-time picker (#596), and the background worker that sends scheduled broadcasts was fixed (#579). A broadcast scheduled for a past/near time should transition and send — not sit stuck.
- Login + registration rate limits still apply (a
429= throttled, not a failure). - The payment gateway is still inert in QA (records only, no live capture) — use "Record Payment" for a manual/offline payment.
- ⚠ Fundraising is a preview, partially landed. The backend schema, donation-create, admin campaign CRUD, admin approval queue, public landing + results pages, and notifications (Epic #505, Stories 1–7 & 9) merged, but the epic is still open and a payment-model gap is tracked (#607). Test what is present (create a campaign, view its public landing, record a donation, approve it) and report gaps against #505/#607 rather than filing them as new bugs.
- Not a prod release. These changes are on
dev/staging only; they have not been promoted to prod. Verify onstg-*hosts, not the prod release build. - Dues Run now works. "Create Dues Run" (bulk dues generation) previously returned
HTTP 501(not wired). It is now functional (#577) — and invoice-number generation is concurrency-safe (#613), so a bulk run won't collide on duplicate numbers. - Security behavior changed by design.
POST /auth/forgot-passwordnow returns the same generic response whether or not the email is registered (#581) — do not flag "it didn't tell me the email was unknown" as a bug. Email-verification links are now single-use and tenant-bound (#582/#593) — a reused or cross-tenant link is correctly rejected. - All DB migrations this window are additive (Fundraising tables, an email-verification-token table, a NO_SHOW audit enum value) — see §Deploy.
💳 Billing, invoices & exports — #577, #580, #578, #583, #586, #587, #589, #594, #595, #585, #600, #571, #534, #561, #560, #529, #573, #613, #611, #506
| # | Change | What to verify on stage |
|---|---|---|
| #577 P1 | "Create Dues Run" now works — bulk dues generation was returning HTTP 501 (wired to a stub); it now generates dues invoices for real. | As admin/treasurer, trigger a Dues Run → invoices are generated for the eligible members (no 501). Confirm the invoices appear with correct amounts + numbers. |
| #580 P1 | Mark-paid endpoint fixed — POST /billing/invoices/:id/mark-paid was returning 404; it now resolves. | Mark an invoice as paid (admin) → it succeeds and the invoice flips to a paid status (no 404). |
| #613 money · concurrency | Concurrency-safe invoice numbers — dues invoice-number generation now retries on a duplicate-number collision (the retry was silently dead under the new DB driver). | Generate several invoices at once (e.g. a Dues Run over many members) → every invoice gets a distinct number, none fail with a duplicate/constraint error. |
| #578 P2 | Invoice detail "Download PDF" wired — the button now calls the (already-existing) backend endpoints. | Open an invoice detail → Download PDF produces the invoice PDF (not a dead button / error). |
| #583 P3 | Finance "Invoice Collection Source" NaN fixed — the admin Finance summary no longer shows NaN. | On the admin Finance / reporting summary, the Invoice Collection Source figures render as numbers/currency — no NaN. |
| #586 | Export CSV on admin Invoices list — new export control. | On the admin Invoices list, click Export CSV → a CSV downloads with the invoice rows. |
| #594 | Invoice export honors filters — the export forwards the active search + multi-status/source filters to the backend. | Apply a search term and/or status/source filter on the Invoices list, then Export → the CSV contains only the filtered rows, not everything. |
| #611 | Export ↔ list filter parity — the export and the on-screen list now parse comma-list filters the same way. | With a multi-value status/source filter applied, the row count in the CSV matches the list exactly. |
| #585 | Bulk-select + bulk actions on admin Invoices — select multiple invoices and bulk export / send reminders. | Select several invoices → bulk export and bulk send-reminders act on exactly the selected set. |
| #587 | Export on member "My Statements" — new export/download control on the member statements page. | As a member, open My Statements → an Export/Download control produces a CSV of your statement rows. |
| #595 | Member statements export is full-history — the export paginates all pages, not just the visible one. | For a member with many statements (multiple pages), the exported CSV contains every row across all pages. |
| #589 money | Decimal guard on amounts — statement/invoice amount inputs are guarded against >2 decimal places (no silent rounding). | Enter an amount like 10.999 → it's rejected or corrected to 2 decimals; no silent Math.round surprise in the stored/displayed total. |
| #600 P2 | Statement list refreshes on create — creating a statement/invoice now dispatches the update event so the admin list refreshes. | Create an invoice from the admin list (Create modal) → the list updates immediately without a manual reload. |
| #560 P1 | Statement "Total Due" refresh after discount — the total no longer stays stale after applying a discount. | Apply a discount on a statement → the Total Due updates right away (no stale figure until reload). |
| #561 P2 | Admin payments search honored — the payments search param was ignored by the backend (returned everything). | Search the admin Payments list by a term → results are filtered, not the full list. |
| #529 | Manual-payment approval records the approver — approving a manual payment now writes verifiedBy / verifiedAt. | Approve a manual/offline payment → the payment detail shows who approved it and when (audit fields populated). |
| #573 P1 | Late-fee engine runs from the manual reminders endpoint — the manual "run scheduled reminders" action now also applies late fees. | With overdue invoices present, trigger the manual run-scheduled-reminders action → applicable late fees are applied (not just reminder emails). |
| #571 | Per-invoice Notes field — a Notes field on the invoice create form + detail display (distinct from line-item descriptions). | Create an invoice with a Note → it saves and shows on the invoice detail. |
| #534 | Invoice ID readable in the list — admin action buttons no longer overlap the Invoice ID in the detail header. | Open the Invoices list / invoice detail → the Invoice ID is fully readable, not hidden behind the action buttons. |
| #506 | Optional note on one-time donations — a notes/message field on the existing one-time donation flow. | Make a one-time donation with a message/note → it's captured and visible to the admin. |
💝 Fundraising (preview) — Epic #505 · #562, #563, #564, #566, #567, #568, #570
| # | Change | What to verify on stage |
|---|---|---|
| #564 | Admin campaign CRUD — create / edit / archive fundraising campaigns (tenant-scoped, with status transitions + audit). | As admin, create a campaign, edit it, and archive it → each action works and is tenant-scoped (not visible to other tenants). |
| #567 | Public campaign landing page — a PII-safe public page per campaign (slug + visibility persisted). | For a published/visible campaign, open its public landing URL (no login) → it renders the campaign and exposes no PII. A hidden/unpublished campaign is not publicly reachable. |
| #563 | Donation-create endpoint — records a donation reusing the canonical Invoice/Payment path (money-parity enforced). | Record a donation against a campaign → an invoice/payment is created with matching totals (amount charged = amount recorded). |
| #566 | Admin donation approval queue — approve / reject donations (tenant-scoped, transition + audit). | A submitted donation appears in the admin approval queue → approving/rejecting updates its status and is auditable. |
| #568 | Post-campaign results / transparency page — a public results page after a campaign concludes. | For a concluded campaign, the public results page shows the totals/outcome (PII-safe). |
| #570 | Donation notifications — received / approved / rejected emails. | Recording and then approving/rejecting a donation sends the corresponding email (check MailHog). |
| #562 | Schema + migration — additive FundraisingCampaign + Donation tables. | Ops/regression-only: no member-facing change; existing billing/donation flows unaffected. |
📣 Communications — scheduled broadcasts — #579, #596, #609
| # | Change | What to verify on stage |
|---|---|---|
| #579 P1 | Scheduled broadcast worker fixed — three wedge bugs resolved: a zero-recipient broadcast, a mid-send failure, and an unresolvable-tenant-origin case each used to leave a broadcast stuck in SCHEDULED/SENDING forever. Each now ends terminal (FAILED or sent). | Schedule a broadcast for a near/past time → it transitions and sends (or ends FAILED with a reason) — it never sits stuck. A broadcast with no recipients ends FAILED, not re-picked every tick. |
| #596 | Schedule-time picker in compose — the FE field the worker needs; the admin broadcast create/edit screen can now set a send time. | On the broadcast composer, set a future schedule time and save → it persists and the broadcast is queued for that time. |
| #609 | Broken broadcast edit page retired — the unlinked, broken AdminBroadcastEditPage and its /admin/broadcasts/:id/edit route were removed (inline edit on the broadcasts list is the real UX). | Editing a broadcast happens inline on the broadcasts list and works; the old edit route is gone (no dead link). |
🔐 Auth, session & security — #582, #593, #581, #552, #588, #559, #601, #537
| # | Change | What to verify on stage |
|---|---|---|
| #582 / #593 security · high-blast | Single-use, tenant-bound verification tokens — email-verification links were accepted across tenants (a token-forgery → account-takeover path). Identity now comes from a DB row, tokens are single-use (atomic), and cross-tenant/replayed links are rejected. | Use a verification link once → it works. Use the same link a second time → rejected. A link issued for one tenant does not verify a member in another tenant. |
| #581 security · by-design change | Forgot-password no longer reveals registration — POST /auth/forgot-password returns the same generic response for known and unknown emails (fixes user-enumeration). | Request a password reset for a registered and an unknown email → the response/UX is identical (generic "if it exists, we sent a link"). This is intended — not a bug. |
| #552 two-portal · security | Shared mv_session cookie no longer mis-auths admin — in optionalAuthMiddleware, a valid Authorization: Bearer is preferred over a lingering shared cookie (completes the #539/#540 cross-portal series). | With org + admin portals open in one browser, admin actions on public/optional-auth routes don't get mis-attributed to the org identity (no spurious 403). |
| #588 P1 | Split-host tenant-config cold load fixed — X-Tenant-Host / X-Tenant-Slug added to CORS allowedHeaders so the tenant-config request isn't blocked when SPA + API are on different hosts. | Cold-load a tenant page (e.g. a reset / magic-link deep link) → no "org not found" and no CORS error in the console; the tenant branding/config loads. |
| #559 P1 | Tenant logo on Reset / Magic-Link / Register — these pages showed a bare "OR" fallback instead of the tenant logo. | Open Reset password, Magic-link, and Register for a tenant → the tenant logo renders (not the "OR" placeholder). |
| #601 P1 | Self-service signup uses host-resolved tenant — registration was binding to VITE_DEFAULT_TENANT_ID instead of the tenant from the host. | Register from a tenant subdomain (e.g. stg-rcme…) → the new member lands in that tenant, not the default. |
| #537 | Admin 401 recovery + switched-tenant refresh — admin raw-fetch calls recover from a 401, and switching tenant reactively refreshes (follow-up to the session series). | In the admin portal, an expired-token request recovers instead of failing; switching the active tenant refreshes the view correctly. |
🎟️ Events & check-in — #572, #597, #574, #608, #598, #507, #508, #526, #590, #535
| # | Change | What to verify on stage |
|---|---|---|
| #597 P1 | Check-in rejects codes for ENDED events — a check-in access code for an event that has already ended is now refused (EVENT_ENDED guard). | Try a check-in code for an ended event → it's rejected with an event-ended message, not accepted. |
| #572 P0 | Member avatars render on attendee surfaces — org admin GuestsTab + the member PublicEventPage now show member avatars. | On an event's Guests/Attendees tab and the member-facing event page, member avatars display (no broken images / blank circles). |
| #574 | Platform check-in writes the audit log — mark / undo / QR check-in from the platform portal now dual-write eventCheckInAuditLog. | Do a platform check-in (mark, then undo, and via QR) → each action appears in the event's Audit Trail. |
| #608 | NO_SHOW in the Audit Trail — marking / un-marking a no-show is now recorded (NO_SHOW / NO_SHOW_UNDO audit actions, both portals). | Mark an attendee no-show (and undo) → both actions show in the Audit Trail with correct labels/filters. |
| #598 P2 | Member self-cancel on routed event page — the self-cancel control was missing on the routed PublicEventPage. | As a registered member, open the routed event page → a self-cancel control is present and cancels your registration. |
| #507 | Unified check-in search — the separate Manual + Search inputs are consolidated into one search (name / ticket# / email). | On the check-in surface, a single search box finds an attendee by name, ticket number, or email. |
| #508 | Reconcile gated to post-event — the event Reconcile action is hidden during a live event (FE) and 403-guarded (BE). | During a live event, Reconcile is not available; after the event concludes it appears and works. |
| #526 P2 | Event forms keep approval/public flags — create/edit forms were dropping requireApproval (and create also dropped isPublic). | Create/edit an event with "require approval" (and public) checked → the flags persist after save. |
| #590 / #535 | "Referred By" in registrant CSV import — the import now maps a Referred By column (parity with the response export); the attendee-upload path maps it to its column instead of dropping it. | Import a registrant CSV containing a Referred By column → the value lands on the registrant record (not blank / not a stray column). |
🎨 UI / UX polish — #591, #592, #575
| # | Change | What to verify on stage |
|---|---|---|
| #591 | Login password placeholder — the field no longer shows ••••••••, which made an empty field look pre-filled. | On login, an empty password field looks empty (no fake dots). |
| #592 | Initials avatar contrast — the low-contrast bg-primary-soft / text-primary initials placeholder is now legible. | Where a member has no photo, the initials avatar has readable contrast. |
| #575 | Password toggle alignment — the show/hide icon is centered and the focus ring is even in PasswordInput. | The password show/hide eye icon is centered and tab-focus shows an even ring. |
🛠️ Platform & ops — #612, #538
| # | Change | What to verify on stage |
|---|---|---|
| #612 P1 | Tenant create/update no longer 500s on missing appDomain — POST/PUT /api/platform/tenants now derives <slug>.membervu.com when appDomain is absent/cleared instead of writing null into a NOT-NULL column. A name-only update doesn't clobber a custom domain. | Super-admin: create a tenant without an app domain → it succeeds (domain defaults to <slug>.membervu.com). Edit a tenant's name only → its custom domain is preserved. |
| #538 | Frontend resolves shared session module — local-dev/runtime fix so frontend containers resolve libs/shared (ops/build; no member-facing change). | Ops-only: frontend loads with no module-resolution error. |
Suggested review order
- Dues Run + invoice numbers (#577, #613, #580) — bulk dues generate for real, distinct numbers, mark-paid works.
- Invoice / statement exports (#586, #594, #611, #585, #587, #595, #589) — exports honor filters, cover all pages, decimals guarded.
- Money-total refresh (#560, #600, #561, #573) — totals refresh after discount/create; payments search filters; late fees apply.
- Scheduled broadcasts (#579, #596) — schedule a send; it transitions and sends, never sticks.
- Security (#582/#593, #581) — single-use tenant-bound verification links; forgot-password is generic (by design).
- Fundraising preview (#505) — campaign CRUD → public landing → record + approve a donation; log gaps to #505/#607.
- Events / check-in (#597, #572, #608, #526, #590) — ENDED-code reject, avatars, no-show audit, form flags, Referred-By import.
- Auth polish (#559, #601, #588, #552) — tenant logo on reset/register, host-resolved signup, split-host cold load.
- UI polish (#591, #592, #575) + platform (#612).
- Skip: live gateway/webhook capture (inert in QA — records only). Use "Record Payment" for manual payments.
🚀 Deploy / migration notes (ops + reviewer gate)
prisma migrate deploy only (timestamp order); never reset/reseed on prod:
- Fundraising (#562) — additive
FundraisingCampaign+Donationtables + indexes/FKs. No change to existing rows. 20260703000000_add_email_verification_token(#582/#593) — newEmailVerificationTokentable (tokenHash @unique, tenantId, memberId, expiresAt, usedAt). CREATE TABLE + indexes + FKs only, reversible. A fenced ~48h transitional payload-fallback exists for in-flight pre-migration links (removal tracked #602).20260706000000(#608) —ADD VALUENO_SHOW/NO_SHOW_UNDOto theCheckInAuditActionenum. Additive.
!267–!281 on dev), not a prod promotion — no prod deploy gate for this window yet. When it is promoted, apply the migrations above in order.
⚠ Follow-ups still open: Fundraising epic #505 (+ payment-model gap #607); verification-token fallback removal #602; broadcast worker requires the schedule-time FE field (#596, shipped this window).
Source of truth: the membervu/membervu-pm tracker + MRs membervu/membervu-saas!267–!281 (all merged to dev). This page consolidates the dev/staging accumulation since the Jul 1 prod release (!264, the last review-triggering MR documented). Stage builds from dev. Gateway flows inert in all QA environments. Verified merged on dev as of 2026-07-06.