feat(forms): associate submissions with members by email match

submitForm now links member_id whenever submitter_email matches a space
member (including raw anonymous public submissions) -- owner-chosen
product decision, with the attribution tradeoff documented inline so a
future audit does not revert it. New pure escapeLike (ILIKE-safe email
match; '_' in addresses is literal) + pickMemberForEmail (deterministic
earliest-joined on duplicates) + 5 tests (suite 464). Shared
linkSubmissionsByEmail helper for reuse; updated the now-superseded
verified-only comment.
This commit is contained in:
Moheeb Zara
2026-05-16 23:44:03 -07:00
parent a364d152ca
commit ea521de342
3 changed files with 98 additions and 4 deletions
+32
View File
@@ -22,6 +22,8 @@ import {
evaluateRequiredFormStep,
slugify,
deriveFieldKeys,
escapeLike,
pickMemberForEmail,
} from '@/lib/forms-logic'
import { INVITE_ROLES, isInviteRole, canAssignInviteRole } from '@/lib/invite-logic'
import { createInviteSchema, updateInviteSchema } from '@/lib/validations'
@@ -529,3 +531,33 @@ describe('slugify / deriveFieldKeys', () => {
expect(out[0]).toMatchObject({ label: 'Q', type: 'short_text', required: true, key: 'q' })
})
})
describe('escapeLike', () => {
it('escapes ILIKE wildcards so an email matches exactly', () => {
expect(escapeLike('a_b@x.com')).toBe('a\\_b@x.com')
expect(escapeLike('100%@x.com')).toBe('100\\%@x.com')
expect(escapeLike('plain@x.com')).toBe('plain@x.com')
})
it('escapes the backslash first (no double-unescape)', () => {
expect(escapeLike('a\\_b')).toBe('a\\\\\\_b')
})
})
describe('pickMemberForEmail', () => {
it('returns null when no member matches', () => {
expect(pickMemberForEmail([])).toBeNull()
})
it('picks the earliest-joined member deterministically', () => {
expect(pickMemberForEmail([
{ id: 'b', joined_at: '2026-02-01T00:00:00Z' },
{ id: 'a', joined_at: '2026-01-01T00:00:00Z' },
{ id: 'c', joined_at: '2026-03-01T00:00:00Z' },
])).toBe('a')
})
it('treats a null joined_at as earliest (stable)', () => {
expect(pickMemberForEmail([
{ id: 'x', joined_at: '2026-01-01T00:00:00Z' },
{ id: 'y', joined_at: null },
])).toBe('y')
})
})