From cdc18f6da02545a05e7e757b9c72a3a5bb50adf9 Mon Sep 17 00:00:00 2001 From: Arshadul Monir Date: Fri, 7 Aug 2026 12:51:18 -0400 Subject: [PATCH 1/2] Replaced all usages of Full Name with First Name and Last Name added notblank --- db/README.md | 3 +- .../emails/_components/CsvUploader.tsx | 18 +++++--- js/src/features/emails/api/emailTemplate.ts | 12 +++--- js/src/features/emails/api/parseCSV.ts | 29 +++++++------ js/src/features/emails/dto/emailDto.ts | 11 ++--- .../emails/examples/pairings-test.csv | 18 ++++---- .../features/emails/examples/users-test.csv | 35 ++++++++------- .../patchats/api/member/MemberService.java | 5 +++ .../api/member/db/repos/MemberRepo.java | 3 ++ .../api/member/db/repos/MemberSqlRepo.java | 7 +++ .../api/member/MemberControllerTest.java | 6 +-- .../api/member/MemberServiceTest.java | 4 +- .../member/db/repos/MemberSqlRepoTest.java | 43 +++++++++++++++++++ 13 files changed, 132 insertions(+), 62 deletions(-) create mode 100644 src/test/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepoTest.java diff --git a/db/README.md b/db/README.md index 8858f83..d0cfe01 100644 --- a/db/README.md +++ b/db/README.md @@ -42,9 +42,10 @@ erDiagram timestamp_with_time_zone created_at "{NOT_NULL}" text email UK "{NOT_NULL}" text extra_notes - text full_name "{NOT_NULL}" + text first_name "{NOT_NULL}" uuid id PK "{NOT_NULL}" text industry + text last_name "{NOT_NULL}" text linked_url text match_pref text referral_source diff --git a/js/src/features/emails/_components/CsvUploader.tsx b/js/src/features/emails/_components/CsvUploader.tsx index ce7571a..7f1d6ca 100644 --- a/js/src/features/emails/_components/CsvUploader.tsx +++ b/js/src/features/emails/_components/CsvUploader.tsx @@ -108,7 +108,8 @@ export function UserCsvUpload({ setRows( Array.from(users.values()).map((user) => ( - {user.name} + {user.firstName} + {user.lastName} {user.email} {user.intro} {user.linkedIn} @@ -156,7 +157,8 @@ export function UserCsvUpload({ > - Name + First name + Last name Email Intro Linked In @@ -198,9 +200,11 @@ export function PairingCsvUpload({ setRows( Array.from(pairs.values()).map((pair) => ( - {pair.fullNameA} + {pair.firstNameA} + {pair.lastNameA} {pair.emailA} - {pair.fullNameB} + {pair.firstNameB} + {pair.lastNameB} {pair.emailB} )), @@ -237,9 +241,11 @@ export function PairingCsvUpload({ > - Name #1 + First name #1 + Last name #1 Email #1 - Name #2 + First name #2 + Last name #2 Email #2 diff --git a/js/src/features/emails/api/emailTemplate.ts b/js/src/features/emails/api/emailTemplate.ts index 84d4603..6197d60 100644 --- a/js/src/features/emails/api/emailTemplate.ts +++ b/js/src/features/emails/api/emailTemplate.ts @@ -9,7 +9,7 @@ * Message-level variables are referenced un-prefixed (e.g. ${pair}); per-recipient * variables are referenced positionally as ${per.x} / ${per.x}. * - * Note: Users are refered to as per1.name even if the email is only sent to one + * Note: Users are referred to as per1.firstName even if the email is only sent to one * user. Also, attributes such as 'intro' is reference without the perN prefix in * emails sent to individual users, but would be referenced with the perN prefix * if included in a message sent to multiple users. @@ -52,7 +52,7 @@ export function emailTemplate( export const SAMPLE_TEMPLATE: EmailTemplate = emailTemplate( "Sample", "You're paired!", - `Hi \${per1.name} and \${per2.name}.\n\nYou're paired with \${per1.name} & \${per2.name}.`, + `Hi \${per1.firstName} \${per1.lastName} and \${per2.firstName} \${per2.lastName}.\n\nYou're paired with \${per1.firstName} & \${per2.firstName}.`, "coordinator@patinanetwork.org", ); @@ -65,10 +65,10 @@ export const PAIR_TEMPLATE: EmailTemplate = emailTemplate( `Hey \${per1.firstName} and \${per2.firstName}! \n We've paired you two for PatChats this month! Find some time to have a 30 minute coffee chat or video call with your pairing! Share a screenshot or selfie of you two in the #pat-chats channel on the Discord server! \n -\${per1.name} (\${per1.email}): +\${per1.firstName} \${per1.lastName} (\${per1.email}): \${per1.intro: ${INTRO_MISSING}} \${per1.linkedin:}\n -\${per2.name} (\${per2.email}): +\${per2.firstName} \${per2.lastName} (\${per2.email}): \${per2.intro: ${INTRO_MISSING}} \${per2.linkedin:}\n Let me know if you'd like to update your pairing information or want to be taken off the list.\n @@ -85,8 +85,8 @@ Just a friendly reminder that you were paired for PatChats this month! If you haven't had your 30 minute coffee chat or video call yet, now's a great time to schedule it. Don't forget to share a screenshot or selfie in the #pat-chats channel on the Discord server! -\${per1.name} (\${per1.email}) -\${per2.name} (\${per2.email}) +\${per1.firstName} \${per1.lastName} (\${per1.email}) +\${per2.firstName} \${per2.lastName} (\${per2.email}) Let me know if you'd like to update your pairing information or be taken off the list. diff --git a/js/src/features/emails/api/parseCSV.ts b/js/src/features/emails/api/parseCSV.ts index 05c4c0c..9c10ee0 100644 --- a/js/src/features/emails/api/parseCSV.ts +++ b/js/src/features/emails/api/parseCSV.ts @@ -22,11 +22,12 @@ export const dataToSendRequest = async ( Object.entries(vars).filter(([, value]) => value?.trim()), ); - // Turn a full User record into a recipient oject with their variables. + // Turn a User record into a recipient object with their variables. const toRecipient = (u: User) => ({ email: u.email, variableToValue: withoutEmpty({ - name: u.name, + firstName: u.firstName, + lastName: u.lastName, email: u.email, intro: u.intro, linkedIn: u.linkedIn, @@ -34,14 +35,12 @@ export const dataToSendRequest = async ( preferences: u.preferences, topics: u.topics, anything: u.anything, - firstName: (u.name ?? "").split(" ")[0], - lastName: (u.name ?? "").split(" ").slice(1).join(" "), }), }); let messages; if (pairList.length > 0) { - // One message per pair, addressed to two recipients. The pairing file only gives us names + emails, + // One message per pair, addressed to two recipients. The pairing file only gives us names and emails, // so we look each person up in userMap to add the user with their full set of variables. messages = pairList.map((p) => { const userA = userMap.get(p.emailA); @@ -98,7 +97,8 @@ export async function parseUserFile( // Check User CSV for required headers const headers = results.meta.fields ?? []; const requiredHeaders = [ - "name", + "firstName", + "lastName", "email", "intro", "linkedIn", @@ -119,10 +119,13 @@ export async function parseUserFile( for (const userData of results.data) { const emailValue = userData.email?.trim(); if (!emailValue) { - throw new Error(`Missing email for user: ${userData.name}`); + throw new Error( + `Missing email for user: ${userData.firstName} ${userData.lastName}`, + ); } const user: User = { - name: userData.name, + firstName: userData.firstName, + lastName: userData.lastName, // Trim so trailing whitespace from the CSV doesn't fail backend email validation. email: emailValue, intro: userData.intro, @@ -160,11 +163,13 @@ export async function parsePairingFile(pairFile: File): Promise { const pairings = []; for (const pairData of results.data) { const pair: Pair = { - fullNameA: pairData[0], + firstNameA: pairData[0], + lastNameA: pairData[1], // Trim so emails match the trimmed userMap keys and pass backend validation. - emailA: pairData[1]?.trim(), - fullNameB: pairData[2], - emailB: pairData[3]?.trim(), + emailA: pairData[2]?.trim(), + firstNameB: pairData[3], + lastNameB: pairData[4], + emailB: pairData[5]?.trim(), }; pairings.push(pair); } diff --git a/js/src/features/emails/dto/emailDto.ts b/js/src/features/emails/dto/emailDto.ts index 858d828..703b700 100644 --- a/js/src/features/emails/dto/emailDto.ts +++ b/js/src/features/emails/dto/emailDto.ts @@ -1,5 +1,6 @@ export interface User { - name: string; + firstName: string; + lastName: string; email: string; intro: string; linkedIn: string; @@ -7,14 +8,14 @@ export interface User { preferences: string; topics: string; anything: string; - firstName?: string; - lastName?: string; } export interface Pair { - fullNameA: string; + firstNameA: string; + lastNameA: string; emailA: string; - fullNameB: string; + firstNameB: string; + lastNameB: string; emailB: string; } diff --git a/js/src/features/emails/examples/pairings-test.csv b/js/src/features/emails/examples/pairings-test.csv index 62caeff..1b34c31 100644 --- a/js/src/features/emails/examples/pairings-test.csv +++ b/js/src/features/emails/examples/pairings-test.csv @@ -1,9 +1,9 @@ -Alice Smith,alice@example.com,Bob Jones,bob@example.com -AB,AB@example.com,CD,CD@example.com -EF,EF@example.com,GH,GH@example.com -IJ,IJ@example.com,KL,KL@example.com -MN,MN@example.com,OP,OP@example.com -QR,QR@example.com,ST,ST@example.com -UV,UV@example.com,WX,WX@example.com -Alice Smith, alice@example.com, YZ,YZ@example.com -Bob Jones, bob@example.com, IL,IL@example.com +Alice,Smith,alice@example.com,Bob,Jones,bob@example.com +AB,,AB@example.com,CD,,CD@example.com +EF,,EF@example.com,GH,,GH@example.com +IJ,,IJ@example.com,KL,,KL@example.com +MN,,MN@example.com,OP,,OP@example.com +QR,,QR@example.com,ST,,ST@example.com +UV,,UV@example.com,WX,,WX@example.com +Alice,Smith, alice@example.com,YZ,,YZ@example.com +Bob,Jones, bob@example.com,IL,,IL@example.com diff --git a/js/src/features/emails/examples/users-test.csv b/js/src/features/emails/examples/users-test.csv index c12f22e..c696e78 100644 --- a/js/src/features/emails/examples/users-test.csv +++ b/js/src/features/emails/examples/users-test.csv @@ -1,18 +1,17 @@ -timestamp,name,email,intro,linkedIn,industry,preferences,topics,anything,,, -4/14/2026 21:30:13,Alice Smith, alice@example.com,"Alice intro",https://www.linkedin.com/in/thmd,SWE,"In order (but no dealbreakers): Industry mentors, industry mentees, mentors, mentee",,,,, -4/11/2026 11:03:07,Bob Jones, bob@example.com,,,, -4/11/2026 11:03:07,AB,AB@example.com,,,, -4/11/2026 11:03:07,CD,CD@example.com,,,, -4/11/2026 11:03:07,EF,EF@example.com,,,, -4/11/2026 11:03:07,GH,GH@example.com,,,, -4/11/2026 11:03:07,IJ,IJ@example.com,,,, -4/11/2026 11:03:07,KL,KL@example.com,,,, -4/11/2026 11:03:07,MN,MN@example.com,,,, -4/11/2026 11:03:07,OP,OP@example.com,,,, -4/11/2026 11:03:07,QR,QR@example.com,,,, -4/11/2026 11:03:07,ST,ST@example.com,,,, -4/11/2026 11:03:07,UV,UV@example.com,,,, -4/11/2026 11:03:07,WX,WX@example.com,,,, -4/11/2026 11:03:07,YZ,YZ@example.com,,,, -4/11/2026 11:03:07,IL,IL@example.com,,,, - +timestamp,firstName,lastName,email,intro,linkedIn,industry,preferences,topics,anything,,, +4/14/2026 21:30:13,Alice,Smith, alice@example.com,"Alice intro",https://www.linkedin.com/in/thmd,SWE,"In order (but no dealbreakers): Industry mentors, industry mentees, mentors, mentee",,,,, +4/11/2026 11:03:07,Bob,Jones, bob@example.com,,,, +4/11/2026 11:03:07,AB,,AB@example.com,,,, +4/11/2026 11:03:07,CD,,CD@example.com,,,, +4/11/2026 11:03:07,EF,,EF@example.com,,,, +4/11/2026 11:03:07,GH,,GH@example.com,,,, +4/11/2026 11:03:07,IJ,,IJ@example.com,,,, +4/11/2026 11:03:07,KL,,KL@example.com,,,, +4/11/2026 11:03:07,MN,,MN@example.com,,,, +4/11/2026 11:03:07,OP,,OP@example.com,,,, +4/11/2026 11:03:07,QR,,QR@example.com,,,, +4/11/2026 11:03:07,ST,,ST@example.com,,,, +4/11/2026 11:03:07,UV,,UV@example.com,,,, +4/11/2026 11:03:07,WX,,WX@example.com,,,, +4/11/2026 11:03:07,YZ,,YZ@example.com,,,, +4/11/2026 11:03:07,IL,,IL@example.com,,,, diff --git a/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java b/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java index fdc4325..169b9ad 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java @@ -1,5 +1,6 @@ package org.patinanetwork.patchats.api.member; +import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.patinanetwork.patchats.api.member.db.models.Member; @@ -40,6 +41,10 @@ public MemberDto createMember(CreateMemberRequest request) { return MemberDto.from(createdMember); } + public List getMembers() { + return memberRepo.getMembers().stream().map(MemberDto::from).toList(); + } + public MemberDto updateMember(UpdateMemberRequest request, UUID id) { Member member = memberRepo.getMemberById(id).orElseThrow(() -> new MemberNotFoundException(id)); diff --git a/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberRepo.java b/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberRepo.java index 304d0d8..8d280e6 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberRepo.java @@ -1,5 +1,6 @@ package org.patinanetwork.patchats.api.member.db.repos; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.patinanetwork.patchats.api.member.db.models.Member; @@ -21,6 +22,8 @@ public interface MemberRepo { */ Member createMember(Member member); + List getMembers(); + /** * @note - The provided object's methods will be overridden with any returned data from the database. * @param member - overridden fields: diff --git a/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepo.java index e4c312a..ccc65d4 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepo.java @@ -3,6 +3,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.time.OffsetDateTime; +import java.util.List; import java.util.Optional; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -90,6 +91,12 @@ public Member createMember(Member member) { .single(); } + @Override + public List getMembers() { + String sql = "SELECT * FROM members ORDER BY created_at DESC, id"; + return jdbc.sql(sql).query((rs, rowNum) -> parseResultSetToMember(rs)).list(); + } + @Override public Optional updateMember(Member member) { throw new UnsupportedOperationException("Not implemented yet"); diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java index 722421f..8532e87 100644 --- a/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java @@ -32,7 +32,7 @@ class MemberControllerTest { private MemberService memberService; @Test - void createMember_ReturnsOkAndMemberDto() throws Exception { + void createMemberReturnsOkAndMemberDto() throws Exception { final CreateMemberRequest request = new CreateMemberRequest( "John", "Doe", @@ -85,7 +85,7 @@ void createMember_ReturnsOkAndMemberDto() throws Exception { } @Test - void createMember_returnsBadRequestWhenFirstNameIsBlank() throws Exception { + void createMemberReturnsBadRequestWhenFirstNameIsBlank() throws Exception { mockMvc.perform( post("/api/members") .contentType(MediaType.APPLICATION_JSON) @@ -96,7 +96,7 @@ void createMember_returnsBadRequestWhenFirstNameIsBlank() throws Exception { } @Test - void createMember_returnsConflictOnDuplicateEmail() throws Exception { + void createMemberReturnsConflictOnDuplicateEmail() throws Exception { when(memberService.createMember(any())).thenThrow(new MemberDuplicateException("john.doe@example.com")); mockMvc.perform( post("/api/members") diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java index 251a49c..8bf3318 100644 --- a/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java @@ -27,7 +27,7 @@ class MemberServiceTest { private final MemberService memberService = new MemberService(memberRepo); @Test - void createMember_successWithAllFieldsFilled() { + void createMemberSuccessWithAllFieldsFilled() { final CreateMemberRequest request = new CreateMemberRequest( "John", "Doe", @@ -77,7 +77,7 @@ void createMember_successWithAllFieldsFilled() { } @Test - void createMember_throwsExceptionWhenEmailAlreadyExists() { + void createMemberThrowsExceptionWhenEmailAlreadyExists() { final CreateMemberRequest request = new CreateMemberRequest( "John", "Doe", diff --git a/src/test/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepoTest.java b/src/test/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepoTest.java new file mode 100644 index 0000000..f40a80e --- /dev/null +++ b/src/test/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepoTest.java @@ -0,0 +1,43 @@ +package org.patinanetwork.patchats.api.member.db.repos; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.patinanetwork.patchats.api.member.db.models.Member; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.JdbcClient; + +class MemberSqlRepoTest { + + private static final String GET_MEMBERS_SQL = "SELECT * FROM members ORDER BY created_at DESC, id"; + + @Test + void getMembersReturnsRowsFromDatabase() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final Member member = Member.builder() + .id(UUID.randomUUID()) + .firstName("Alex") + .lastName("Morgan") + .email("alex@example.com") + .active(true) + .build(); + + when(jdbc.sql(GET_MEMBERS_SQL)).thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.list()).thenReturn(List.of(member)); + + final List result = new MemberSqlRepo(jdbc).getMembers(); + + assertEquals(List.of(member), result); + verify(jdbc).sql(GET_MEMBERS_SQL); + verify(query).list(); + } +} From 7cc6603b49dcd3f880c3463d6ef19ae12fb81116 Mon Sep 17 00:00:00 2001 From: Arshadul Monir Date: Fri, 7 Aug 2026 14:59:38 -0400 Subject: [PATCH 2/2] Ordered db README columns --- db/README.md | 54 ++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/db/README.md b/db/README.md index d0cfe01..3ce1804 100644 --- a/db/README.md +++ b/db/README.md @@ -3,55 +3,55 @@ ```mermaid erDiagram flyway_schema_history { - integer checksum - character_varying description "{NOT_NULL}" - integer execution_time "{NOT_NULL}" - character_varying installed_by "{NOT_NULL}" - timestamp_without_time_zone installed_on "{NOT_NULL}" integer installed_rank PK "{NOT_NULL}" + character_varying version + character_varying description "{NOT_NULL}" + character_varying type "{NOT_NULL}" character_varying script "{NOT_NULL}" + integer checksum + character_varying installed_by "{NOT_NULL}" + timestamp installed_on "{NOT_NULL}" + integer execution_time "{NOT_NULL}" boolean success "{NOT_NULL}" - character_varying type "{NOT_NULL}" - character_varying version } match_cycles { - uuid id PK "{NOT_NULL}" + integer id PK "{NOT_NULL}" text period - timestamp_with_time_zone run_at "{NOT_NULL}" - integer total_matched + timestamptz run_at "{NOT_NULL}" integer total_members - ARRAY unmatched_id + integer total_matched + integer[] unmatched_id } matches { - timestamp_with_time_zone created_at "{NOT_NULL}" - uuid cycle_id FK "{NOT_NULL}" - integer feedback_a - integer feedback_b uuid id PK "{NOT_NULL}" - real match_score uuid member_a_id FK "{NOT_NULL}" uuid member_b_id FK "{NOT_NULL}" + integer cycle_id FK "{NOT_NULL}" + real match_score text status + text feedback_a + text feedback_b + timestamptz created_at "{NOT_NULL}" } members { - boolean active - text bio - timestamp_with_time_zone created_at "{NOT_NULL}" - text email UK "{NOT_NULL}" - text extra_notes - text first_name "{NOT_NULL}" uuid id PK "{NOT_NULL}" - text industry + text first_name "{NOT_NULL}" text last_name "{NOT_NULL}" - text linked_url - text match_pref + text email UK "{NOT_NULL}" + text linked_in_url + text introduction text referral_source - text role + boolean active + text match_pref + text industry_pref + text role_pref text topics - timestamp_with_time_zone updated_at "{NOT_NULL}" + text extra_notes + timestamptz created_at "{NOT_NULL}" + timestamptz updated_at "{NOT_NULL}" } matches }o--|| match_cycles : "cycle_id"