fix(forms): link by an email collected as a form field, not only submitter_email

Root cause: association only ever looked at the dedicated submitter_email
(public 'your email' box / auth email). A form that asks for email as an
ordinary field stored it in answers, so submitter_email stayed null and
nothing matched -- 'Re-link' reported 0 ('already linked') and the
submission never appeared under the member. New pure deriveSubmitterEmail
(explicit > email-type field answer > any email-looking answer; trimmed
lowercase) + 5 tests. submitForm now derives + persists submitter_email
from answers; relinkAllSubmissions re-derives per submission from its
form_snapshot and backfills member_id + submitter_email for existing
rows. Suite 469.
This commit is contained in:
Moheeb Zara
2026-05-17 00:31:33 -07:00
parent c2f8368634
commit 9f0ee189a6
3 changed files with 107 additions and 10 deletions
+30
View File
@@ -101,6 +101,36 @@ export function escapeLike(s: string): string {
return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')
}
const FORMS_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
// The email used to associate a submission with a member. The dedicated
// `submitter_email` (the public "your email" box, or the signed-in user's
// address) wins; otherwise fall back to the answer of an email-type field,
// then any answer that is itself a valid email. Without this, a form that
// collects email as an ordinary field never populates submitter_email and
// so never links. Normalized to trimmed lowercase.
export function deriveSubmitterEmail(
fields: Array<{ key: string; type: string }>,
answers: Record<string, unknown> | null | undefined,
explicit?: string | null,
): string | null {
const norm = (v: unknown) => (typeof v === 'string' ? v.trim().toLowerCase() : '')
const e = norm(explicit)
if (e && FORMS_EMAIL_RE.test(e)) return e
const a = answers ?? {}
for (const f of fields) {
if (f.type === 'email') {
const v = norm(a[f.key])
if (FORMS_EMAIL_RE.test(v)) return v
}
}
for (const k of Object.keys(a)) {
const v = norm(a[k])
if (FORMS_EMAIL_RE.test(v)) return v
}
return null
}
// When an email matches more than one member row in a space (e.g. a manually
// added duplicate), associate deterministically with the earliest-joined one
// so the result is stable and reproducible.