Release Notes — Jul 6, 2026 — on stage (dev accumulation since the Jul 1 prod release)

What this release is. Everything merged to 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

SurfaceURLLogin to use
Org portal (tenant)stg-rcme.membervu.comAdmin admin@rcme.membervu.com / Admin123!; Treasurer treasurer@demo.membervu.com / Treasurer123!
Member viewstg-rcme.membervu.comMember testmember@rcme.membervu.com / Member123!; Expired expired@demo.membervu.com / Expired123!
Super-admin portalstg-admin.membervu.comSuper Admin superadmin@zeniark.com / SuperAdmin123!
Email inbox (MailHog)stg-webmail.membervu.com
Scope notes — read before testing.

💳 Billing, invoices & exports — #577, #580, #578, #583, #586, #587, #589, #594, #595, #585, #600, #571, #534, #561, #560, #529, #573, #613, #611, #506

#ChangeWhat 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 P1Mark-paid endpoint fixedPOST /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 · concurrencyConcurrency-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 P2Invoice 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 P3Finance "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.
#586Export CSV on admin Invoices list — new export control.On the admin Invoices list, click Export CSV → a CSV downloads with the invoice rows.
#594Invoice 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.
#611Export ↔ 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.
#585Bulk-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.
#587Export 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.
#595Member 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 moneyDecimal 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 P2Statement 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 P1Statement "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 P2Admin 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.
#529Manual-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 P1Late-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).
#571Per-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.
#534Invoice 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.
#506Optional 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

⚠ Preview only. Epic #505 is still open and a payment-model gap is tracked as #607. Test what's present; log gaps against #505/#607 rather than filing new bugs.
#ChangeWhat to verify on stage
#564Admin 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).
#567Public 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.
#563Donation-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).
#566Admin 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.
#568Post-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).
#570Donation notifications — received / approved / rejected emails.Recording and then approving/rejecting a donation sends the corresponding email (check MailHog).
#562Schema + migration — additive FundraisingCampaign + Donation tables.Ops/regression-only: no member-facing change; existing billing/donation flows unaffected.

📣 Communications — scheduled broadcasts — #579, #596, #609

#ChangeWhat to verify on stage
#579 P1Scheduled 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.
#596Schedule-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.
#609Broken 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

#ChangeWhat to verify on stage
#582 / #593 security · high-blastSingle-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 changeForgot-password no longer reveals registrationPOST /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 · securityShared 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 P1Split-host tenant-config cold load fixedX-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 P1Tenant 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 P1Self-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.
#537Admin 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

#ChangeWhat to verify on stage
#597 P1Check-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 P0Member 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).
#574Platform 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.
#608NO_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 P2Member 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.
#507Unified 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.
#508Reconcile 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 P2Event 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

#ChangeWhat to verify on stage
#591Login 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).
#592Initials 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.
#575Password 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

#ChangeWhat to verify on stage
#612 P1Tenant create/update no longer 500s on missing appDomainPOST/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.
#538Frontend 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

  1. Dues Run + invoice numbers (#577, #613, #580) — bulk dues generate for real, distinct numbers, mark-paid works.
  2. Invoice / statement exports (#586, #594, #611, #585, #587, #595, #589) — exports honor filters, cover all pages, decimals guarded.
  3. Money-total refresh (#560, #600, #561, #573) — totals refresh after discount/create; payments search filters; late fees apply.
  4. Scheduled broadcasts (#579, #596) — schedule a send; it transitions and sends, never sticks.
  5. Security (#582/#593, #581) — single-use tenant-bound verification links; forgot-password is generic (by design).
  6. Fundraising preview (#505) — campaign CRUD → public landing → record + approve a donation; log gaps to #505/#607.
  7. Events / check-in (#597, #572, #608, #526, #590) — ENDED-code reject, avatars, no-show audit, form flags, Referred-By import.
  8. Auth polish (#559, #601, #588, #552) — tenant logo on reset/register, host-resolved signup, split-host cold load.
  9. UI polish (#591, #592, #575) + platform (#612).
  10. Skip: live gateway/webhook capture (inert in QA — records only). Use "Record Payment" for manual payments.

🚀 Deploy / migration notes (ops + reviewer gate)

All migrations this window are additive — deploy via prisma migrate deploy only (timestamp order); never reset/reseed on prod:
  1. Fundraising (#562) — additive FundraisingCampaign + Donation tables + indexes/FKs. No change to existing rows.
  2. 20260703000000_add_email_verification_token (#582/#593) — new EmailVerificationToken table (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).
  3. 20260706000000 (#608) — ADD VALUE NO_SHOW / NO_SHOW_UNDO to the CheckInAuditAction enum. Additive.
ℹ️ Status: this is a staging/dev accumulation (MRs !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.