@@ -4,6 +4,7 @@ use prometheus::{
44 register_histogram_vec, CounterVec , Encoder , Gauge , GaugeVec , HistogramVec , TextEncoder ,
55} ;
66use serde:: Serialize ;
7+ use dashmap:: DashMap ;
78use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
89use std:: time:: { SystemTime , UNIX_EPOCH } ;
910
@@ -144,13 +145,24 @@ lazy_static! {
144145 )
145146 . expect( "Can't create payment_processing_duration_seconds metric" ) ;
146147
148+ /// Payment transaction lifecycle counter
149+ pub static ref PAYMENT_TRANSACTIONS_TOTAL : CounterVec = register_counter_vec!(
150+ "payment_transactions_total" ,
151+ "Total payment transactions by merchant, status, method and currency" ,
152+ & [ "merchant_id" , "status" , "payment_method" , "currency" ]
153+ )
154+ . expect( "Can't create payment_transactions_total metric" ) ;
155+
147156 /// Application start time (Unix timestamp)
148157 static ref APP_START_TIME : AtomicU64 = AtomicU64 :: new(
149158 SystemTime :: now( )
150159 . duration_since( UNIX_EPOCH )
151160 . unwrap( )
152161 . as_secs( )
153162 ) ;
163+
164+ /// In-memory counters for computing success rate gauge (process lifetime).
165+ static ref PAYMENT_OUTCOME_COUNTS : DashMap <String , ( u64 , u64 , u64 ) > = DashMap :: new( ) ;
154166}
155167
156168/// Metrics payload as specified in the issue
@@ -275,6 +287,7 @@ pub struct AlertPayload {
275287 let _ = & * PAYMENT_VOLUME_TOTAL ;
276288 let _ = & * PAYMENT_SUCCESS_RATE ;
277289 let _ = & * PAYMENT_PROCESSING_DURATION_SECONDS ;
290+ let _ = & * PAYMENT_TRANSACTIONS_TOTAL ;
278291
279292 tracing:: info!( "Metrics service initialized" ) ;
280293 }
@@ -304,6 +317,55 @@ pub struct AlertPayload {
304317 . observe ( duration_secs) ;
305318 }
306319
320+ fn currency_label_from_asset ( asset : & str ) -> String {
321+ if asset. eq_ignore_ascii_case ( "XLM" ) {
322+ return "XLM" . to_string ( ) ;
323+ }
324+
325+ // Expected format: CODE:ISSUER
326+ asset. split ( ':' ) . next ( ) . unwrap_or ( asset) . to_string ( )
327+ }
328+
329+ /// Record a payment lifecycle event and refresh derived gauges.
330+ pub fn record_payment_transaction (
331+ merchant_id : & str ,
332+ status : & str ,
333+ payment_method : & str ,
334+ send_asset : & str ,
335+ amount : i64 ,
336+ ) {
337+ let currency = Self :: currency_label_from_asset ( send_asset) ;
338+
339+ PAYMENT_TRANSACTIONS_TOTAL
340+ . with_label_values ( & [ merchant_id, status, payment_method, & currency] )
341+ . inc ( ) ;
342+
343+ // Treat volume as "requested" unless status indicates a finalized successful payment.
344+ if status == "created" || status == "completed" {
345+ Self :: record_payment_volume ( merchant_id, & currency, amount as f64 ) ;
346+ }
347+
348+ let mut entry = PAYMENT_OUTCOME_COUNTS
349+ . entry ( merchant_id. to_string ( ) )
350+ . or_insert ( ( 0 , 0 , 0 ) ) ;
351+
352+ match status {
353+ "created" => entry. value_mut ( ) . 0 = entry. value ( ) . 0 . saturating_add ( 1 ) ,
354+ "completed" => entry. value_mut ( ) . 1 = entry. value ( ) . 1 . saturating_add ( 1 ) ,
355+ "failed" => entry. value_mut ( ) . 2 = entry. value ( ) . 2 . saturating_add ( 1 ) ,
356+ _ => { }
357+ }
358+
359+ let ( created, completed, failed) = * entry. value ( ) ;
360+ let denom = created. saturating_add ( failed) ;
361+ let success_rate_percent = if denom == 0 {
362+ 0.0
363+ } else {
364+ ( completed as f64 / denom as f64 ) * 100.0
365+ } ;
366+ Self :: update_payment_success_rate ( merchant_id, success_rate_percent) ;
367+ }
368+
307369 /// Get application uptime in seconds
308370 pub fn get_uptime ( ) -> u64 {
309371 let start_time = APP_START_TIME . load ( Ordering :: Relaxed ) ;
@@ -607,6 +669,43 @@ pub struct AlertPayload {
607669 }
608670}
609671
672+ #[ cfg( test) ]
673+ mod payment_metrics_tests {
674+ use super :: * ;
675+
676+ #[ test]
677+ fn record_payment_transaction_updates_counters_and_success_rate ( ) {
678+ MetricsService :: init ( ) ;
679+
680+ MetricsService :: record_payment_transaction ( "m1" , "created" , "api" , "XLM" , 100 ) ;
681+ MetricsService :: record_payment_transaction ( "m1" , "completed" , "stellar_indexer" , "XLM" , 100 ) ;
682+
683+ // Counter exists and has been incremented.
684+ let families = PAYMENT_TRANSACTIONS_TOTAL . collect ( ) ;
685+ let total: f64 = families
686+ . first ( )
687+ . unwrap ( )
688+ . get_metric ( )
689+ . iter ( )
690+ . map ( |m| m. get_counter ( ) . get_value ( ) )
691+ . sum ( ) ;
692+ assert ! ( total >= 2.0 ) ;
693+
694+ // Derived gauge is updated for merchant.
695+ let gauge_families = PAYMENT_SUCCESS_RATE . collect ( ) ;
696+ let merchant_gauge = gauge_families
697+ . first ( )
698+ . unwrap ( )
699+ . get_metric ( )
700+ . iter ( )
701+ . find ( |m| m. get_label ( ) . iter ( ) . any ( |l| l. get_name ( ) == "merchant_id" && l. get_value ( ) == "m1" ) )
702+ . unwrap ( )
703+ . get_gauge ( )
704+ . get_value ( ) ;
705+ assert ! ( merchant_gauge > 0.0 ) ;
706+ }
707+ }
708+
610709#[ cfg( test) ]
611710mod tests {
612711 use super :: * ;
0 commit comments