@@ -35,17 +35,40 @@ const bodySchema = z
3535 path : [ 'verifiedBy' ] ,
3636 } )
3737
38+ type DbConstraintError = Error & {
39+ constraint ?: string
40+ original ?: { constraint ?: string }
41+ parent ?: { constraint ?: string }
42+ }
43+
44+ function throwIdentityConflict (
45+ error : DbConstraintError ,
46+ data : { platform : string ; value : string ; type : MemberIdentityType } ,
47+ ) : never {
48+ const constraint = error . constraint ?? error . original ?. constraint ?? error . parent ?. constraint
49+
50+ if ( constraint === 'uix_memberIdentities_platform_value_type_verified' ) {
51+ throw new ConflictError ( 'Identity already verified on another member' , data )
52+ }
53+
54+ throw error
55+ }
56+
3857export async function createMemberIdentity ( req : Request , res : Response ) : Promise < void > {
3958 const { memberId } = validateOrThrow ( paramsSchema , req . params )
4059 const data = validateOrThrow ( bodySchema , req . body )
4160
4261 const qx = optionsQx ( req )
43-
4462 const member = await findMemberById ( qx , memberId , [ MemberField . ID ] )
4563 if ( ! member ) {
4664 throw new NotFoundError ( 'Member not found' )
4765 }
4866
67+ // The data-sink writes identity values as trimmed lowercase, so normalize here
68+ // to keep idempotency checks reliable against existing rows.
69+ const normalizedValue = data . value . trim ( ) . toLowerCase ( )
70+ const conflictContext = { platform : data . platform , value : normalizedValue , type : data . type }
71+
4972 let result ! : IMemberIdentity
5073 let alreadyExisted = false
5174
@@ -55,23 +78,22 @@ export async function createMemberIdentity(req: Request, res: Response): Promise
5578 captureOldState ( { } )
5679
5780 await qx . tx ( async ( tx ) => {
58- const existing = await findMemberIdentitiesByValue ( tx , memberId , data . value , {
81+ const existing = await findMemberIdentitiesByValue ( tx , memberId , normalizedValue , {
5982 type : data . type ,
6083 } )
61-
6284 const exactMatch = existing . find ( ( i ) => i . platform === data . platform )
6385
64- if ( exactMatch ) {
65- alreadyExisted = true
66- result = exactMatch
67- } else {
68- try {
86+ try {
87+ if ( exactMatch ) {
88+ alreadyExisted = true
89+ result = exactMatch
90+ } else {
6991 result = await insertMemberIdentity (
7092 tx ,
7193 {
7294 memberId,
7395 platform : data . platform ,
74- value : data . value ,
96+ value : normalizedValue ,
7597 type : data . type ,
7698 source : data . source ,
7799 verified : data . verified ,
@@ -80,39 +102,26 @@ export async function createMemberIdentity(req: Request, res: Response): Promise
80102 true ,
81103 true ,
82104 )
83- } catch ( error ) {
84- const constraint =
85- error . constraint ?? error . original ?. constraint ?? error . parent ?. constraint
86-
87- if ( constraint === 'uix_memberIdentities_platform_value_type_verified' ) {
88- throw new ConflictError ( 'Identity already verified on another member' , {
89- platform : data . platform ,
90- value : data . value ,
91- type : data . type ,
92- } )
93- }
94-
95- throw error
96105 }
97- }
98106
99- if ( data . verified && existing . length > 0 ) {
100- await Promise . all (
101- existing . map ( ( i ) =>
102- updateMemberIdentity ( tx , memberId , i . id , {
107+ // A verified identity confirms the same value for this member, so keep same-value
108+ // identities in sync instead of leaving stale unverified duplicates behind.
109+ if ( data . verified && existing . length > 0 ) {
110+ const updatedResults : IMemberIdentity [ ] = [ ]
111+ for ( const identity of existing ) {
112+ const updated = await updateMemberIdentity ( tx , memberId , identity . id , {
103113 verified : true ,
104114 verifiedBy : data . verifiedBy ,
105- } ) ,
106- ) ,
107- )
108-
109- if ( alreadyExisted ) {
110- result = {
111- ...exactMatch ,
112- verified : true ,
113- verifiedBy : data . verifiedBy ,
115+ } )
116+ if ( updated ) updatedResults . push ( updated )
117+ }
118+
119+ if ( alreadyExisted ) {
120+ result = updatedResults . find ( ( r ) => r . id === exactMatch . id ) ?? result
114121 }
115122 }
123+ } catch ( error ) {
124+ throwIdentityConflict ( error as DbConstraintError , conflictContext )
116125 }
117126
118127 await touchMemberUpdatedAt ( tx , memberId )
0 commit comments