@@ -547,6 +547,137 @@ describe("POST /api/payments/coinpayportal/webhook", () => {
547547 expect ( notifChain . insert ) . not . toHaveBeenCalled ( ) ;
548548 } ) ;
549549
550+ it ( "escrow.released does not release a pending payment escrow" , async ( ) => {
551+ const payload = makeWebhookPayload ( "escrow.released" , {
552+ payment_id : "cp-escrow-pending" ,
553+ status : "released" ,
554+ } ) ;
555+
556+ const escrowFetchChain = chainResult ( {
557+ data : {
558+ id : "escrow-pending" ,
559+ coinpay_escrow_id : "cp-escrow-pending" ,
560+ status : "pending_payment" ,
561+ application_id : "app-escrow" ,
562+ gig_id : "gig-escrow" ,
563+ worker_id : "worker-escrow" ,
564+ poster_id : "poster-escrow" ,
565+ amount_usd : 20 ,
566+ } ,
567+ error : null ,
568+ } ) ;
569+ const escrowUpdateChain = chainResult ( { data : null , error : null } ) ;
570+ const appChain = chainResult ( { data : null , error : null } ) ;
571+ let escrowCalls = 0 ;
572+
573+ mockFrom . mockImplementation ( ( table : string ) => {
574+ if ( table === "gig_escrows" ) {
575+ escrowCalls += 1 ;
576+ return escrowCalls === 1 ? escrowFetchChain : escrowUpdateChain ;
577+ }
578+ if ( table === "applications" ) return appChain ;
579+ return chainResult ( { data : null , error : null } ) ;
580+ } ) ;
581+
582+ const res = await POST ( makeRequest ( payload , WEBHOOK_SECRET ) ) ;
583+ expect ( res . status ) . toBe ( 200 ) ;
584+ expect ( escrowUpdateChain . update ) . not . toHaveBeenCalled ( ) ;
585+ expect ( appChain . update ) . not . toHaveBeenCalled ( ) ;
586+ } ) ;
587+
588+ it ( "escrow.released transitions a funded escrow and completes the application" , async ( ) => {
589+ const payload = makeWebhookPayload ( "escrow.released" , {
590+ payment_id : "cp-escrow-funded" ,
591+ status : "released" ,
592+ } ) ;
593+
594+ const escrowFetchChain = chainResult ( {
595+ data : {
596+ id : "escrow-funded" ,
597+ coinpay_escrow_id : "cp-escrow-funded" ,
598+ status : "funded" ,
599+ application_id : "app-escrow" ,
600+ gig_id : "gig-escrow" ,
601+ worker_id : "worker-escrow" ,
602+ poster_id : "poster-escrow" ,
603+ amount_usd : 20 ,
604+ } ,
605+ error : null ,
606+ } ) ;
607+ const escrowUpdateChain = chainResult ( { data : { id : "escrow-funded" } , error : null } ) ;
608+ const appChain = chainResult ( { data : null , error : null } ) ;
609+ let escrowCalls = 0 ;
610+
611+ mockFrom . mockImplementation ( ( table : string ) => {
612+ if ( table === "gig_escrows" ) {
613+ escrowCalls += 1 ;
614+ return escrowCalls === 1 ? escrowFetchChain : escrowUpdateChain ;
615+ }
616+ if ( table === "applications" ) return appChain ;
617+ return chainResult ( { data : null , error : null } ) ;
618+ } ) ;
619+
620+ const res = await POST ( makeRequest ( payload , WEBHOOK_SECRET ) ) ;
621+ expect ( res . status ) . toBe ( 200 ) ;
622+ expect ( escrowUpdateChain . update ) . toHaveBeenCalledWith (
623+ expect . objectContaining ( { status : "released" } )
624+ ) ;
625+ expect ( escrowUpdateChain . eq ) . toHaveBeenCalledWith ( "status" , "funded" ) ;
626+ expect ( appChain . update ) . toHaveBeenCalledWith (
627+ expect . objectContaining ( { status : "completed" } )
628+ ) ;
629+ } ) ;
630+
631+ it ( "escrow.refunded transitions a funded escrow and notifies the poster" , async ( ) => {
632+ const payload = makeWebhookPayload ( "escrow.refunded" , {
633+ payment_id : "cp-escrow-funded" ,
634+ status : "refunded" ,
635+ } ) ;
636+
637+ const escrowFetchChain = chainResult ( {
638+ data : {
639+ id : "escrow-funded" ,
640+ coinpay_escrow_id : "cp-escrow-funded" ,
641+ status : "funded" ,
642+ application_id : "app-escrow" ,
643+ gig_id : "gig-escrow" ,
644+ worker_id : "worker-escrow" ,
645+ poster_id : "poster-escrow" ,
646+ amount_usd : 20 ,
647+ } ,
648+ error : null ,
649+ } ) ;
650+ const escrowUpdateChain = chainResult ( { data : { id : "escrow-funded" } , error : null } ) ;
651+ const notifChain = chainResult ( { data : null , error : null } ) ;
652+ let escrowCalls = 0 ;
653+
654+ mockFrom . mockImplementation ( ( table : string ) => {
655+ if ( table === "gig_escrows" ) {
656+ escrowCalls += 1 ;
657+ return escrowCalls === 1 ? escrowFetchChain : escrowUpdateChain ;
658+ }
659+ if ( table === "notifications" ) return notifChain ;
660+ return chainResult ( { data : null , error : null } ) ;
661+ } ) ;
662+
663+ const res = await POST ( makeRequest ( payload , WEBHOOK_SECRET ) ) ;
664+ expect ( res . status ) . toBe ( 200 ) ;
665+ expect ( escrowUpdateChain . update ) . toHaveBeenCalledWith (
666+ expect . objectContaining ( { status : "refunded" } )
667+ ) ;
668+ expect ( escrowUpdateChain . in ) . toHaveBeenCalledWith ( "status" , [
669+ "pending_payment" ,
670+ "funded" ,
671+ "disputed" ,
672+ ] ) ;
673+ expect ( notifChain . insert ) . toHaveBeenCalledWith (
674+ expect . objectContaining ( {
675+ user_id : "poster-escrow" ,
676+ title : "Escrow refunded" ,
677+ } )
678+ ) ;
679+ } ) ;
680+
550681 it ( "escrow.refunded does not override an already released escrow" , async ( ) => {
551682 const payload = makeWebhookPayload ( "escrow.refunded" , {
552683 payment_id : "cp-escrow-released" ,
0 commit comments