@@ -7,7 +7,7 @@ import { testClient } from "hono/testing";
77import { createHash , createPrivateKey , createSign , generateKeyPairSync } from "node:crypto" ;
88import { hexToBytes , padHex , zeroHash } from "viem" ;
99import { privateKeyToAddress } from "viem/accounts" ;
10- import { afterEach , beforeAll , describe , expect , inject , it , vi } from "vitest" ;
10+ import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , inject , it , vi } from "vitest" ;
1111
1212import deriveAddress from "@exactly/common/deriveAddress" ;
1313
@@ -16,6 +16,8 @@ import app from "../../hooks/bridge";
1616import * as onesignal from "../../utils/onesignal" ;
1717import * as persona from "../../utils/persona" ;
1818import * as bridge from "../../utils/ramps/bridge" ;
19+ import { feeWindow } from "../../utils/ramps/bridge" ;
20+ import redis , { close as closeRedis } from "../../utils/redis" ;
1921import * as segment from "../../utils/segment" ;
2022
2123const appClient = testClient ( app ) ;
@@ -54,11 +56,21 @@ describe("bridge hook", () => {
5456 ] ) ;
5557 } ) ;
5658
59+ beforeEach ( async ( ) => {
60+ const keys = await redis . keys ( "wr:bridge-fees:*" ) ;
61+ if ( keys . length > 0 ) await redis . del ( ...keys ) ;
62+ } ) ;
63+
5764 afterEach ( ( ) => {
5865 vi . clearAllMocks ( ) ;
5966 vi . restoreAllMocks ( ) ;
6067 } ) ;
6168
69+ afterAll ( async ( ) => {
70+ await feeWindow . stop ( ) ;
71+ await closeRedis ( ) ;
72+ } ) ;
73+
6274 it ( "returns 200 with valid signature and payload" , async ( ) => {
6375 vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
6476 const response = await appClient . index . $post ( {
@@ -138,6 +150,39 @@ describe("bridge hook", () => {
138150 await expect ( response . json ( ) ) . resolves . toMatchObject ( { code : "bad bridge" } ) ;
139151 } ) ;
140152
153+ it ( "rejects payment_submitted with invalid created_at" , async ( ) => {
154+ const payload = {
155+ ...paymentSubmitted ,
156+ event_object : { ...paymentSubmitted . event_object , created_at : "not-a-date" } ,
157+ } ;
158+ const response = await appClient . index . $post ( {
159+ header : { "x-webhook-signature" : createSignature ( payload ) } ,
160+ json : payload as never ,
161+ } ) ;
162+
163+ expect ( response . status ) . toBe ( 200 ) ;
164+ await expect ( response . json ( ) ) . resolves . toStrictEqual ( {
165+ code : "bad bridge" ,
166+ legacy : "bad bridge" ,
167+ message : expect . arrayContaining ( [ expect . stringContaining ( "invalid date" ) ] ) , // eslint-disable-line @typescript-eslint/no-unsafe-assignment
168+ } ) ;
169+ } ) ;
170+
171+ it ( "rejects drain with invalid created_at" , async ( ) => {
172+ const payload = { ...drain , event_object : { ...drain . event_object , created_at : "invalid" } } ;
173+ const response = await appClient . index . $post ( {
174+ header : { "x-webhook-signature" : createSignature ( payload ) } ,
175+ json : payload as never ,
176+ } ) ;
177+
178+ expect ( response . status ) . toBe ( 200 ) ;
179+ await expect ( response . json ( ) ) . resolves . toStrictEqual ( {
180+ code : "bad bridge" ,
181+ legacy : "bad bridge" ,
182+ message : expect . arrayContaining ( [ expect . stringContaining ( "invalid date" ) ] ) , // eslint-disable-line @typescript-eslint/no-unsafe-assignment
183+ } ) ;
184+ } ) ;
185+
141186 it ( "returns 200 without side effects for non-payment virtual account types" , async ( ) => {
142187 vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
143188 const sendPushNotification = vi . spyOn ( onesignal , "sendPushNotification" ) ;
@@ -153,6 +198,36 @@ describe("bridge hook", () => {
153198 expect ( captureException ) . not . toHaveBeenCalled ( ) ;
154199 } ) ;
155200
201+ it ( "returns 500 when feeWindow.report fails on payment_submitted" , async ( ) => {
202+ vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
203+ const sendPushNotification = vi . spyOn ( onesignal , "sendPushNotification" ) ;
204+ vi . spyOn ( feeWindow , "report" ) . mockRejectedValue ( new Error ( "redis down" ) ) ;
205+ const response = await appClient . index . $post ( {
206+ header : { "x-webhook-signature" : createSignature ( paymentSubmitted ) } ,
207+ json : paymentSubmitted as never ,
208+ } ) ;
209+
210+ expect ( response . status ) . toBe ( 500 ) ;
211+ expect ( sendPushNotification ) . not . toHaveBeenCalled ( ) ;
212+ expect ( segment . track ) . not . toHaveBeenCalled ( ) ;
213+ expect ( captureException ) . not . toHaveBeenCalled ( ) ;
214+ } ) ;
215+
216+ it ( "returns 500 when feeWindow.report fails on drain" , async ( ) => {
217+ vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
218+ const sendPushNotification = vi . spyOn ( onesignal , "sendPushNotification" ) ;
219+ vi . spyOn ( feeWindow , "report" ) . mockRejectedValue ( new Error ( "redis down" ) ) ;
220+ const response = await appClient . index . $post ( {
221+ header : { "x-webhook-signature" : createSignature ( drain ) } ,
222+ json : drain as never ,
223+ } ) ;
224+
225+ expect ( response . status ) . toBe ( 500 ) ;
226+ expect ( sendPushNotification ) . not . toHaveBeenCalled ( ) ;
227+ expect ( segment . track ) . not . toHaveBeenCalled ( ) ;
228+ expect ( captureException ) . not . toHaveBeenCalled ( ) ;
229+ } ) ;
230+
156231 it ( "does not track onramp for payment_submitted virtual account" , async ( ) => {
157232 vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
158233 const response = await appClient . index . $post ( {
@@ -185,6 +260,7 @@ describe("bridge hook", () => {
185260 it ( "tracks onramp for payment_processed virtual account" , async ( ) => {
186261 vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
187262 const sendPushNotification = vi . spyOn ( onesignal , "sendPushNotification" ) ;
263+ const reportSpy = vi . spyOn ( feeWindow , "report" ) ;
188264 const response = await appClient . index . $post ( {
189265 header : { "x-webhook-signature" : createSignature ( paymentProcessed ) } ,
190266 json : paymentProcessed as never ,
@@ -198,6 +274,7 @@ describe("bridge hook", () => {
198274 properties : { currency : "usd" , amount : 1000 , provider : "bridge" , source : null , usdcAmount : 995 } ,
199275 } ) ;
200276 expect ( sendPushNotification ) . not . toHaveBeenCalled ( ) ;
277+ expect ( reportSpy ) . not . toHaveBeenCalled ( ) ;
201278 expect ( captureException ) . not . toHaveBeenCalled ( ) ;
202279 } ) ;
203280
@@ -600,6 +677,78 @@ describe("bridge hook", () => {
600677 expect ( sendPushNotification ) . not . toHaveBeenCalled ( ) ;
601678 expect ( captureException ) . not . toHaveBeenCalled ( ) ;
602679 } ) ;
680+
681+ it ( "tracks ramp amount via feeWindow.report on payment_submitted" , async ( ) => {
682+ vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
683+ const reportSpy = vi . spyOn ( feeWindow , "report" ) ;
684+ await appClient . index . $post ( {
685+ header : { "x-webhook-signature" : createSignature ( paymentSubmitted ) } ,
686+ json : paymentSubmitted as never ,
687+ } ) ;
688+
689+ expect ( reportSpy ) . toHaveBeenCalledExactlyOnceWith (
690+ { amount : "1000" , bridgeId : "bridgeCustomerId" , eventId : "evt_123" } ,
691+ new Date ( "2026-03-01T00:00:00.000Z" ) ,
692+ ) ;
693+ } ) ;
694+
695+ it ( "tracks ramp amount via feeWindow.report on drain payment_submitted" , async ( ) => {
696+ vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
697+ const reportSpy = vi . spyOn ( feeWindow , "report" ) ;
698+ await appClient . index . $post ( {
699+ header : { "x-webhook-signature" : createSignature ( drain ) } ,
700+ json : drain as never ,
701+ } ) ;
702+
703+ expect ( reportSpy ) . toHaveBeenCalledExactlyOnceWith (
704+ { amount : "500" , bridgeId : "bridgeCustomerId" , eventId : "drain_123" } ,
705+ new Date ( "2026-03-01T00:00:00.000Z" ) ,
706+ ) ;
707+ } ) ;
708+
709+ it ( "passes fractional amount to feeWindow.report on payment_submitted" , async ( ) => {
710+ vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
711+ const reportSpy = vi . spyOn ( feeWindow , "report" ) ;
712+ const payload = {
713+ ...paymentSubmitted ,
714+ event_object : {
715+ ...paymentSubmitted . event_object ,
716+ id : "evt_frac_1" ,
717+ receipt : { initial_amount : "99.999" , final_amount : "99.999" } ,
718+ } ,
719+ } ;
720+ await appClient . index . $post ( {
721+ header : { "x-webhook-signature" : createSignature ( payload ) } ,
722+ json : payload as never ,
723+ } ) ;
724+
725+ expect ( reportSpy ) . toHaveBeenCalledExactlyOnceWith (
726+ { amount : "99.999" , bridgeId : "bridgeCustomerId" , eventId : "evt_frac_1" } ,
727+ expect . any ( Date ) ,
728+ ) ;
729+ } ) ;
730+
731+ it ( "passes fractional amount to feeWindow.report on drain" , async ( ) => {
732+ vi . spyOn ( segment , "track" ) . mockReturnValue ( ) ;
733+ const reportSpy = vi . spyOn ( feeWindow , "report" ) ;
734+ const payload = {
735+ ...drain ,
736+ event_object : {
737+ ...drain . event_object ,
738+ id : "drain_frac_1" ,
739+ receipt : { initial_amount : "49.991" , outgoing_amount : "49.991" } ,
740+ } ,
741+ } ;
742+ await appClient . index . $post ( {
743+ header : { "x-webhook-signature" : createSignature ( payload ) } ,
744+ json : payload as never ,
745+ } ) ;
746+
747+ expect ( reportSpy ) . toHaveBeenCalledExactlyOnceWith (
748+ { amount : "49.991" , bridgeId : "bridgeCustomerId" , eventId : "drain_frac_1" } ,
749+ expect . any ( Date ) ,
750+ ) ;
751+ } ) ;
603752} ) ;
604753
605754const testSigningKey = createPrivateKey ( `-----BEGIN PRIVATE KEY-----
@@ -651,6 +800,7 @@ const fundsReceived = {
651800const paymentSubmitted = {
652801 event_type : "virtual_account.activity.created" ,
653802 event_object : {
803+ created_at : "2026-03-01T00:00:00.000Z" ,
654804 id : "evt_123" ,
655805 type : "payment_submitted" ,
656806 currency : "usd" ,
@@ -678,6 +828,7 @@ const statusTransitioned = {
678828const drain = {
679829 event_type : "liquidation_address.drain.updated.status_transitioned" ,
680830 event_object : {
831+ created_at : "2026-03-01T00:00:00.000Z" ,
681832 id : "drain_123" ,
682833 state : "payment_submitted" ,
683834 currency : "usdc" ,
@@ -687,3 +838,8 @@ const drain = {
687838} ;
688839
689840vi . mock ( "@sentry/core" , { spy : true } ) ;
841+ vi . mock ( "../../utils/ramps/bridge" , async ( importOriginal ) => ( {
842+ ...( await importOriginal ( ) ) ,
843+ enableFees : vi . fn ( ) ,
844+ disableFees : vi . fn ( ) ,
845+ } ) ) ;
0 commit comments