Phase 2: P1 Critical Fixes

> Execution Document for Claude CLI Agent

> Goal: Fix 3 P1 bugs in reporting, validation, and email compliance.

> Scope: Backend route ordering, frontend form validation, backend email template

> Dependencies: None — all 3 bugs are independent of each other and of Phase 1


Bug 1: BUG-PHASE5-001 — Financial Report Export Fails ("Invoice not found")

Root Cause

Express route ordering issue in mmp/auth-service/src/server.ts. The billing router defines:

Express matches routes in registration order. When the frontend calls /api/billing/admin/invoices/export, Express matches line 743 first with id = "export". The getAdminInvoiceDetailHandler tries to find an invoice with ID "export" in the database, which doesn't exist, returning "Invoice not found".

The frontend correctly calls ${API_BASE_URL}/billing/admin/invoices/export (confirmed at client.ts:2273).

Files to Modify

  1. mmp/auth-service/src/server.ts (lines 743, 751)

Exact Changes

In server.ts, move the /admin/invoices/export route BEFORE the /admin/invoices/:id route. Static routes must always precede parameterized routes in Express.

Find these lines (around 740-752):

billingRouter.get("/admin/invoices/:id", requireManager, getAdminInvoiceDetailHandler);

billingRouter.get("/admin/payments/manual/pending", requireManager, listPendingManualPaymentsHandler);

billingRouter.get("/admin/payments", requireManager, listAdminPaymentsHandler);

billingRouter.get("/admin/payments/export", requireManager, exportAdminPaymentsCSVHandler);

billingRouter.get("/admin/payments/:paymentId", requireManager, getAdminPaymentDetailHandler);

billingRouter.get("/admin/payments/:paymentId/proof", requireManager, getPaymentProofHandler);

billingRouter.get("/admin/credits", requireManager, listCreditsHandler);

billingRouter.get("/admin/finance/summary", requireManager, getFinanceSummaryHandler);

billingRouter.get("/admin/invoices/export", requireManager, exportInvoicesCSVHandler);

billingRouter.get("/admin/donations/report", requireManager, getDonationsReportHandler);

Reorder to put static invoice routes BEFORE the parameterized :id route:

// Static routes MUST come before parameterized routes

billingRouter.get("/admin/invoices/export", requireManager, exportInvoicesCSVHandler);

billingRouter.get("/admin/invoices/:id", requireManager, getAdminInvoiceDetailHandler);

billingRouter.get("/admin/payments/manual/pending", requireManager, listPendingManualPaymentsHandler);

billingRouter.get("/admin/payments", requireManager, listAdminPaymentsHandler);

billingRouter.get("/admin/payments/export", requireManager, exportAdminPaymentsCSVHandler);

billingRouter.get("/admin/payments/:paymentId", requireManager, getAdminPaymentDetailHandler);

billingRouter.get("/admin/payments/:paymentId/proof", requireManager, getPaymentProofHandler);

billingRouter.get("/admin/credits", requireManager, listCreditsHandler);

billingRouter.get("/admin/finance/summary", requireManager, getFinanceSummaryHandler);

billingRouter.get("/admin/donations/report", requireManager, getDonationsReportHandler);

Verification

  1. Restart backend: docker restart membervu-backend
  2. Log in as Admin (admin@rcme.membervu.com)
  3. Navigate to Finance > Dashboard (/admin/finance)
  4. Click "Export Report" button in Quick Actions
  5. Expected: CSV file downloads successfully
  6. Not expected: "Invoice not found" error
  7. Check network tab: GET /api/billing/admin/invoices/export returns 200 with CSV content-type

Bug 2: BUG-PHASE6-001 — Email Format Validation Fails Silently

Root Cause

The

tags in LoginPage.tsx and RegisterPage.tsx do NOT have the noValidate attribute. The email inputs use type="email", which triggers browser-native HTML5 validation. When an invalid email is entered:

  1. Browser-native validation blocks the form submit event from firing
  2. The React onSubmit handler never executes
  3. The React validation code (validateLogin / validateRegistration) never runs
  4. The React error state is never set, so no error message appears
  5. User sees nothing — form appears to do nothing

The React validation functions in mmp/frontend/pwa-app/src/utils/validation.ts ALREADY have correct email format validation:

The fix is simply to add noValidate to the tags so browser validation is bypassed, allowing React validation to handle everything with visible error messages.

Files to Modify

  1. mmp/frontend/pwa-app/src/pages/LoginPage.tsx (line ~481)
  2. mmp/frontend/pwa-app/src/pages/RegisterPage.tsx (line ~181)

Exact Changes

LoginPage.tsx — Find (around line 481):

<form onSubmit={onSubmit} className="grid gap-3">

Replace with:

<form onSubmit={onSubmit} noValidate className="grid gap-3">

RegisterPage.tsx — Find (around line 181):

<form onSubmit={onSubmit}>

Replace with:

<form onSubmit={onSubmit} noValidate>

That's it. The React validation functions already handle email format validation and set the correct error messages. Adding noValidate lets them run instead of being blocked by browser-native validation.

Verification

  1. Frontend hot-reloads automatically
  2. Navigate to https://stg-app.membervu.com/rcme/login
  3. Enter not-an-email in the email field
  4. Enter any password
  5. Click "Sign In"
  6. Expected: Error message "Email is invalid" appears below the email field
  7. Not expected: No visible feedback, form appears stuck
  8. Navigate to https://stg-app.membervu.com/rcme/register
  9. Enter @missing-name.com in the email field
  10. Fill first name, last name
  11. Click "Create account"
  12. Expected: Error message "Email is invalid" appears below the email field
  13. Test that valid emails still work: test@example.com should submit normally

Bug 3: BUG-PHASE4-001 — Broadcast Emails Missing Unsubscribe Link

Root Cause

The broadcast email HTML template in mmp/auth-service/src/handlers/broadcastHandlers.ts (function buildBroadcastHtml at line 565) has a footer that only contains the org name:

<div class="footer">

<p>This email was sent by ${orgName}</p>

</div>

No unsubscribe link is included. The unsubscribe infrastructure already exists:

The buildBroadcastHtml function needs to accept an unsubscribe URL parameter and include it in the footer. The caller (the broadcast send handler) needs to generate per-recipient tokens.

Files to Modify

  1. mmp/auth-service/src/handlers/broadcastHandlers.ts (lines 565-637 for template, plus the send handler)

Exact Changes

Step 1: Update buildBroadcastHtml function signature to accept an optional unsubscribe URL.

Find (line 565):

function buildBroadcastHtml(params: { body: string; orgName: string; logoUrl?: string | null }): string {

const { body, orgName, logoUrl } = params;

Replace with:

function buildBroadcastHtml(params: { body: string; orgName: string; logoUrl?: string | null; unsubscribeUrl?: string }): string {

const { body, orgName, logoUrl, unsubscribeUrl } = params;

Step 2: Update the footer section in the HTML template.

Find (lines 630-632):

    <div class="footer">

<p>This email was sent by ${orgName}</p>

</div>

Replace with:

    <div class="footer">

<p>This email was sent by ${orgName}</p>

${unsubscribeUrl ? <p><a href="${unsubscribeUrl}" style="color: #666; text-decoration: underline;">Unsubscribe</a> from these emails</p> : ""}

</div>

Step 3: Find the handler function that sends broadcast emails (search for where buildBroadcastHtml is called). For each recipient in the send loop, generate an unsubscribe token and construct the unsubscribe URL.

Add import at the top of the file:

import { generateUnsubscribeToken } from "../services/email/preferences";

In the send loop (where individual emails are sent to each recipient), generate the unsubscribe URL:

const unsubscribeToken = generateUnsubscribeToken(recipient.memberId, tenantId, "broadcasts");

const frontendUrl = process.env.FRONTEND_URL || "https://stg-app.membervu.com";

const unsubscribeUrl = ${frontendUrl}/unsubscribe?token=${unsubscribeToken}&memberId=${recipient.memberId}&tenantId=${tenantId}&category=broadcasts;

Pass unsubscribeUrl to buildBroadcastHtml for each recipient's email.

Important: Since the unsubscribe URL is per-recipient, the HTML must be generated per-recipient (not once for all recipients). If the current code generates HTML once and sends to all recipients, it needs to be restructured to generate per-recipient HTML. Check the send loop carefully — if it already iterates per-recipient for sending, just move the buildBroadcastHtml call inside the loop.

Step 4: Also add the List-Unsubscribe email header (best practice for email clients):

headers: {

"List-Unsubscribe": <${unsubscribeUrl}>,

"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",

}

Verification

  1. Restart backend: docker restart membervu-backend
  2. Log in as Comms Manager (comms@demo.membervu.com or admin)
  3. Navigate to Communications > Broadcasts
  4. Create a new broadcast:
  1. Click "Send Now" and confirm
  2. Open MailHog at https://stg-mail.membervu.com
  3. Open any received broadcast email
  4. Expected: Footer contains "Unsubscribe from these emails" link
  5. Expected: Link URL contains /unsubscribe?token=...&memberId=...&tenantId=...&category=broadcasts
  6. Click the unsubscribe link
  7. Expected: Navigates to unsubscribe page (may or may not be fully implemented — the link presence is the acceptance criterion for this bug)

Execution Order

All 3 bugs are completely independent. They can be fixed in any order or in parallel.

Recommended order (fastest path to verification):

  1. BUG-PHASE6-001 (2 one-line changes, instant hot-reload verification)
  2. BUG-PHASE5-001 (route reorder, needs backend restart)
  3. BUG-PHASE4-001 (most complex, needs backend restart)

Docker Commands

# Restart backend after server.ts / handler changes:

docker restart membervu-backend

Check backend logs:

docker logs membervu-backend --tail 50

Open MailHog (for broadcast email verification):

open https://stg-mail.membervu.com

Frontend hot-reloads automatically for .tsx changes