Bug: Add gsm api for v2



Add gsm api for v2
diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs
index 8fbc29c6963..c874271285b 100644
--- a/crates/common_enums/src/enums.rs
+++ b/crates/common_enums/src/enums.rs
@@ -8385,6 +8385,8 @@ pub enum ErrorCategory {
     ProcessorDeclineUnauthorized,
     IssueWithPaymentMethod,
     ProcessorDeclineIncorrectData,
+    HardDecline,
+    SoftDecline,
 }
 
 impl ErrorCategory {
@@ -8393,7 +8395,9 @@ impl ErrorCategory {
             Self::ProcessorDowntime | Self::ProcessorDeclineUnauthorized => true,
             Self::IssueWithPaymentMethod
             | Self::ProcessorDeclineIncorrectData
-            | Self::FrmDecline => false,
+            | Self::FrmDecline
+            | Self::HardDecline
+            | Self::SoftDecline => false,
         }
     }
 }
diff --git a/crates/router/src/core/revenue_recovery/types.rs b/crates/router/src/core/revenue_recovery/types.rs
index 00a661527a1..1bccb5134bb 100644
--- a/crates/router/src/core/revenue_recovery/types.rs
+++ b/crates/router/src/core/revenue_recovery/types.rs
@@ -49,7 +49,7 @@ use crate::{
 };
 
 type RecoveryResult<T> = error_stack::Result<T, errors::RecoveryError>;
-
+pub const REVENUE_RECOVERY: &str = "revenue_recovery";
 /// The status of Passive Churn Payments
 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
 pub enum RevenueRecoveryPaymentsAttemptStatus {
@@ -559,7 +559,7 @@ impl Action {
             revenue_recovery_payment_data,
         )
         .await;
-        let db = &*state.store;
+
         match response {
             Ok(_payment_data) => match payment_attempt.status.foreign_into() {
                 RevenueRecoveryPaymentsAttemptStatus::Succeeded => {
@@ -757,7 +757,35 @@ impl Action {
         payment_attempt: &PaymentAttempt,
         payment_intent: &PaymentIntent,
     ) -> RecoveryResult<Self> {
+        let db = &*state.store;
         let next_retry_count = pt.retry_count + 1;
+        let error_message = payment_attempt
+            .error
+            .as_ref()
+            .map(|details| details.message.clone());
+        let error_code = payment_attempt
+            .error
+            .as_ref()
+            .map(|details| details.code.clone());
+        let connector_name = payment_attempt
+            .connector
+            .clone()
+            .ok_or(errors::RecoveryError::ValueNotFound)
+            .attach_printable("unable to derive payment connector from payment attempt")?;
+        let gsm_record = helpers::get_gsm_record(
+            state,
+            error_code,
+            error_message,
+            connector_name,
+            REVENUE_RECOVERY.to_string(),
+        )
+        .await;
+        let is_hard_decline = gsm_record
+            .and_then(|gsm_record| gsm_record.error_category)
+            .map(|gsm_error_category| {
+                gsm_error_category == common_enums::ErrorCategory::HardDecline
+            })
+            .unwrap_or(false);
         let schedule_time = revenue_recovery_payment_data
             .get_schedule_time_based_on_retry_type(
                 state,
@@ -765,6 +793,7 @@ impl Action {
                 next_retry_count,
                 payment_attempt,
                 payment_intent,
+                is_hard_decline,
             )
             .await;
 
@@ -775,7 +804,6 @@ impl Action {
         }
     }
 }
-
 // TODO: Move these to impl based functions
 async fn record_back_to_billing_connector(
     state: &SessionState,
diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs
index 828986aeeee..4584bee1725 100644
--- a/crates/router/src/lib.rs
+++ b/crates/router/src/lib.rs
@@ -219,7 +219,8 @@ pub fn mk_app(
             server_app = server_app
                 .service(routes::UserDeprecated::server(state.clone()))
                 .service(routes::ProcessTrackerDeprecated::server(state.clone()))
-                .service(routes::ProcessTracker::server(state.clone()));
+                .service(routes::ProcessTracker::server(state.clone()))
+                .service(routes::Gsm::server(state.clone()));
         }
     }
 
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index f5e7eee7347..cf684a680e8 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -2292,7 +2292,7 @@ impl ProfileNew {
 
 pub struct Gsm;
 
-#[cfg(all(feature = "olap", feature = "v1"))]
+#[cfg(all(feature = "v1", feature = "olap"))]
 impl Gsm {
     pub fn server(state: AppState) -> Scope {
         web::scope("/gsm")
@@ -2304,6 +2304,17 @@ impl Gsm {
     }
 }
 
+#[cfg(all(feature = "v2", feature = "olap"))]
+impl Gsm {
+    pub fn server(state: AppState) -> Scope {
+        web::scope("/v2/gsm")
+            .app_data(web::Data::new(state))
+            .service(web::resource("").route(web::post().to(gsm::create_gsm_rule)))
+            .service(web::resource("/get").route(web::post().to(gsm::get_gsm_rule)))
+            .service(web::resource("/update").route(web::post().to(gsm::update_gsm_rule)))
+            .service(web::resource("/delete").route(web::post().to(gsm::delete_gsm_rule)))
+    }
+}
 pub struct Chat;
 
 #[cfg(feature = "olap")]
diff --git a/crates/router/src/routes/gsm.rs b/crates/router/src/routes/gsm.rs
index 77a0cb0a645..886b24bbdb9 100644
--- a/crates/router/src/routes/gsm.rs
+++ b/crates/router/src/routes/gsm.rs
@@ -7,7 +7,10 @@ use crate::{
     core::{api_locking, gsm},
     services::{api, authentication as auth},
 };
-
+#[cfg(feature = "v1")]
+const ADMIN_API_AUTH: auth::AdminApiAuth = auth::AdminApiAuth;
+#[cfg(feature = "v2")]
+const ADMIN_API_AUTH: auth::V2AdminApiAuth = auth::V2AdminApiAuth;
 /// Gsm - Create
 ///
 /// To create a Gsm Rule
@@ -40,7 +43,7 @@ pub async fn create_gsm_rule(
         &req,
         payload,
         |state, _, payload, _| gsm::create_gsm_rule(state, payload),
-        &auth::AdminApiAuth,
+        &ADMIN_API_AUTH,
         api_locking::LockAction::NotApplicable,
     ))
     .await
@@ -77,7 +80,7 @@ pub async fn get_gsm_rule(
         &req,
         gsm_retrieve_req,
         |state, _, gsm_retrieve_req, _| gsm::retrieve_gsm_rule(state, gsm_retrieve_req),
-        &auth::AdminApiAuth,
+        &ADMIN_API_AUTH,
         api_locking::LockAction::NotApplicable,
     ))
     .await
@@ -115,7 +118,7 @@ pub async fn update_gsm_rule(
         &req,
         payload,
         |state, _, payload, _| gsm::update_gsm_rule(state, payload),
-        &auth::AdminApiAuth,
+        &ADMIN_API_AUTH,
         api_locking::LockAction::NotApplicable,
     ))
     .await
@@ -154,7 +157,7 @@ pub async fn delete_gsm_rule(
         &req,
         payload,
         |state, _, payload, _| gsm::delete_gsm_rule(state, payload),
-        &auth::AdminApiAuth,
+        &ADMIN_API_AUTH,
         api_locking::LockAction::NotApplicable,
     ))
     .await
diff --git a/crates/router/src/types/storage/revenue_recovery.rs b/crates/router/src/types/storage/revenue_recovery.rs
index 4b0dd2f2bc8..1c1a076fd01 100644
--- a/crates/router/src/types/storage/revenue_recovery.rs
+++ b/crates/router/src/types/storage/revenue_recovery.rs
@@ -1,7 +1,7 @@
 use std::fmt::Debug;
 
 use common_enums::enums;
-use common_utils::{ext_traits::ValueExt, id_type};
+use common_utils::{date_time, ext_traits::ValueExt, id_type};
 use external_services::grpc_client::{self as external_grpc_client, GrpcHeaders};
 use hyperswitch_domain_models::{
     business_profile, merchant_account, merchant_connector_account, merchant_key_store,
@@ -38,6 +38,7 @@ impl RevenueRecoveryPaymentData {
         retry_count: i32,
         payment_attempt: &PaymentAttempt,
         payment_intent: &PaymentIntent,
+        is_hard_decline: bool,
     ) -> Option<time::PrimitiveDateTime> {
         match self.retry_algorithm {
             enums::RevenueRecoveryAlgorithmType::Monitoring => {
@@ -53,13 +54,18 @@ impl RevenueRecoveryPaymentData {
                 .await
             }
             enums::RevenueRecoveryAlgorithmType::Smart => {
-                revenue_recovery::get_schedule_time_for_smart_retry(
-                    state,
-                    payment_attempt,
-                    payment_intent,
-                    retry_count,
-                )
-                .await
+                if is_hard_decline {
+                    None
+                } else {
+                    // TODO: Integrate the smart retry call to return back a schedule time
+                    revenue_recovery::get_schedule_time_for_smart_retry(
+                        state,
+                        payment_attempt,
+                        payment_intent,
+                        retry_count,
+                    )
+                    .await
+                }
             }
         }
     }

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [ ] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
Add support for hard-decline switch for revenue-recovery service. If we encounter a hard-decline we would not call the external service . This PR also adds support for gsm apis in v2

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
- hit this curl
``` curl
 curl --location 'http://localhost:8080/v2/gsm' \
--header 'Authorization: admin-api-key=test_admin' \ 
--header 'Content-Type: application/json' \
  --data '{
  "connector": "authipay",
  "flow": "revenue_recovery",
  "sub_flow": "sub_flow",
  "code": "100",
  "message": "Insuffiecient Funds",
  "status": "Failure",
  "decision": "retry",
"clear_pan_possible":false,
"step_up_possible":false,
  "error_category": "hard_decline"
}'
```
- Response ```{"connector":"authipay","flow":"revenue_recovery","sub_flow":"sub_flow","code":"100","message":"Insuffiecient Funds","status":"Failure","router_error":null,"decision":"retry","step_up_possible":false,"unified_code":null,"unified_message":null,"error_category":"hard_decline","clear_pan_possible":false,"feature":"retry","feature_data":{"retry":{"step_up_possible":false,"clear_pan_possible":false,"alternate_network_possible":false,"decision":"retry"}}}%   ```

- Db entry would be present 
<img width="1017" height="385" alt="Screenshot 2025-08-21 at 5 45 52 PM" src="https://github.com/user-attachments/assets/7f9da820-f793-4e57-81dd-cd34608979e9" />


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [ ] I formatted the code `cargo +nightly fmt --all`
- [ ] I addressed lints thrown by `cargo clippy`
- [ ] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
