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:
- Line 743:
billingRouter.get("/admin/invoices/:id", requireManager, getAdminInvoiceDetailHandler); - Line 751:
billingRouter.get("/admin/invoices/export", requireManager, exportInvoicesCSVHandler);
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
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
- Restart backend:
docker restart membervu-backend - Log in as Admin (
admin@rcme.membervu.com) - Navigate to Finance > Dashboard (
/admin/finance) - Click "Export Report" button in Quick Actions
- Expected: CSV file downloads successfully
- Not expected: "Invoice not found" error
- Check network tab: GET
/api/billing/admin/invoices/exportreturns 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:
- Browser-native validation blocks the form submit event from firing
- The React
onSubmithandler never executes - The React validation code (
validateLogin/validateRegistration) never runs - The React error state is never set, so no error message appears
- 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:
validateLogin: line 25 —else if (!isEmail(data.email)) errors.email = "Email is invalid";validateRegistration: line 15 —else if (!isEmail(data.email)) errors.email = "Email is invalid";
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
mmp/frontend/pwa-app/src/pages/LoginPage.tsx(line ~481)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
- Frontend hot-reloads automatically
- Navigate to
https://stg-app.membervu.com/rcme/login - Enter
not-an-emailin the email field - Enter any password
- Click "Sign In"
- Expected: Error message "Email is invalid" appears below the email field
- Not expected: No visible feedback, form appears stuck
- Navigate to
https://stg-app.membervu.com/rcme/register - Enter
@missing-name.comin the email field - Fill first name, last name
- Click "Create account"
- Expected: Error message "Email is invalid" appears below the email field
- Test that valid emails still work:
test@example.comshould 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:
- Token generation:
mmp/auth-service/src/services/email/preferences.ts—generateUnsubscribeToken(memberId, tenantId, category) - Backend endpoints:
mmp/auth-service/src/services/email/unsubscribeHandlers.ts - Frontend pages: unsubscribe/resubscribe functions in
publicClient.ts
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
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
- Restart backend:
docker restart membervu-backend - Log in as Comms Manager (
comms@demo.membervu.comor admin) - Navigate to Communications > Broadcasts
- Create a new broadcast:
- Subject: "Test Unsubscribe Link"
- Audience: "All Active Members"
- Body: "This is a test broadcast."
- Click "Send Now" and confirm
- Open MailHog at
https://stg-mail.membervu.com - Open any received broadcast email
- Expected: Footer contains "Unsubscribe from these emails" link
- Expected: Link URL contains
/unsubscribe?token=...&memberId=...&tenantId=...&category=broadcasts - Click the unsubscribe link
- 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):
- BUG-PHASE6-001 (2 one-line changes, instant hot-reload verification)
- BUG-PHASE5-001 (route reorder, needs backend restart)
- 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