From 4ece376b56549b53bd81c16fd9fdebbd0b9b1114 Mon Sep 17 00:00:00 2001 From: ThisIsMani <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 8 May 2023 15:04:50 +0530 Subject: [PATCH] feat(connector): add payment routes for dummy connector (#980) --- config/development.toml | 5 + crates/redis_interface/src/commands.rs | 26 +++++ crates/router/Cargo.toml | 3 +- crates/router/src/configs/settings.rs | 10 ++ crates/router/src/lib.rs | 6 ++ crates/router/src/routes.rs | 4 + crates/router/src/routes/app.rs | 19 ++++ crates/router/src/routes/dummy_connector.rs | 29 ++++++ .../src/routes/dummy_connector/errors.rs | 34 +++++++ .../src/routes/dummy_connector/types.rs | 98 +++++++++++++++++++ .../src/routes/dummy_connector/utils.rs | 87 ++++++++++++++++ 11 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 crates/router/src/routes/dummy_connector.rs create mode 100644 crates/router/src/routes/dummy_connector/errors.rs create mode 100644 crates/router/src/routes/dummy_connector/types.rs create mode 100644 crates/router/src/routes/dummy_connector/utils.rs diff --git a/config/development.toml b/config/development.toml index 46b0766be9..4ecd69879c 100644 --- a/config/development.toml +++ b/config/development.toml @@ -230,3 +230,8 @@ checkout = { long_lived_token = false, payment_method = "wallet" } [connector_customer] connector_list = "stripe" + +[dummy_connector] +payment_ttl = 172800 +payment_duration = 1000 +payment_tolerance = 100 diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index e000326d23..6429d2f84f 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -79,6 +79,32 @@ impl super::RedisConnectionPool { self.set_key(key, serialized.as_slice()).await } + #[instrument(level = "DEBUG", skip(self))] + pub async fn serialize_and_set_key_with_expiry( + &self, + key: &str, + value: V, + seconds: i64, + ) -> CustomResult<(), errors::RedisError> + where + V: serde::Serialize + Debug, + { + let serialized = Encode::::encode_to_vec(&value) + .change_context(errors::RedisError::JsonSerializationFailed)?; + + self.pool + .set( + key, + serialized.as_slice(), + Some(Expiration::EX(seconds)), + None, + false, + ) + .await + .into_report() + .change_context(errors::RedisError::SetExFailed) + } + #[instrument(level = "DEBUG", skip(self))] pub async fn get_key(&self, key: &str) -> CustomResult where diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index b1c60d3035..e4a9a25efc 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -24,7 +24,8 @@ accounts_cache = [] openapi = ["olap", "oltp"] vergen = ["router_env/vergen"] multiple_mca = ["api_models/multiple_mca"] -dummy_connector = [] +dummy_connector = ["api_models/dummy_connector"] +external_access_dc = ["dummy_connector"] [dependencies] diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 9a29cd23a1..5c27605007 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -67,6 +67,8 @@ pub struct Settings { pub file_upload_config: FileUploadConfig, pub tokenization: TokenizationConfig, pub connector_customer: ConnectorCustomer, + #[cfg(feature = "dummy_connector")] + pub dummy_connector: DummyConnector, } #[derive(Debug, Deserialize, Clone, Default)] @@ -93,6 +95,14 @@ where .collect()) } +#[cfg(feature = "dummy_connector")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct DummyConnector { + pub payment_ttl: i64, + pub payment_duration: u64, + pub payment_tolerance: u64, +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct PaymentMethodTokenFilter { #[serde(deserialize_with = "pm_deser")] diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index b35e503f2a..bf20ef67e4 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -95,6 +95,12 @@ pub fn mk_app( ); } + #[cfg(feature = "dummy_connector")] + { + use routes::DummyConnector; + server_app = server_app.service(DummyConnector::server(state.clone())); + } + #[cfg(any(feature = "olap", feature = "oltp"))] { server_app = server_app diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 4b73b2a7e9..f97484ca34 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -5,6 +5,8 @@ pub mod cards_info; pub mod configs; pub mod customers; pub mod disputes; +#[cfg(feature = "dummy_connector")] +pub mod dummy_connector; pub mod ephemeral_key; pub mod files; pub mod health; @@ -16,6 +18,8 @@ pub mod payouts; pub mod refunds; pub mod webhooks; +#[cfg(feature = "dummy_connector")] +pub use self::app::DummyConnector; pub use self::app::{ ApiKeys, AppState, Cards, Configs, Customers, Disputes, EphemeralKey, Files, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 970debf1c1..98ce3f4467 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1,6 +1,8 @@ use actix_web::{web, Scope}; use tokio::sync::oneshot; +#[cfg(feature = "dummy_connector")] +use super::dummy_connector::*; use super::health::*; #[cfg(feature = "olap")] use super::{admin::*, api_keys::*, disputes::*, files::*}; @@ -76,6 +78,23 @@ impl Health { } } +#[cfg(feature = "dummy_connector")] +pub struct DummyConnector; + +#[cfg(feature = "dummy_connector")] +impl DummyConnector { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/dummy-connector").app_data(web::Data::new(state)); + #[cfg(not(feature = "external_access_dc"))] + { + route = route.guard(actix_web::guard::Host("localhost")); + } + route = + route.service(web::resource("/payment").route(web::post().to(dummy_connector_payment))); + route + } +} + pub struct Payments; #[cfg(any(feature = "olap", feature = "oltp"))] diff --git a/crates/router/src/routes/dummy_connector.rs b/crates/router/src/routes/dummy_connector.rs new file mode 100644 index 0000000000..133fbb2d3e --- /dev/null +++ b/crates/router/src/routes/dummy_connector.rs @@ -0,0 +1,29 @@ +use actix_web::web; +use router_env::{instrument, tracing}; + +use self::types::DummyConnectorPaymentRequest; +use super::app; +use crate::services::{api, authentication as auth}; + +mod errors; +mod types; +mod utils; + +#[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentCreate))] +pub async fn dummy_connector_payment( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, +) -> impl actix_web::Responder { + let flow = types::Flow::DummyPaymentCreate; + let payload = json_payload.into_inner(); + api::server_wrap( + flow, + state.get_ref(), + &req, + payload, + |state, _, req| utils::payment(state, req), + &auth::NoAuth, + ) + .await +} diff --git a/crates/router/src/routes/dummy_connector/errors.rs b/crates/router/src/routes/dummy_connector/errors.rs new file mode 100644 index 0000000000..4b75bb4057 --- /dev/null +++ b/crates/router/src/routes/dummy_connector/errors.rs @@ -0,0 +1,34 @@ +#[derive(Clone, Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ErrorType { + ServerNotAvailable, +} + +#[derive(Debug, Clone, router_derive::ApiError)] +#[error(error_type_enum = ErrorType)] +pub enum DummyConnectorErrors { + #[error(error_type = ErrorType::ServerNotAvailable, code = "DC_00", message = "")] + PaymentStoringError, +} + +impl core::fmt::Display for DummyConnectorErrors { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"{{"error":{}}}"#, + serde_json::to_string(self) + .unwrap_or_else(|_| "Dummy connector error response".to_string()) + ) + } +} + +impl common_utils::errors::ErrorSwitch + for DummyConnectorErrors +{ + fn switch(&self) -> api_models::errors::types::ApiErrorResponse { + use api_models::errors::types::{ApiError, ApiErrorResponse as AER}; + match self { + Self::PaymentStoringError => AER::InternalServerError(ApiError::new("DC", 0, "", None)), + } + } +} diff --git a/crates/router/src/routes/dummy_connector/types.rs b/crates/router/src/routes/dummy_connector/types.rs new file mode 100644 index 0000000000..5685c6eb99 --- /dev/null +++ b/crates/router/src/routes/dummy_connector/types.rs @@ -0,0 +1,98 @@ +use common_utils::errors::CustomResult; +use masking::Secret; +use router_env::types::FlowMetric; +use strum::Display; + +use super::errors::DummyConnectorErrors; +use crate::services; + +#[derive(Debug, Display, Clone, PartialEq, Eq)] +pub enum Flow { + DummyPaymentCreate, +} + +impl FlowMetric for Flow {} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct DummyConnectorPaymentRequest { + pub amount: i64, + pub payment_method_data: DummyConnectorPaymentMethodData, +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub enum DummyConnectorPaymentMethodData { + Card(DummyConnectorCard), +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct DummyConnectorCard { + pub name: Secret, + pub number: Secret, + pub expiry_month: Secret, + pub expiry_year: Secret, + pub cvc: Secret, + pub complete: bool, +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] +pub struct DummyConnectorPaymentData { + pub status: String, + pub amount: i64, + pub eligible_amount: i64, + pub payemnt_method_type: String, +} + +impl DummyConnectorPaymentData { + pub fn new( + status: String, + amount: i64, + eligible_amount: i64, + payemnt_method_type: String, + ) -> Self { + Self { + status, + amount, + eligible_amount, + payemnt_method_type, + } + } +} + +#[allow(dead_code)] +pub enum DummyConnectorTransactionStatus { + Success, + InProcess, + Fail, +} + +impl std::fmt::Display for DummyConnectorTransactionStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Success => write!(f, "succeeded"), + Self::InProcess => write!(f, "processing"), + Self::Fail => write!(f, "failed"), + } + } +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DummyConnectorPaymentResponse { + pub status: String, + pub id: String, + pub amount: i64, + pub payment_method_type: String, +} + +impl DummyConnectorPaymentResponse { + pub fn new(status: String, id: String, amount: i64, payment_method_type: String) -> Self { + Self { + status, + id, + amount, + payment_method_type, + } + } +} + +pub type DummyConnectorResponse = + CustomResult, DummyConnectorErrors>; diff --git a/crates/router/src/routes/dummy_connector/utils.rs b/crates/router/src/routes/dummy_connector/utils.rs new file mode 100644 index 0000000000..a510a14685 --- /dev/null +++ b/crates/router/src/routes/dummy_connector/utils.rs @@ -0,0 +1,87 @@ +use app::AppState; +use error_stack::{IntoReport, ResultExt}; +use masking::ExposeInterface; +use rand::Rng; +use redis_interface::RedisConnectionPool; +use tokio::time as tokio; + +use super::{errors, types}; +use crate::{connection, core::errors as api_errors, logger, routes::app, services::api}; + +pub async fn tokio_mock_sleep(delay: u64, tolerance: u64) { + let mut rng = rand::thread_rng(); + let effective_delay = rng.gen_range((delay - tolerance)..(delay + tolerance)); + tokio::sleep(tokio::Duration::from_millis(effective_delay)).await +} + +pub async fn payment( + state: &AppState, + req: types::DummyConnectorPaymentRequest, +) -> types::DummyConnectorResponse { + let payment_id = format!("dummy_{}", uuid::Uuid::new_v4()); + match req.payment_method_data { + types::DummyConnectorPaymentMethodData::Card(card) => { + let card_number: String = card.number.expose(); + tokio_mock_sleep( + state.conf.dummy_connector.payment_duration, + state.conf.dummy_connector.payment_tolerance, + ) + .await; + + if card_number == "4111111111111111" || card_number == "4242424242424242" { + let key_for_dummy_payment = format!("p_{}", payment_id); + + let redis_conn = connection::redis_connection(&state.conf).await; + store_payment_data( + &redis_conn, + key_for_dummy_payment, + types::DummyConnectorPaymentData::new( + types::DummyConnectorTransactionStatus::Success.to_string(), + req.amount, + req.amount, + "card".to_string(), + ), + state.conf.dummy_connector.payment_ttl, + ) + .await?; + + Ok(api::ApplicationResponse::Json( + types::DummyConnectorPaymentResponse::new( + types::DummyConnectorTransactionStatus::Success.to_string(), + payment_id, + req.amount, + "card".to_string(), + ), + )) + } else { + Ok(api::ApplicationResponse::Json( + types::DummyConnectorPaymentResponse::new( + types::DummyConnectorTransactionStatus::Fail.to_string(), + payment_id, + req.amount, + "card".to_string(), + ), + )) + } + } + } +} + +async fn store_payment_data( + redis_conn: &RedisConnectionPool, + key: String, + payment_data: types::DummyConnectorPaymentData, + ttl: i64, +) -> Result<(), error_stack::Report> { + redis_conn + .serialize_and_set_key_with_expiry(&key, payment_data, ttl) + .await + .map_err(|error| { + logger::error!(dummy_connector_payment_storage_error=?error); + api_errors::StorageError::KVError + }) + .into_report() + .change_context(errors::DummyConnectorErrors::PaymentStoringError) + .attach_printable("Failed to add data in redis")?; + Ok(()) +}