@@ -2467,6 +2467,20 @@ All errors return JSON with an \`error\` field and optional \`code\`:
24672467 }
24682468
24692469 try {
2470+ // Deduplicate assets by (code, issuer) before insert
2471+ const seen = new Set < string > ( ) ;
2472+ const duplicates : string [ ] = [ ] ;
2473+ for ( const asset of parsed . data . assets ) {
2474+ const key = `${ asset . code } :${ asset . issuer ?? "" } ` ;
2475+ if ( seen . has ( key ) ) {
2476+ duplicates . push ( `${ asset . code } ${ asset . issuer ? ` (issuer ${ asset . issuer } )` : "" } ` ) ;
2477+ }
2478+ seen . add ( key ) ;
2479+ }
2480+ if ( duplicates . length > 0 ) {
2481+ return sendError ( res , 422 , `Duplicate assets: ${ duplicates . join ( ", " ) } ` ) ;
2482+ }
2483+
24702484 await prisma . $transaction ( [
24712485 prisma . acceptedAsset . deleteMany ( { where : { profileId : profile . id } } ) ,
24722486 prisma . acceptedAsset . createMany ( {
@@ -2919,20 +2933,23 @@ All errors return JSON with an \`error\` field and optional \`code\`:
29192933 if ( ! profile ) return ;
29202934
29212935 try {
2922- const result = await prisma . $transaction ( async ( tx ) => {
2923- const existingCount = await tx . webhook . count ( {
2924- where : { profileId : profile . id } ,
2925- } ) ;
2926- if ( existingCount >= 10 ) {
2927- throw new Error ( "MAX_WEBHOOKS_EXCEEDED" ) ;
2928- }
2936+ const result = await prisma . $transaction (
2937+ async ( tx ) => {
2938+ const existingCount = await tx . webhook . count ( {
2939+ where : { profileId : profile . id } ,
2940+ } ) ;
2941+ if ( existingCount >= 10 ) {
2942+ throw new Error ( "MAX_WEBHOOKS_EXCEEDED" ) ;
2943+ }
29292944
2930- const secret = randomBytes ( 32 ) . toString ( "hex" ) ;
2931- const webhook = await tx . webhook . create ( {
2932- data : { url : parsed . data . url , secretHash : secret , profileId : profile . id } ,
2933- } ) ;
2934- return { webhook, secret } ;
2935- } ) ;
2945+ const secret = randomBytes ( 32 ) . toString ( "hex" ) ;
2946+ const webhook = await tx . webhook . create ( {
2947+ data : { url : parsed . data . url , secretHash : secret , profileId : profile . id } ,
2948+ } ) ;
2949+ return { webhook, secret } ;
2950+ } ,
2951+ { isolationLevel : "Serializable" } ,
2952+ ) ;
29362953
29372954 return res . status ( 201 ) . json ( { id : result . webhook . id , url : result . webhook . url , secret : result . secret } ) ;
29382955 } catch ( err ) {
@@ -3999,15 +4016,26 @@ All errors return JSON with an \`error\` field and optional \`code\`:
39994016 }
40004017
40014018 try {
4002- const result = await prisma . webhookDelivery . updateMany ( {
4019+ const failed = await prisma . webhookDelivery . findMany ( {
40034020 where : { status : "failed" } ,
4021+ select : { id : true } ,
4022+ } ) ;
4023+
4024+ const result = await prisma . webhookDelivery . updateMany ( {
4025+ where : { id : { in : failed . map ( ( d ) => d . id ) } } ,
40044026 data : {
40054027 status : "pending" ,
40064028 attemptCount : 0 ,
40074029 nextRetryAt : new Date ( ) ,
40084030 } ,
40094031 } ) ;
40104032
4033+ for ( const delivery of failed ) {
4034+ enqueueWebhookDelivery ( delivery . id ) . catch ( ( err ) => {
4035+ req . log . warn ( { deliveryId : delivery . id , err } , "Failed to enqueue requeued webhook" ) ;
4036+ } ) ;
4037+ }
4038+
40114039 req . log . info ( { count : result . count } , "requeued failed webhooks" ) ;
40124040 return res . json ( { count : result . count } ) ;
40134041 } catch ( e : unknown ) {
@@ -4104,26 +4132,29 @@ All errors return JSON with an \`error\` field and optional \`code\`:
41044132 ) ;
41054133 }
41064134
4107- const milestone = await prisma . $transaction ( async ( tx ) => {
4108- const activeCount = await tx . milestone . count ( {
4109- where : { profileId : profile . id , status : { not : "reached" } } ,
4110- } ) ;
4111- if ( activeCount >= 20 ) {
4112- throw new Error ( "MAX_MILESTONES_EXCEEDED" ) ;
4113- }
4135+ const milestone = await prisma . $transaction (
4136+ async ( tx ) => {
4137+ const activeCount = await tx . milestone . count ( {
4138+ where : { profileId : profile . id , status : { not : "reached" } } ,
4139+ } ) ;
4140+ if ( activeCount >= 20 ) {
4141+ throw new Error ( "MAX_MILESTONES_EXCEEDED" ) ;
4142+ }
41144143
4115- const created = await tx . milestone . create ( {
4116- data : {
4117- title : parsed . data . title ,
4118- description : parsed . data . description ,
4119- targetAmount : parsed . data . targetAmount ,
4120- assetCode : parsed . data . assetCode ,
4121- assetIssuer : parsed . data . assetIssuer ?? null ,
4122- profileId : profile . id ,
4123- } ,
4124- } ) ;
4125- return created ;
4126- } ) ;
4144+ const created = await tx . milestone . create ( {
4145+ data : {
4146+ title : parsed . data . title ,
4147+ description : parsed . data . description ,
4148+ targetAmount : parsed . data . targetAmount ,
4149+ assetCode : parsed . data . assetCode ,
4150+ assetIssuer : parsed . data . assetIssuer ?? null ,
4151+ profileId : profile . id ,
4152+ } ,
4153+ } ) ;
4154+ return created ;
4155+ } ,
4156+ { isolationLevel : "Serializable" } ,
4157+ ) ;
41274158
41284159 res . status ( 201 ) . json ( milestone ) ;
41294160 } catch ( err ) {
0 commit comments