Skip to content

Commit d350d0d

Browse files
committed
Agent implementation plan
1 parent 76dd26e commit d350d0d

File tree

1 file changed

+112
-0
lines changed

1 file changed

+112
-0
lines changed

agent plans/multiple-emails.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Multi-Email Member Accounts With Verified Primary Email
2+
3+
## Summary
4+
- Add support for multiple email addresses per member, with one selected `primaryEmailAddress`.
5+
- New emails are always added as `unverified`.
6+
- Only verified emails can be used for login and only verified emails can be selected as primary.
7+
- Users cannot remove emails.
8+
- Existing `MemberNumberLinkedToEmail` history remains valid by projecting it as a verified primary email, so current users continue to work without migration of the event store.
9+
10+
## Key Changes
11+
- **Domain events and invariants**
12+
- Add `MemberEmailAdded`, `MemberEmailVerificationRequested`, `MemberEmailVerified`, and `MemberPrimaryEmailChanged`.
13+
- Keep `MemberNumberLinkedToEmail` as a legacy bootstrap event only.
14+
- Enforce these rules in command processing:
15+
- email addresses are globally unique across all members, including unverified ones
16+
- adding an already-linked email is a no-op for the same member and a failure event for a different member
17+
- `MemberPrimaryEmailChanged` is allowed only when the target email belongs to the member and is verified
18+
- verification is idempotent
19+
- Keep self-service commands authorized with the existing self-or-privileged pattern.
20+
21+
- **Read model and types**
22+
- Replace the single-email assumption in the shared member model with:
23+
- `primaryEmailAddress: EmailAddress`
24+
- `emails: ReadonlyArray<MemberEmail>`
25+
- Introduce a `MemberEmail` shape with `emailAddress`, `verifiedAt: O.Option<Date>`, and `addedAt: Date`.
26+
- Add a dedicated `memberEmails` table in the shared read model and store `primaryEmailAddress` on `members` for cheap rendering and stable downstream access.
27+
- Project legacy `MemberNumberLinkedToEmail` into:
28+
- a `members` row if missing
29+
- a verified `memberEmails` row
30+
- `members.primaryEmailAddress`
31+
- Update member merge behavior so grouped member numbers merge all email rows and expose one primary email for the merged member. Primary selection should come from the highest-priority merged record, matching the current precedence pattern used in `mergeMemberCore`.
32+
33+
- **Authentication and verification flow**
34+
- Keep `POST /auth` as the login entrypoint.
35+
- Change login lookup to search verified emails only; unverified emails behave as “no member associated”.
36+
- Continue sending the login email to the matched verified address, not to the member’s primary email unless they are the same.
37+
- Extend the session and JWT user payload to include:
38+
- `memberNumber`
39+
- `emailAddress` as the authenticated email used for that login
40+
- `primaryEmailAddress`
41+
- Add a separate email-verification token flow using a distinct token purpose from magic-link login.
42+
- Add routes:
43+
- `GET /auth/verify-email/landing`
44+
- `GET /auth/verify-email/callback`
45+
- an invalid-verification-link page or reuse the existing invalid-link pattern with verification-specific copy
46+
- Verification tokens should carry `memberNumber`, `emailAddress`, purpose, and expiry.
47+
- Reuse the existing email rate limiter for verification emails.
48+
49+
- **UI and commands**
50+
- Update the `/me` page to show:
51+
- primary email prominently
52+
- a table/list of all email addresses with `Verified` / `Unverified` status
53+
- an add-email form
54+
- a `Send verification email` action for unverified emails
55+
- a `Make primary` action for verified non-primary emails
56+
- Do not render any remove action.
57+
- Mirror the multi-email display anywhere a member profile currently shows only `emailAddress`, but use `primaryEmailAddress` where the screen only has room for one canonical address.
58+
- Add commands/forms under the existing `members` route family:
59+
- `members/add-email`
60+
- `members/send-email-verification`
61+
- `members/change-primary-email`
62+
- Use `primaryEmailAddress` everywhere the app currently means “default displayed member email”, including Gravatar, member tables, and training/admin views.
63+
64+
- **Compatibility and downstream behavior**
65+
- Update `sharedReadModel.members.findByEmail` to read from `memberEmails` and return only members with a verified match.
66+
- Update any code that currently reads `member.emailAddress` to either:
67+
- `member.primaryEmailAddress` for display/default contact behavior, or
68+
- `member.emails` when the feature actually needs the full set
69+
- Update Recurly subscription application so it resolves membership status through verified emails only. It must not activate a member based on an unverified address.
70+
- Preserve the existing admin “link email + member number” workflow by making it project as an immediately verified primary email for bootstrap/import use.
71+
72+
## Public Interfaces and Types
73+
- `User` gains `primaryEmailAddress` while keeping `emailAddress` as the authenticated email for the current session.
74+
- `MemberCoreInfo` and `Member` replace the single `emailAddress` field with `primaryEmailAddress` plus `emails`.
75+
- `SharedReadModel.members.findByEmail(email)` keeps the same signature but now matches verified emails only.
76+
- New form/command inputs:
77+
- `AddMemberEmail { memberNumber, email }`
78+
- `SendMemberEmailVerification { memberNumber, email }`
79+
- `ChangeMemberPrimaryEmail { memberNumber, email }`
80+
81+
## Test Plan
82+
- **Command tests**
83+
- add unverified email to a member
84+
- reject add when email belongs to another member
85+
- no-op when re-adding same email to same member
86+
- reject primary change to unverified email
87+
- allow primary change to verified email
88+
- verification is idempotent
89+
- **Read-model tests**
90+
- legacy `MemberNumberLinkedToEmail` produces a verified primary email
91+
- grouped/rejoined members expose all emails after merge
92+
- only verified emails are returned by `findByEmail`
93+
- primary email projection changes correctly after `MemberPrimaryEmailChanged`
94+
- **Authentication tests**
95+
- login succeeds with any verified email
96+
- login fails for unverified email
97+
- login email is sent to the matched verified address
98+
- verification token expiry and invalid token handling
99+
- session decoding works with the new `User` shape
100+
- **Query/render tests**
101+
- `/me` shows primary email and all addresses with status labels
102+
- unverified rows show verification action only
103+
- verified non-primary rows show `Make primary`
104+
- pages that previously rendered `member.emailAddress` now render `member.primaryEmailAddress`
105+
106+
## Assumptions and Defaults
107+
- Email uniqueness is global across all member emails, not just verified emails.
108+
- Users cannot delete emails in v1.
109+
- A member must always have a primary email once they have at least one email.
110+
- Legacy linked emails are treated as verified and primary automatically.
111+
- `primaryEmailAddress` is the canonical display/default-contact email; `emailAddress` on the session user is the email used to authenticate that session.
112+
- Recurly and any other email-based external syncs should trust verified emails only.

0 commit comments

Comments
 (0)