diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 78f34b4b87fa..23e7c9dc706a 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,4 +1,5 @@ pub mod customer; +pub mod gsm; pub mod payment; #[cfg(feature = "payouts")] pub mod payouts; diff --git a/crates/api_models/src/events/gsm.rs b/crates/api_models/src/events/gsm.rs new file mode 100644 index 000000000000..d984ae1ff698 --- /dev/null +++ b/crates/api_models/src/events/gsm.rs @@ -0,0 +1,33 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::gsm; + +impl ApiEventMetric for gsm::GsmCreateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmUpdateRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmRetrieveRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmDeleteRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} + +impl ApiEventMetric for gsm::GsmDeleteResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs new file mode 100644 index 000000000000..6bd8fd99dd93 --- /dev/null +++ b/crates/api_models/src/gsm.rs @@ -0,0 +1,75 @@ +use crate::enums; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmCreateRequest { + pub connector: enums::Connector, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: String, + pub router_error: Option, + pub decision: GsmDecision, + pub step_up_possible: bool, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmRetrieveRequest { + pub connector: enums::Connector, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, +} + +#[derive( + Default, + Clone, + Copy, + Debug, + strum::Display, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::EnumString, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GsmDecision { + Retry, + Requeue, + #[default] + DoDefault, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmUpdateRequest { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, + pub status: Option, + pub router_error: Option, + pub decision: Option, + pub step_up_possible: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GsmDeleteRequest { + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, + pub message: String, +} + +#[derive(Debug, serde::Serialize)] +pub struct GsmDeleteResponse { + pub gsm_rule_delete: bool, + pub connector: String, + pub flow: String, + pub sub_flow: String, + pub code: String, +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 9fff344b9ff7..5da916b14817 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -11,6 +11,7 @@ pub mod ephemeral_key; pub mod errors; pub mod events; pub mod files; +pub mod gsm; pub mod mandates; pub mod organization; pub mod payment_methods; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 1d487364031d..8c52f6c36d63 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -41,6 +41,7 @@ pub enum ApiEventsType { Routing, ResourceListAPI, PaymentRedirectionResponse, + Gsm, // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, } diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs index d5b3122c7806..2e824758aa5a 100644 --- a/crates/diesel_models/src/gsm.rs +++ b/crates/diesel_models/src/gsm.rs @@ -1,6 +1,9 @@ //! Gateway status mapping -use common_utils::custom_serde; +use common_utils::{ + custom_serde, + events::{ApiEventMetric, ApiEventsType}, +}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; use time::PrimitiveDateTime; @@ -95,3 +98,9 @@ impl From for GatewayStatusMapperUpdateInternal { } } } + +impl ApiEventMetric for GatewayStatusMap { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Gsm) + } +} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index d87ff64003b4..817fafdae520 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -8,6 +8,7 @@ pub mod customers; pub mod disputes; pub mod errors; pub mod files; +pub mod gsm; pub mod mandate; pub mod metrics; pub mod payment_link; diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs new file mode 100644 index 000000000000..d25860674570 --- /dev/null +++ b/crates/router/src/core/gsm.rs @@ -0,0 +1,137 @@ +use api_models::gsm as gsm_api_types; +use diesel_models::gsm as storage; +use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, tracing}; + +use crate::{ + core::{ + errors, + errors::{RouterResponse, StorageErrorExt}, + }, + db::gsm::GsmInterface, + services, + types::{self, transformers::ForeignInto}, + AppState, +}; + +#[instrument(skip_all)] +pub async fn create_gsm_rule( + state: AppState, + gsm_rule: gsm_api_types::GsmCreateRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + GsmInterface::add_gsm_rule(db, gsm_rule.foreign_into()) + .await + .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { + message: "GSM with given key already exists in our records".to_string(), + }) + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn retrieve_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmRetrieveRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmRetrieveRequest { + connector, + flow, + sub_flow, + code, + message, + } = gsm_request; + GsmInterface::find_gsm_rule(db, connector.to_string(), flow, sub_flow, code, message) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn update_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmUpdateRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmUpdateRequest { + connector, + flow, + sub_flow, + code, + message, + decision, + status, + router_error, + step_up_possible, + } = gsm_request; + GsmInterface::update_gsm_rule( + db, + connector.to_string(), + flow, + sub_flow, + code, + message, + storage::GatewayStatusMappingUpdate { + decision: decision.map(|d| d.to_string()), + status, + router_error: Some(router_error), + step_up_possible, + }, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .attach_printable("Failed while updating Gsm rule") + .map(services::ApplicationResponse::Json) +} + +#[instrument(skip_all)] +pub async fn delete_gsm_rule( + state: AppState, + gsm_request: gsm_api_types::GsmDeleteRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let gsm_api_types::GsmDeleteRequest { + connector, + flow, + sub_flow, + code, + message, + } = gsm_request; + match GsmInterface::delete_gsm_rule( + db, + connector.to_string(), + flow.to_owned(), + sub_flow.to_owned(), + code.to_owned(), + message.to_owned(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "GSM with given key does not exist in our records".to_string(), + }) + .attach_printable("Failed while Deleting Gsm rule") + { + Ok(is_deleted) => { + if is_deleted { + Ok(services::ApplicationResponse::Json( + gsm_api_types::GsmDeleteResponse { + gsm_rule_delete: true, + connector, + flow, + sub_flow, + code, + }, + )) + } else { + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Failed while Deleting Gsm rule, got response as false") + } + } + Err(err) => Err(err), + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 21ebfc06137b..38efe8b75134 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -142,6 +142,7 @@ pub fn mk_app( .service(routes::Files::server(state.clone())) .service(routes::Disputes::server(state.clone())) .service(routes::Routing::server(state.clone())) + .service(routes::Gsm::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 38f95c4cdda8..47b9f23cf8cb 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -10,6 +10,7 @@ pub mod disputes; pub mod dummy_connector; pub mod ephemeral_key; pub mod files; +pub mod gsm; pub mod health; pub mod lock_utils; pub mod mandates; @@ -36,7 +37,7 @@ pub use self::app::Routing; pub use self::app::Verify; pub use self::app::{ ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, + Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Refunds, Webhooks, }; #[cfg(feature = "stripe")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 268b2ed703bf..ec87fcdc3900 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -19,7 +19,7 @@ use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*}; +use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*}; use super::{cache::*, health::*, payment_link::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; @@ -666,6 +666,20 @@ impl BusinessProfile { } } +pub struct Gsm; + +#[cfg(feature = "olap")] +impl Gsm { + pub fn server(state: AppState) -> Scope { + web::scope("/gsm") + .app_data(web::Data::new(state)) + .service(web::resource("").route(web::post().to(create_gsm_rule))) + .service(web::resource("/get").route(web::post().to(get_gsm_rule))) + .service(web::resource("/update").route(web::post().to(update_gsm_rule))) + .service(web::resource("/delete").route(web::post().to(delete_gsm_rule))) + } +} + #[cfg(all(feature = "olap", feature = "kms"))] pub struct Verify; diff --git a/crates/router/src/routes/gsm.rs b/crates/router/src/routes/gsm.rs new file mode 100644 index 000000000000..02d943792dba --- /dev/null +++ b/crates/router/src/routes/gsm.rs @@ -0,0 +1,93 @@ +use actix_web::{web, HttpRequest, Responder}; +use api_models::gsm as gsm_api_types; +use router_env::{instrument, tracing, Flow}; + +use super::app::AppState; +use crate::{ + core::{api_locking, gsm}, + services::{api, authentication as auth}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleCreate))] +pub async fn create_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleCreate; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, _, payload| gsm::create_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleRetrieve))] +pub async fn get_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let gsm_retrieve_req = json_payload.into_inner(); + let flow = Flow::GsmRuleRetrieve; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + gsm_retrieve_req, + |state, _, gsm_retrieve_req| gsm::retrieve_gsm_rule(state, gsm_retrieve_req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleUpdate))] +pub async fn update_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleUpdate; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, _, payload| gsm::update_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::GsmRuleDelete))] +pub async fn delete_gsm_rule( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + + let flow = Flow::GsmRuleDelete; + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, _, payload| gsm::delete_gsm_rule(state, payload), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 14614268d79d..4e6fc1870f56 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -23,6 +23,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Gsm, } impl From for ApiIdentifier { @@ -129,6 +130,10 @@ impl From for ApiIdentifier { Flow::Verification => Self::Verification, Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, + Flow::GsmRuleCreate + | Flow::GsmRuleRetrieve + | Flow::GsmRuleUpdate + | Flow::GsmRuleDelete => Self::Gsm, } } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 8f08ce062560..f2e86a4bf335 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -1193,3 +1193,5 @@ impl } } } + +pub type GsmResponse = storage::GatewayStatusMap; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 83ca0d014dc8..1cd016de18e6 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1,6 +1,6 @@ // use actix_web::HttpMessage; use actix_web::http::header::HeaderMap; -use api_models::{enums as api_enums, payments, routing::ConnectorSelection}; +use api_models::{enums as api_enums, gsm as gsm_api_types, payments, routing::ConnectorSelection}; use common_utils::{ consts::X_HS_LATENCY, crypto::Encryptable, @@ -1031,3 +1031,19 @@ impl ForeignFrom } } } + +impl ForeignFrom for storage::GatewayStatusMappingNew { + fn foreign_from(value: gsm_api_types::GsmCreateRequest) -> Self { + Self { + connector: value.connector.to_string(), + flow: value.flow, + sub_flow: value.sub_flow, + code: value.code, + message: value.message, + decision: value.decision.to_string(), + status: value.status, + router_error: value.router_error, + step_up_possible: value.step_up_possible, + } + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 9822432115b0..0c9751aee440 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -235,6 +235,14 @@ pub enum Flow { BusinessProfileList, /// Different verification flows Verification, + /// Gsm Rule Creation flow + GsmRuleCreate, + /// Gsm Rule Retrieve flow + GsmRuleRetrieve, + /// Gsm Rule Update flow + GsmRuleUpdate, + /// Gsm Rule Delete flow + GsmRuleDelete, } ///