Deduplicate referral invite emails - #124
Conversation
Greptile SummaryThis PR fixes a batch-level duplicate invite bug by hoisting email normalization (
Confidence Score: 4/5Safe to merge; the deduplication fix is correct and the new test covers the target scenario. The core change is straightforward and well-targeted: normalization now happens before every guard that needs it. The only leftover is the
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[POST /api/referrals] --> B{Auth check}
B -- fail --> Z1[401 Unauthorized]
B -- pass --> C{emails array valid & non-empty?}
C -- fail --> Z2[400 Bad Request]
C -- pass --> D{emails.length > 20?}
D -- yes --> Z3[400 Max 20]
D -- no --> E["normalizedEmails = Set(emails.trim().toLowerCase())"]
E --> F{hourlyCount + normalizedEmails.length > 10?}
F -- yes --> Z4[429 Hourly limit]
F -- no --> G{dailyCount + normalizedEmails.length > 50?}
G -- yes --> Z5[429 Daily limit]
G -- no --> H["Query existing invites for normalizedEmails"]
H --> I["newEmails = normalizedEmails − alreadyInvited"]
I --> J{newEmails.length == 0?}
J -- yes --> Z6[400 All already invited]
J -- no --> K[Filter valid email format]
K --> L{validEmails.length == 0?}
L -- yes --> Z7[400 No valid emails]
L -- no --> M[Insert referral rows]
M --> N[Send invite emails]
N --> O[200 Created & sent]
|
| expect(mockSendEmail).toHaveBeenCalledTimes(1); | ||
| expect(mockSendEmail).toHaveBeenCalledWith({ | ||
| to: "friend@test.com", | ||
| subject: "Join ugig.net", | ||
| html: "<p>Join</p>", | ||
| text: "Join", | ||
| }); |
There was a problem hiding this comment.
Test relies on cross-describe mock state
The assertion that mockSendEmail was called with subject, html, and text only works because mockReferralInviteEmail.mockReturnValue(...) is set in the GET describe's beforeEach and vi.clearAllMocks() (used in the POST describe's beforeEach) does not clear mock return values — only call records. If the GET block is ever removed, reordered, or the POST beforeEach is upgraded to vi.resetAllMocks(), this assertion will silently pass with mockSendEmail receiving { to: "friend@test.com" } (no subject/html/text). Add mockReferralInviteEmail.mockReturnValue({ subject: "Join ugig.net", html: "<p>Join</p>", text: "Join" }) inside this test or the POST beforeEach.
Summary
Why
A single /api/referrals POST could include the same recipient more than once, bypassing the existing database-only duplicate-invite filter. That could create duplicate referral rows and send duplicate invite emails in one batch.
Validation