@@ -956,3 +956,169 @@ describe("resolveCloneChain", () => {
956956 await expect ( client . resolveCloneChain ( "x" ) ) . rejects . toThrow ( "clone chain depth exceeded" ) ;
957957 } ) ;
958958} ) ;
959+
960+ describe ( "trackVelocity" , ( ) => {
961+ it ( "calculates payments per day from payment timestamps" , async ( ) => {
962+ const { trackVelocity } = await import ( "../src/velocityTracker.js" ) ;
963+
964+ const client = new StellarSplitClient ( {
965+ rpcUrl : "https://example.com" ,
966+ networkPassphrase : "Test Network" ,
967+ contractId : StrKey . encodeContract ( Keypair . random ( ) . rawPublicKey ( ) ) ,
968+ } ) ;
969+
970+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
971+ const creatorAddr = "GCREATORXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ;
972+
973+ vi . spyOn ( client , "getInvoicesByCreator" ) . mockResolvedValue ( {
974+ items : [ "inv1" ] ,
975+ nextCursor : null ,
976+ total : 1 ,
977+ } ) ;
978+
979+ vi . spyOn ( client , "getInvoice" ) . mockResolvedValue ( {
980+ id : "inv1" ,
981+ creator : creatorAddr ,
982+ recipients : [ ] ,
983+ token : "GUSDCXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ,
984+ deadline : now + 86_400 ,
985+ funded : 1_000_000n ,
986+ status : "Pending" as const ,
987+ payments : [
988+ { payer : "GPAYER1" , amount : 100_000n , timestamp : now } ,
989+ { payer : "GPAYER2" , amount : 100_000n , timestamp : now + 43_200 } , // 12 hours later
990+ { payer : "GPAYER3" , amount : 100_000n , timestamp : now + 86_400 } , // 1 day later
991+ ] ,
992+ } as any ) ;
993+
994+ const report = await trackVelocity ( creatorAddr , client ) ;
995+
996+ expect ( report . address ) . toBe ( creatorAddr ) ;
997+ expect ( report . invoices ) . toHaveLength ( 1 ) ;
998+ expect ( report . invoices [ 0 ] ! . invoiceId ) . toBe ( "inv1" ) ;
999+ expect ( report . invoices [ 0 ] ! . paymentsPerDay ) . toBeGreaterThan ( 0 ) ;
1000+ expect ( report . invoices [ 0 ] ! . paymentsPerDay ) . toBeLessThan ( 10 ) ;
1001+ } ) ;
1002+
1003+ it ( "classifies stalling trend for decreasing payment rate" , async ( ) => {
1004+ const { trackVelocity } = await import ( "../src/velocityTracker.js" ) ;
1005+
1006+ const client = new StellarSplitClient ( {
1007+ rpcUrl : "https://example.com" ,
1008+ networkPassphrase : "Test Network" ,
1009+ contractId : StrKey . encodeContract ( Keypair . random ( ) . rawPublicKey ( ) ) ,
1010+ } ) ;
1011+
1012+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
1013+ const creatorAddr = "GCREATORXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ;
1014+
1015+ vi . spyOn ( client , "getInvoicesByCreator" ) . mockResolvedValue ( {
1016+ items : [ "inv1" ] ,
1017+ nextCursor : null ,
1018+ total : 1 ,
1019+ } ) ;
1020+
1021+ // Payments concentrated early (stalling pattern)
1022+ vi . spyOn ( client , "getInvoice" ) . mockResolvedValue ( {
1023+ id : "inv1" ,
1024+ creator : creatorAddr ,
1025+ recipients : [ ] ,
1026+ token : "GUSDCXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ,
1027+ deadline : now + 864_000 ,
1028+ funded : 1_000_000n ,
1029+ status : "Pending" as const ,
1030+ payments : [
1031+ { payer : "GPAYER1" , amount : 100_000n , timestamp : now } ,
1032+ { payer : "GPAYER2" , amount : 100_000n , timestamp : now + 3_600 } ,
1033+ { payer : "GPAYER3" , amount : 100_000n , timestamp : now + 7_200 } ,
1034+ { payer : "GPAYER4" , amount : 100_000n , timestamp : now + 432_000 } , // 5 days later
1035+ { payer : "GPAYER5" , amount : 100_000n , timestamp : now + 435_600 } ,
1036+ ] ,
1037+ } as any ) ;
1038+
1039+ const report = await trackVelocity ( creatorAddr , client ) ;
1040+
1041+ expect ( report . invoices [ 0 ] ! . trend ) . toBe ( "stalling" ) ;
1042+ } ) ;
1043+
1044+ it ( "classifies accelerating trend for increasing payment rate" , async ( ) => {
1045+ const { trackVelocity } = await import ( "../src/velocityTracker.js" ) ;
1046+
1047+ const client = new StellarSplitClient ( {
1048+ rpcUrl : "https://example.com" ,
1049+ networkPassphrase : "Test Network" ,
1050+ contractId : StrKey . encodeContract ( Keypair . random ( ) . rawPublicKey ( ) ) ,
1051+ } ) ;
1052+
1053+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
1054+ const creatorAddr = "GCREATORXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ;
1055+
1056+ vi . spyOn ( client , "getInvoicesByCreator" ) . mockResolvedValue ( {
1057+ items : [ "inv1" ] ,
1058+ nextCursor : null ,
1059+ total : 1 ,
1060+ } ) ;
1061+
1062+ // Payments concentrated later (accelerating pattern)
1063+ vi . spyOn ( client , "getInvoice" ) . mockResolvedValue ( {
1064+ id : "inv1" ,
1065+ creator : creatorAddr ,
1066+ recipients : [ ] ,
1067+ token : "GUSDCXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ,
1068+ deadline : now + 864_000 ,
1069+ funded : 1_000_000n ,
1070+ status : "Pending" as const ,
1071+ payments : [
1072+ { payer : "GPAYER1" , amount : 100_000n , timestamp : now } ,
1073+ { payer : "GPAYER2" , amount : 100_000n , timestamp : now + 172_800 } , // First half ends here (5 payments / 2 = 2.5)
1074+ { payer : "GPAYER3" , amount : 100_000n , timestamp : now + 345_600 } ,
1075+ { payer : "GPAYER4" , amount : 100_000n , timestamp : now + 432_000 } ,
1076+ { payer : "GPAYER5" , amount : 100_000n , timestamp : now + 439_200 } ,
1077+ ] ,
1078+ } as any ) ;
1079+
1080+ const report = await trackVelocity ( creatorAddr , client ) ;
1081+
1082+ expect ( report . invoices [ 0 ] ! . trend ) . toBe ( "accelerating" ) ;
1083+ } ) ;
1084+
1085+ it ( "classifies steady trend for constant payment rate" , async ( ) => {
1086+ const { trackVelocity } = await import ( "../src/velocityTracker.js" ) ;
1087+
1088+ const client = new StellarSplitClient ( {
1089+ rpcUrl : "https://example.com" ,
1090+ networkPassphrase : "Test Network" ,
1091+ contractId : StrKey . encodeContract ( Keypair . random ( ) . rawPublicKey ( ) ) ,
1092+ } ) ;
1093+
1094+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
1095+ const creatorAddr = "GCREATORXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ;
1096+
1097+ vi . spyOn ( client , "getInvoicesByCreator" ) . mockResolvedValue ( {
1098+ items : [ "inv1" ] ,
1099+ nextCursor : null ,
1100+ total : 1 ,
1101+ } ) ;
1102+
1103+ // Evenly distributed payments
1104+ vi . spyOn ( client , "getInvoice" ) . mockResolvedValue ( {
1105+ id : "inv1" ,
1106+ creator : creatorAddr ,
1107+ recipients : [ ] ,
1108+ token : "GUSDCXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ,
1109+ deadline : now + 864_000 ,
1110+ funded : 1_000_000n ,
1111+ status : "Pending" as const ,
1112+ payments : [
1113+ { payer : "GPAYER1" , amount : 100_000n , timestamp : now } ,
1114+ { payer : "GPAYER2" , amount : 100_000n , timestamp : now + 86_400 } ,
1115+ { payer : "GPAYER3" , amount : 100_000n , timestamp : now + 172_800 } ,
1116+ { payer : "GPAYER4" , amount : 100_000n , timestamp : now + 259_200 } ,
1117+ ] ,
1118+ } as any ) ;
1119+
1120+ const report = await trackVelocity ( creatorAddr , client ) ;
1121+
1122+ expect ( report . invoices [ 0 ] ! . trend ) . toBe ( "steady" ) ;
1123+ } ) ;
1124+ } ) ;
0 commit comments