diff --git a/config/config.example.toml b/config/config.example.toml index bae450663a..dc2dfa1bf1 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -306,6 +306,7 @@ refund_duration = 1000 # Fake delay duration for dummy connector refun refund_tolerance = 100 # Fake delay tolerance for dummy connector refund refund_retrieve_duration = 500 # Fake delay duration for dummy connector refund sync refund_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector refund sync +authorize_ttl = 36000 # Time to live for dummy connector authorize request in redis [mandates.supported_payment_methods] card.credit = {connector_list = "stripe,adyen"} # Mandate supported payment method type and connector for card diff --git a/config/development.toml b/config/development.toml index 11c268dca3..4f4b6d0ac9 100644 --- a/config/development.toml +++ b/config/development.toml @@ -337,6 +337,7 @@ refund_duration = 1000 refund_tolerance = 100 refund_retrieve_duration = 500 refund_retrieve_tolerance = 100 +authorize_ttl = 36000 [delayed_session_response] connectors_with_delayed_session_response = "trustpay" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index f142e31a15..314c96d874 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -202,6 +202,7 @@ refund_duration = 1000 refund_tolerance = 100 refund_retrieve_duration = 500 refund_retrieve_tolerance = 100 +authorize_ttl = 36000 [payouts] payout_eligibility = true diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f1e611ebd8..cffbfe562d 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -146,6 +146,7 @@ pub struct DummyConnector { pub refund_tolerance: u64, pub refund_retrieve_duration: u64, pub refund_retrieve_tolerance: u64, + pub authorize_ttl: i64, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index 3506e2ecb3..e9f52dbaba 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -4,7 +4,6 @@ use std::fmt::Debug; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; -use transformers as dummyconnector; use super::utils::RefundsRequestData; use crate::{ @@ -74,16 +73,7 @@ where impl ConnectorCommon for DummyConnector { fn id(&self) -> &'static str { - match T { - 1 => "phonypay", - 2 => "fauxpay", - 3 => "pretendpay", - 4 => "stripe_test", - 5 => "adyen_test", - 6 => "checkout_test", - 7 => "paypal_test", - _ => "phonypay", - } + Into::::into(T).get_dummy_connector_id() } fn common_get_content_type(&self) -> &'static str { @@ -94,7 +84,7 @@ impl ConnectorCommon for DummyConnector { &self, val: &types::ConnectorAuthType, ) -> Result<(), error_stack::Report> { - dummyconnector::DummyConnectorAuthType::try_from(val)?; + transformers::DummyConnectorAuthType::try_from(val)?; Ok(()) } @@ -106,7 +96,7 @@ impl ConnectorCommon for DummyConnector { &self, auth_type: &types::ConnectorAuthType, ) -> CustomResult)>, errors::ConnectorError> { - let auth = dummyconnector::DummyConnectorAuthType::try_from(auth_type) + let auth = transformers::DummyConnectorAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![( headers::AUTHORIZATION.to_string(), @@ -118,7 +108,7 @@ impl ConnectorCommon for DummyConnector { &self, res: Response, ) -> CustomResult { - let response: dummyconnector::DummyConnectorErrorResponse = res + let response: transformers::DummyConnectorErrorResponse = res .response .parse_struct("DummyConnectorErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -174,10 +164,12 @@ impl ) -> CustomResult { match req.payment_method { enums::PaymentMethod::Card => Ok(format!("{}/payment", self.base_url(connectors))), + enums::PaymentMethod::Wallet => Ok(format!("{}/payment", self.base_url(connectors))), + enums::PaymentMethod::PayLater => Ok(format!("{}/payment", self.base_url(connectors))), _ => Err(error_stack::report!(errors::ConnectorError::NotSupported { message: format!("The payment method {} is not supported", req.payment_method), - connector: "dummyconnector", - payment_experience: api::enums::PaymentExperience::InvokeSdkClient.to_string(), + connector: Into::::into(T).get_dummy_connector_id(), + payment_experience: api::enums::PaymentExperience::RedirectToUrl.to_string(), })), } } @@ -186,12 +178,12 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, ) -> CustomResult, errors::ConnectorError> { - let connector_request = dummyconnector::DummyConnectorPaymentsRequest::try_from(req)?; + let connector_request = transformers::DummyConnectorPaymentsRequest::::try_from(req)?; let dummmy_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + &connector_request, + utils::Encode::>::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(dummmy_payments_request)) } @@ -220,10 +212,11 @@ impl data: &types::PaymentsAuthorizeRouterData, res: Response, ) -> CustomResult { - let response: dummyconnector::PaymentsResponse = res + let response: transformers::PaymentsResponse = res .response - .parse_struct("DummyConnector PaymentsAuthorizeResponse") + .parse_struct("DummyConnector PaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + println!("handle_response: {:#?}", response); types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -297,7 +290,7 @@ impl data: &types::PaymentsSyncRouterData, res: Response, ) -> CustomResult { - let response: dummyconnector::PaymentsResponse = res + let response: transformers::PaymentsResponse = res .response .parse_struct("dummyconnector PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -370,9 +363,9 @@ impl data: &types::PaymentsCaptureRouterData, res: Response, ) -> CustomResult { - let response: dummyconnector::PaymentsResponse = res + let response: transformers::PaymentsResponse = res .response - .parse_struct("DummyConnector PaymentsCaptureResponse") + .parse_struct("transformers PaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -427,10 +420,10 @@ impl ConnectorIntegration, ) -> CustomResult, errors::ConnectorError> { - let connector_request = dummyconnector::DummyConnectorRefundRequest::try_from(req)?; + let connector_request = transformers::DummyConnectorRefundRequest::try_from(req)?; let dummmy_refund_request = types::RequestBody::log_and_get_request_body( &connector_request, - utils::Encode::::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(dummmy_refund_request)) @@ -458,9 +451,9 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: dummyconnector::RefundResponse = res + let response: transformers::RefundResponse = res .response - .parse_struct("dummyconnector RefundResponse") + .parse_struct("transformers RefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -527,9 +520,9 @@ impl ConnectorIntegration CustomResult { - let response: dummyconnector::RefundResponse = res + let response: transformers::RefundResponse = res .response - .parse_struct("dummyconnector RefundSyncResponse") + .parse_struct("transformers RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, diff --git a/crates/router/src/connector/dummyconnector/transformers.rs b/crates/router/src/connector/dummyconnector/transformers.rs index adb917e3c1..f7081c870a 100644 --- a/crates/router/src/connector/dummyconnector/transformers.rs +++ b/crates/router/src/connector/dummyconnector/transformers.rs @@ -1,56 +1,173 @@ use diesel_models::enums::Currency; use masking::Secret; use serde::{Deserialize, Serialize}; +use url::Url; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, core::errors, + services, types::{self, api, storage::enums}, }; +#[derive(Debug, Serialize, strum::Display, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DummyConnectors { + #[serde(rename = "phonypay")] + #[strum(serialize = "phonypay")] + PhonyPay, + #[serde(rename = "fauxpay")] + #[strum(serialize = "fauxpay")] + FauxPay, + #[serde(rename = "pretendpay")] + #[strum(serialize = "pretendpay")] + PretendPay, + StripeTest, + AdyenTest, + CheckoutTest, + PaypalTest, +} + +impl DummyConnectors { + pub fn get_dummy_connector_id(self) -> &'static str { + match self { + Self::PhonyPay => "phonypay", + Self::FauxPay => "fauxpay", + Self::PretendPay => "pretendpay", + Self::StripeTest => "stripe_test", + Self::AdyenTest => "adyen_test", + Self::CheckoutTest => "checkout_test", + Self::PaypalTest => "paypal_test", + } + } +} + +impl From for DummyConnectors { + fn from(value: u8) -> Self { + match value { + 1 => Self::PhonyPay, + 2 => Self::FauxPay, + 3 => Self::PretendPay, + 4 => Self::StripeTest, + 5 => Self::AdyenTest, + 6 => Self::CheckoutTest, + 7 => Self::PaypalTest, + _ => Self::PhonyPay, + } + } +} + #[derive(Debug, Serialize, Eq, PartialEq)] -pub struct DummyConnectorPaymentsRequest { +pub struct DummyConnectorPaymentsRequest { amount: i64, currency: Currency, payment_method_data: PaymentMethodData, + return_url: Option, + connector: DummyConnectors, } -#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] pub enum PaymentMethodData { Card(DummyConnectorCard), + Wallet(DummyConnectorWallet), + PayLater(DummyConnectorPayLater), } -#[derive(Debug, Serialize, Eq, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct DummyConnectorCard { name: Secret, number: cards::CardNumber, expiry_month: Secret, expiry_year: Secret, cvc: Secret, - complete: bool, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for DummyConnectorPaymentsRequest { +impl From for DummyConnectorCard { + fn from(value: api_models::payments::Card) -> Self { + Self { + name: value.card_holder_name, + number: value.card_number, + expiry_month: value.card_exp_month, + expiry_year: value.card_exp_year, + cvc: value.card_cvc, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum DummyConnectorWallet { + GooglePay, + Paypal, + WeChatPay, + MbWay, + AliPay, + AliPayHK, +} + +impl TryFrom for DummyConnectorWallet { + type Error = error_stack::Report; + fn try_from(value: api_models::payments::WalletData) -> Result { + match value { + api_models::payments::WalletData::GooglePayRedirect(_) => Ok(Self::GooglePay), + api_models::payments::WalletData::PaypalRedirect(_) => Ok(Self::Paypal), + api_models::payments::WalletData::WeChatPay(_) => Ok(Self::WeChatPay), + api_models::payments::WalletData::MbWayRedirect(_) => Ok(Self::MbWay), + api_models::payments::WalletData::AliPayRedirect(_) => Ok(Self::AliPay), + api_models::payments::WalletData::AliPayHkRedirect(_) => Ok(Self::AliPayHK), + _ => Err(errors::ConnectorError::NotImplemented("Dummy wallet".to_string()).into()), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum DummyConnectorPayLater { + Klarna, + Affirm, + AfterPayClearPay, +} + +impl TryFrom for DummyConnectorPayLater { + type Error = error_stack::Report; + fn try_from(value: api_models::payments::PayLaterData) -> Result { + match value { + api_models::payments::PayLaterData::KlarnaRedirect { .. } => Ok(Self::Klarna), + api_models::payments::PayLaterData::AffirmRedirect {} => Ok(Self::Affirm), + api_models::payments::PayLaterData::AfterpayClearpayRedirect { .. } => { + Ok(Self::AfterPayClearPay) + } + _ => Err(errors::ConnectorError::NotImplemented("Dummy pay later".to_string()).into()), + } + } +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> + for DummyConnectorPaymentsRequest +{ type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - match item.request.payment_method_data.clone() { - api::PaymentMethodData::Card(req_card) => { - let card = DummyConnectorCard { - name: req_card.card_holder_name, - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.request.is_auto_capture()?, - }; - Ok(Self { - amount: item.request.amount, - currency: item.request.currency, - payment_method_data: PaymentMethodData::Card(card), - }) + let payment_method_data: Result = match item + .request + .payment_method_data + { + api::PaymentMethodData::Card(ref req_card) => { + Ok(PaymentMethodData::Card(req_card.clone().into())) } + api::PaymentMethodData::Wallet(ref wallet_data) => { + Ok(PaymentMethodData::Wallet(wallet_data.clone().try_into()?)) + } + api::PaymentMethodData::PayLater(ref pay_later_data) => Ok( + PaymentMethodData::PayLater(pay_later_data.clone().try_into()?), + ), _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), - } + }; + Ok(Self { + amount: item.request.amount, + currency: item.request.currency, + payment_method_data: payment_method_data?, + return_url: item.request.router_return_url.clone(), + connector: Into::::into(T), + }) } } @@ -86,19 +203,28 @@ impl From for enums::AttemptStatus { match item { DummyConnectorPaymentStatus::Succeeded => Self::Charged, DummyConnectorPaymentStatus::Failed => Self::Failure, - DummyConnectorPaymentStatus::Processing => Self::Authorizing, + DummyConnectorPaymentStatus::Processing => Self::AuthenticationPending, } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PaymentsResponse { status: DummyConnectorPaymentStatus, id: String, amount: i64, currency: Currency, created: String, - payment_method_type: String, + payment_method_type: PaymentMethodType, + next_action: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum PaymentMethodType { + Card, + Wallet(DummyConnectorWallet), + PayLater(DummyConnectorPayLater), } impl TryFrom> @@ -108,11 +234,18 @@ impl TryFrom, ) -> Result { + let redirection_data = item + .response + .next_action + .and_then(|redirection_data| redirection_data.get_url()) + .map(|redirection_url| { + services::RedirectForm::from((redirection_url, services::Method::Get)) + }); Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: None, + redirection_data, mandate_reference: None, connector_metadata: None, network_txn_id: None, @@ -123,6 +256,20 @@ impl TryFrom Option { + match self { + Self::RedirectToUrl(redirect_to_url) => Some(redirect_to_url.to_owned()), + } + } +} + // REFUND : // Type definition for RefundRequest #[derive(Default, Debug, Serialize)] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 219d86422d..acb1950e30 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -118,12 +118,13 @@ 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)); + let mut routes_with_restricted_access = web::scope(""); #[cfg(not(feature = "external_access_dc"))] { - route = route.guard(actix_web::guard::Host("localhost")); + routes_with_restricted_access = + routes_with_restricted_access.guard(actix_web::guard::Host("localhost")); } - route = route + routes_with_restricted_access = routes_with_restricted_access .service(web::resource("/payment").route(web::post().to(dummy_connector_payment))) .service( web::resource("/payments/{payment_id}") @@ -136,7 +137,17 @@ impl DummyConnector { web::resource("/refunds/{refund_id}") .route(web::get().to(dummy_connector_refund_data)), ); - route + web::scope("/dummy-connector") + .app_data(web::Data::new(state)) + .service( + web::resource("/authorize/{attempt_id}") + .route(web::get().to(dummy_connector_authorize_payment)), + ) + .service( + web::resource("/complete/{attempt_id}") + .route(web::get().to(dummy_connector_complete_payment)), + ) + .service(routes_with_restricted_access) } } diff --git a/crates/router/src/routes/dummy_connector.rs b/crates/router/src/routes/dummy_connector.rs index 457501968a..e21f9d4dd9 100644 --- a/crates/router/src/routes/dummy_connector.rs +++ b/crates/router/src/routes/dummy_connector.rs @@ -4,24 +4,70 @@ use router_env::{instrument, tracing}; use super::app; use crate::services::{api, authentication as auth}; +mod consts; +mod core; mod errors; mod types; mod utils; +#[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentCreate))] +pub async fn dummy_connector_authorize_payment( + state: web::Data, + req: actix_web::HttpRequest, + path: web::Path, +) -> impl actix_web::Responder { + let flow = types::Flow::DummyPaymentAuthorize; + let attempt_id = path.into_inner(); + let payload = types::DummyConnectorPaymentConfirmRequest { attempt_id }; + api::server_wrap( + flow, + state.get_ref(), + &req, + payload, + |state, _, req| core::payment_authorize(state, req), + &auth::NoAuth, + ) + .await +} + +#[instrument(skip_all, fields(flow = ?types::Flow::DummyPaymentCreate))] +pub async fn dummy_connector_complete_payment( + state: web::Data, + req: actix_web::HttpRequest, + path: web::Path, + json_payload: web::Query, +) -> impl actix_web::Responder { + let flow = types::Flow::DummyPaymentComplete; + let attempt_id = path.into_inner(); + let payload = types::DummyConnectorPaymentCompleteRequest { + attempt_id, + confirm: json_payload.confirm, + }; + api::server_wrap( + flow, + state.get_ref(), + &req, + payload, + |state, _, req| core::payment_complete(state, req), + &auth::NoAuth, + ) + .await +} + #[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(); + let flow = types::Flow::DummyPaymentCreate; api::server_wrap( flow, state.get_ref(), &req, payload, - |state, _, req| utils::payment(state, req), + |state, _, req| core::payment(state, req), &auth::NoAuth, ) .await @@ -41,7 +87,7 @@ pub async fn dummy_connector_payment_data( state.get_ref(), &req, payload, - |state, _, req| utils::payment_data(state, req), + |state, _, req| core::payment_data(state, req), &auth::NoAuth, ) .await @@ -62,7 +108,7 @@ pub async fn dummy_connector_refund( state.get_ref(), &req, payload, - |state, _, req| utils::refund_payment(state, req), + |state, _, req| core::refund_payment(state, req), &auth::NoAuth, ) .await @@ -82,7 +128,7 @@ pub async fn dummy_connector_refund_data( state.get_ref(), &req, payload, - |state, _, req| utils::refund_data(state, req), + |state, _, req| core::refund_data(state, req), &auth::NoAuth, ) .await diff --git a/crates/router/src/routes/dummy_connector/consts.rs b/crates/router/src/routes/dummy_connector/consts.rs new file mode 100644 index 0000000000..3892cedfde --- /dev/null +++ b/crates/router/src/routes/dummy_connector/consts.rs @@ -0,0 +1,97 @@ +pub const PAYMENT_ID_PREFIX: &str = "dummy_pay"; +pub const ATTEMPT_ID_PREFIX: &str = "dummy_attempt"; +pub const REFUND_ID_PREFIX: &str = "dummy_ref"; +pub const DEFAULT_RETURN_URL: &str = "https://app.hyperswitch.io/"; +pub const THREE_DS_CSS: &str = r#" + @import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap'); + body { + font-family: Inter; + background-image: url('https://app.hyperswitch.io/images/hyperswitchImages/PostLoginBackground.svg'); + display: flex; + background-size: cover; + height: 100%; + padding-top: 3rem; + margin: 0; + align-items: center; + flex-direction: column; + } + .authorize { + color: white; + background-color: #006DF9; + border: 1px solid #006DF9; + box-sizing: border-box; + margin: 1.5rem 0.5rem 1rem 0; + border-radius: 0.25rem; + padding: 0.75rem 1rem; + font-weight: 500; + font-size: 1.1rem; + } + .authorize:hover { + background-color: #0099FF; + border: 1px solid #0099FF; + cursor: pointer; + } + .reject { + background-color: #F7F7F7; + color: black; + border: 1px solid #E8E8E8; + box-sizing: border-box; + border-radius: 0.25rem; + margin-left: 0.5rem; + font-size: 1.1rem; + padding: 0.75rem 1rem; + font-weight: 500; + } + .reject:hover { + cursor: pointer; + background-color: #E8E8E8; + border: 1px solid #E8E8E8; + } + .container { + background-color: white; + width: 33rem; + margin: 1rem 0; + border: 1px solid #E8E8E8; + color: black; + border-radius: 0.25rem; + padding: 1rem 1.4rem 1rem 1.4rem; + font-family: Inter; + } + .container p { + font-weight: 400; + margin-top: 0.5rem 1rem 0.5rem 0.5rem; + color: #151A1F; + opacity: 0.5; + } + b { + font-weight: 600; + } + .disclaimer { + font-size: 1.25rem; + font-weight: 500 !important; + margin-top: 0.5rem !important; + margin-bottom: 0.5rem; + opacity: 1 !important; + } + .heading { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin-bottom: 1rem; + } + .logo { + width: 8rem; + } + .payment_details { + height: 2rem; + display: flex; + margin: 1rem 0 2rem 0; + } + .border { + border-top: 1px dashed #151a1f80; + height: 1px; + margin: 0 1rem; + margin-top: 1rem; + width: 20%; + }"#; diff --git a/crates/router/src/routes/dummy_connector/core.rs b/crates/router/src/routes/dummy_connector/core.rs new file mode 100644 index 0000000000..50b91fab0c --- /dev/null +++ b/crates/router/src/routes/dummy_connector/core.rs @@ -0,0 +1,205 @@ +use app::AppState; +use common_utils::generate_id_with_default_len; +use error_stack::ResultExt; + +use super::{errors, types, utils}; +use crate::{ + routes::{app, dummy_connector::consts}, + services::api, + utils::OptionExt, +}; + +pub async fn payment( + state: &AppState, + req: types::DummyConnectorPaymentRequest, +) -> types::DummyConnectorResponse { + utils::tokio_mock_sleep( + state.conf.dummy_connector.payment_duration, + state.conf.dummy_connector.payment_tolerance, + ) + .await; + + let payment_attempt: types::DummyConnectorPaymentAttempt = req.into(); + let payment_data = + types::DummyConnectorPaymentData::process_payment_attempt(state, payment_attempt)?; + + utils::store_data_in_redis( + state, + payment_data.attempt_id.clone(), + payment_data.payment_id.clone(), + state.conf.dummy_connector.authorize_ttl, + ) + .await?; + utils::store_data_in_redis( + state, + payment_data.payment_id.clone(), + payment_data.clone(), + state.conf.dummy_connector.payment_ttl, + ) + .await?; + Ok(api::ApplicationResponse::Json(payment_data.into())) +} + +pub async fn payment_data( + state: &AppState, + req: types::DummyConnectorPaymentRetrieveRequest, +) -> types::DummyConnectorResponse { + utils::tokio_mock_sleep( + state.conf.dummy_connector.payment_retrieve_duration, + state.conf.dummy_connector.payment_retrieve_tolerance, + ) + .await; + + let payment_data = utils::get_payment_data_from_payment_id(state, req.payment_id).await?; + Ok(api::ApplicationResponse::Json(payment_data.into())) +} + +pub async fn payment_authorize( + state: &AppState, + req: types::DummyConnectorPaymentConfirmRequest, +) -> types::DummyConnectorResponse { + let payment_data = utils::get_payment_data_by_attempt_id(state, req.attempt_id.clone()).await; + + if let Ok(payment_data_inner) = payment_data { + let return_url = format!( + "{}/dummy-connector/complete/{}", + state.conf.server.base_url, req.attempt_id + ); + Ok(api::ApplicationResponse::FileData(( + utils::get_authorize_page(payment_data_inner, return_url) + .as_bytes() + .to_vec(), + mime::TEXT_HTML, + ))) + } else { + Ok(api::ApplicationResponse::FileData(( + utils::get_expired_page().as_bytes().to_vec(), + mime::TEXT_HTML, + ))) + } +} + +pub async fn payment_complete( + state: &AppState, + req: types::DummyConnectorPaymentCompleteRequest, +) -> types::DummyConnectorResponse<()> { + let payment_data = utils::get_payment_data_by_attempt_id(state, req.attempt_id.clone()).await; + + let payment_status = if req.confirm { + types::DummyConnectorStatus::Succeeded + } else { + types::DummyConnectorStatus::Failed + }; + + let redis_conn = state.store.get_redis_conn(); + let _ = redis_conn.delete_key(req.attempt_id.as_str()).await; + + if let Ok(payment_data) = payment_data { + let updated_payment_data = types::DummyConnectorPaymentData { + status: payment_status, + next_action: None, + ..payment_data + }; + utils::store_data_in_redis( + state, + updated_payment_data.payment_id.clone(), + updated_payment_data.clone(), + state.conf.dummy_connector.payment_ttl, + ) + .await?; + return Ok(api::ApplicationResponse::JsonForRedirection( + api_models::payments::RedirectionResponse { + return_url: String::new(), + params: vec![], + return_url_with_query_params: updated_payment_data + .return_url + .unwrap_or(consts::DEFAULT_RETURN_URL.to_string()), + http_method: "GET".to_string(), + headers: vec![], + }, + )); + } + Ok(api::ApplicationResponse::JsonForRedirection( + api_models::payments::RedirectionResponse { + return_url: String::new(), + params: vec![], + return_url_with_query_params: consts::DEFAULT_RETURN_URL.to_string(), + http_method: "GET".to_string(), + headers: vec![], + }, + )) +} + +pub async fn refund_payment( + state: &AppState, + req: types::DummyConnectorRefundRequest, +) -> types::DummyConnectorResponse { + utils::tokio_mock_sleep( + state.conf.dummy_connector.refund_duration, + state.conf.dummy_connector.refund_tolerance, + ) + .await; + + let payment_id = req + .payment_id + .get_required_value("payment_id") + .change_context(errors::DummyConnectorErrors::MissingRequiredField { + field_name: "payment_id", + })?; + + let mut payment_data = + utils::get_payment_data_from_payment_id(state, payment_id.clone()).await?; + + payment_data.is_eligible_for_refund(req.amount)?; + + let refund_id = generate_id_with_default_len(consts::REFUND_ID_PREFIX); + payment_data.eligible_amount -= req.amount; + + utils::store_data_in_redis( + state, + payment_id, + payment_data.to_owned(), + state.conf.dummy_connector.payment_ttl, + ) + .await?; + + let refund_data = types::DummyConnectorRefundResponse::new( + types::DummyConnectorStatus::Succeeded, + refund_id.to_owned(), + payment_data.currency, + common_utils::date_time::now(), + payment_data.amount, + req.amount, + ); + + utils::store_data_in_redis( + state, + refund_id, + refund_data.to_owned(), + state.conf.dummy_connector.refund_ttl, + ) + .await?; + Ok(api::ApplicationResponse::Json(refund_data)) +} + +pub async fn refund_data( + state: &AppState, + req: types::DummyConnectorRefundRetrieveRequest, +) -> types::DummyConnectorResponse { + let refund_id = req.refund_id; + utils::tokio_mock_sleep( + state.conf.dummy_connector.refund_retrieve_duration, + state.conf.dummy_connector.refund_retrieve_tolerance, + ) + .await; + + let redis_conn = state.store.get_redis_conn(); + let refund_data = redis_conn + .get_and_deserialize_key::( + refund_id.as_str(), + "DummyConnectorRefundResponse", + ) + .await + .change_context(errors::DummyConnectorErrors::RefundNotFound)?; + Ok(api::ApplicationResponse::Json(refund_data)) +} diff --git a/crates/router/src/routes/dummy_connector/errors.rs b/crates/router/src/routes/dummy_connector/errors.rs index 0cce7076c5..4501df0a0f 100644 --- a/crates/router/src/routes/dummy_connector/errors.rs +++ b/crates/router/src/routes/dummy_connector/errors.rs @@ -34,6 +34,9 @@ pub enum DummyConnectorErrors { #[error(error_type = ErrorType::ServerNotAvailable, code = "DC_07", message = "Error occurred while storing the payment")] PaymentStoringError, + + #[error(error_type = ErrorType::InvalidRequestError, code = "DC_08", message = "Payment declined: {message}")] + PaymentDeclined { message: &'static str }, } impl core::fmt::Display for DummyConnectorErrors { @@ -77,6 +80,9 @@ impl common_utils::errors::ErrorSwitch { AER::InternalServerError(ApiError::new("DC", 7, self.error_message(), None)) } + Self::PaymentDeclined { message: _ } => { + AER::BadRequest(ApiError::new("DC", 8, self.error_message(), None)) + } } } } diff --git a/crates/router/src/routes/dummy_connector/types.rs b/crates/router/src/routes/dummy_connector/types.rs index 9ebf229c7b..0954d71b0f 100644 --- a/crates/router/src/routes/dummy_connector/types.rs +++ b/crates/router/src/routes/dummy_connector/types.rs @@ -1,11 +1,12 @@ use api_models::enums::Currency; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, generate_id_with_default_len}; +use error_stack::report; use masking::Secret; use router_env::types::FlowMetric; use strum::Display; use time::PrimitiveDateTime; -use super::errors::DummyConnectorErrors; +use super::{consts, errors::DummyConnectorErrors}; use crate::services; #[derive(Debug, Display, Clone, PartialEq, Eq)] @@ -13,13 +14,52 @@ use crate::services; pub enum Flow { DummyPaymentCreate, DummyPaymentRetrieve, + DummyPaymentAuthorize, + DummyPaymentComplete, DummyRefundCreate, DummyRefundRetrieve, } impl FlowMetric for Flow {} -#[allow(dead_code)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, strum::Display, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DummyConnectors { + #[serde(rename = "phonypay")] + #[strum(serialize = "phonypay")] + PhonyPay, + #[serde(rename = "fauxpay")] + #[strum(serialize = "fauxpay")] + FauxPay, + #[serde(rename = "pretendpay")] + #[strum(serialize = "pretendpay")] + PretendPay, + StripeTest, + AdyenTest, + CheckoutTest, + PaypalTest, +} + +impl DummyConnectors { + pub fn get_connector_image_link(self) -> &'static str { + match self { + Self::PhonyPay => "https://app.hyperswitch.io/euler-icons/Gateway/Light/PHONYPAY.svg", + Self::FauxPay => "https://app.hyperswitch.io/euler-icons/Gateway/Light/FAUXPAY.svg", + Self::PretendPay => { + "https://app.hyperswitch.io/euler-icons/Gateway/Light/PRETENDPAY.svg" + } + Self::StripeTest => { + "https://app.hyperswitch.io/euler-icons/Gateway/Light/STRIPE_TEST.svg" + } + Self::PaypalTest => { + "https://app.hyperswitch.io/euler-icons/Gateway/Light/PAYPAL_TEST.svg" + } + _ => "https://app.hyperswitch.io/euler-icons/Gateway/Light/PHONYPAY.svg", + } + } +} + #[derive( Default, serde::Serialize, serde::Deserialize, strum::Display, Clone, PartialEq, Debug, Eq, )] @@ -31,16 +71,110 @@ pub enum DummyConnectorStatus { Failed, } -#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct DummyConnectorPaymentAttempt { + pub timestamp: PrimitiveDateTime, + pub attempt_id: String, + pub payment_id: String, + pub payment_request: DummyConnectorPaymentRequest, +} + +impl From for DummyConnectorPaymentAttempt { + fn from(payment_request: DummyConnectorPaymentRequest) -> Self { + let timestamp = common_utils::date_time::now(); + let payment_id = generate_id_with_default_len(consts::PAYMENT_ID_PREFIX); + let attempt_id = generate_id_with_default_len(consts::ATTEMPT_ID_PREFIX); + Self { + timestamp, + attempt_id, + payment_id, + payment_request, + } + } +} + +impl DummyConnectorPaymentAttempt { + pub fn build_payment_data( + self, + status: DummyConnectorStatus, + next_action: Option, + return_url: Option, + ) -> DummyConnectorPaymentData { + DummyConnectorPaymentData { + attempt_id: self.attempt_id, + payment_id: self.payment_id, + status, + amount: self.payment_request.amount, + eligible_amount: self.payment_request.amount, + connector: self.payment_request.connector, + created: self.timestamp, + currency: self.payment_request.currency, + payment_method_type: self.payment_request.payment_method_data.into(), + next_action, + return_url, + } + } +} + +#[derive(Clone, Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] pub struct DummyConnectorPaymentRequest { pub amount: i64, pub currency: Currency, pub payment_method_data: DummyConnectorPaymentMethodData, + pub return_url: Option, + pub connector: DummyConnectors, } -#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub trait GetPaymentMethodDetails { + fn get_name(&self) -> &'static str; + fn get_image_link(&self) -> &'static str; +} + +#[derive(Clone, Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +#[serde(rename_all = "lowercase")] pub enum DummyConnectorPaymentMethodData { Card(DummyConnectorCard), + Wallet(DummyConnectorWallet), + PayLater(DummyConnectorPayLater), +} + +#[derive( + Default, serde::Serialize, serde::Deserialize, strum::Display, PartialEq, Debug, Clone, +)] +#[serde(rename_all = "lowercase")] +pub enum DummyConnectorPaymentMethodType { + #[default] + Card, + Wallet(DummyConnectorWallet), + PayLater(DummyConnectorPayLater), +} + +impl From for DummyConnectorPaymentMethodType { + fn from(value: DummyConnectorPaymentMethodData) -> Self { + match value { + DummyConnectorPaymentMethodData::Card(_) => Self::Card, + DummyConnectorPaymentMethodData::Wallet(wallet) => Self::Wallet(wallet), + DummyConnectorPaymentMethodData::PayLater(pay_later) => Self::PayLater(pay_later), + } + } +} + +impl GetPaymentMethodDetails for DummyConnectorPaymentMethodType { + fn get_name(&self) -> &'static str { + match self { + Self::Card => "3D Secure", + Self::Wallet(wallet) => wallet.get_name(), + Self::PayLater(pay_later) => pay_later.get_name(), + } + } + + fn get_image_link(&self) -> &'static str { + match self { + Self::Card => "https://www.svgrepo.com/show/115459/credit-card.svg", + Self::Wallet(wallet) => wallet.get_image_link(), + Self::PayLater(pay_later) => pay_later.get_image_link(), + } + } } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -50,49 +184,108 @@ pub struct DummyConnectorCard { pub expiry_month: Secret, pub expiry_year: Secret, pub cvc: Secret, - pub complete: bool, } -#[derive( - Default, serde::Serialize, serde::Deserialize, strum::Display, PartialEq, Debug, Clone, -)] -#[serde(rename_all = "lowercase")] -pub enum PaymentMethodType { - #[default] - Card, +pub enum DummyConnectorCardFlow { + NoThreeDS(DummyConnectorStatus, Option), + ThreeDS(DummyConnectorStatus, Option), +} + +#[derive(Clone, Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub enum DummyConnectorWallet { + GooglePay, + Paypal, + WeChatPay, + MbWay, + AliPay, + AliPayHK, +} + +impl GetPaymentMethodDetails for DummyConnectorWallet { + fn get_name(&self) -> &'static str { + match self { + Self::GooglePay => "Google Pay", + Self::Paypal => "PayPal", + Self::WeChatPay => "WeChat Pay", + Self::MbWay => "Mb Way", + Self::AliPay => "Alipay", + Self::AliPayHK => "Alipay HK", + } + } + fn get_image_link(&self) -> &'static str { + match self { + Self::GooglePay => "https://pay.google.com/about/static_kcs/images/logos/google-pay-logo.svg", + Self::Paypal => "https://app.hyperswitch.io/euler-icons/Gateway/Light/PAYPAL.svg", + Self::WeChatPay => "https://raw.githubusercontent.com/datatrans/payment-logos/master/assets/apm/wechat-pay.svg?sanitize=true", + Self::MbWay => "https://upload.wikimedia.org/wikipedia/commons/e/e3/Logo_MBWay.svg", + Self::AliPay => "https://www.logo.wine/a/logo/Alipay/Alipay-Logo.wine.svg", + Self::AliPayHK => "https://www.logo.wine/a/logo/Alipay/Alipay-Logo.wine.svg", + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +pub enum DummyConnectorPayLater { + Klarna, + Affirm, + AfterPayClearPay, +} + +impl GetPaymentMethodDetails for DummyConnectorPayLater { + fn get_name(&self) -> &'static str { + match self { + Self::Klarna => "Klarna", + Self::Affirm => "Affirm", + Self::AfterPayClearPay => "Afterpay Clearpay", + } + } + fn get_image_link(&self) -> &'static str { + match self { + Self::Klarna => "https://docs.klarna.com/assets/media/7404df75-d165-4eee-b33c-a9537b847952/Klarna_Logo_Primary_Black.svg", + Self::Affirm => "https://upload.wikimedia.org/wikipedia/commons/f/ff/Affirm_logo.svg", + Self::AfterPayClearPay => "https://upload.wikimedia.org/wikipedia/en/c/c3/Afterpay_logo.svg" + } + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct DummyConnectorPaymentData { + pub attempt_id: String, + pub payment_id: String, pub status: DummyConnectorStatus, pub amount: i64, pub eligible_amount: i64, pub currency: Currency, #[serde(with = "common_utils::custom_serde::iso8601")] pub created: PrimitiveDateTime, - pub payment_method_type: PaymentMethodType, + pub payment_method_type: DummyConnectorPaymentMethodType, + pub connector: DummyConnectors, + pub next_action: Option, + pub return_url: Option, } impl DummyConnectorPaymentData { - pub fn new( - status: DummyConnectorStatus, - amount: i64, - eligible_amount: i64, - currency: Currency, - created: PrimitiveDateTime, - payment_method_type: PaymentMethodType, - ) -> Self { - Self { - status, - amount, - eligible_amount, - currency, - created, - payment_method_type, + pub fn is_eligible_for_refund(&self, refund_amount: i64) -> DummyConnectorResult<()> { + if self.eligible_amount < refund_amount { + return Err( + report!(DummyConnectorErrors::RefundAmountExceedsPaymentAmount) + .attach_printable("Eligible amount is lesser than refund amount"), + ); } + if self.status != DummyConnectorStatus::Succeeded { + return Err(report!(DummyConnectorErrors::PaymentNotSuccessful) + .attach_printable("Payment is not successful to process the refund")); + } + Ok(()) } } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum DummyConnectorNextAction { + RedirectToUrl(String), +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DummyConnectorPaymentResponse { pub status: DummyConnectorStatus, @@ -101,7 +294,22 @@ pub struct DummyConnectorPaymentResponse { pub currency: Currency, #[serde(with = "common_utils::custom_serde::iso8601")] pub created: PrimitiveDateTime, - pub payment_method_type: PaymentMethodType, + pub payment_method_type: DummyConnectorPaymentMethodType, + pub next_action: Option, +} + +impl From for DummyConnectorPaymentResponse { + fn from(value: DummyConnectorPaymentData) -> Self { + Self { + status: value.status, + id: value.payment_id, + amount: value.amount, + currency: value.currency, + created: value.created, + payment_method_type: value.payment_method_type, + next_action: value.next_action, + } + } } #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -109,24 +317,20 @@ pub struct DummyConnectorPaymentRetrieveRequest { pub payment_id: String, } -impl DummyConnectorPaymentResponse { - pub fn new( - status: DummyConnectorStatus, - id: String, - amount: i64, - currency: Currency, - created: PrimitiveDateTime, - payment_method_type: PaymentMethodType, - ) -> Self { - Self { - status, - id, - amount, - currency, - created, - payment_method_type, - } - } +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DummyConnectorPaymentConfirmRequest { + pub attempt_id: String, +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DummyConnectorPaymentCompleteRequest { + pub attempt_id: String, + pub confirm: bool, +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DummyConnectorPaymentCompleteBody { + pub confirm: bool, } #[derive(Default, Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] @@ -173,3 +377,5 @@ pub struct DummyConnectorRefundRetrieveRequest { pub type DummyConnectorResponse = CustomResult, DummyConnectorErrors>; + +pub type DummyConnectorResult = CustomResult; diff --git a/crates/router/src/routes/dummy_connector/utils.rs b/crates/router/src/routes/dummy_connector/utils.rs index 44f5217407..1f56f36537 100644 --- a/crates/router/src/routes/dummy_connector/utils.rs +++ b/crates/router/src/routes/dummy_connector/utils.rs @@ -1,15 +1,17 @@ -use std::{fmt::Debug, sync::Arc}; +use std::fmt::Debug; -use app::AppState; -use common_utils::generate_id; -use error_stack::{report, ResultExt}; +use common_utils::ext_traits::AsyncExt; +use error_stack::{report, IntoReport, ResultExt}; use masking::PeekInterface; +use maud::html; use rand::Rng; -use redis_interface::RedisConnectionPool; use tokio::time as tokio; -use super::{errors, types}; -use crate::{routes::app, services::api, utils::OptionExt}; +use super::{ + consts, errors, + types::{self, GetPaymentMethodDetails}, +}; +use crate::routes::AppState; pub async fn tokio_mock_sleep(delay: u64, tolerance: u64) { let mut rng = rand::thread_rng(); @@ -17,185 +19,14 @@ pub async fn tokio_mock_sleep(delay: u64, tolerance: u64) { tokio::sleep(tokio::Duration::from_millis(effective_delay)).await } -pub async fn payment( +pub async fn store_data_in_redis( state: &AppState, - req: types::DummyConnectorPaymentRequest, -) -> types::DummyConnectorResponse { - tokio_mock_sleep( - state.conf.dummy_connector.payment_duration, - state.conf.dummy_connector.payment_tolerance, - ) - .await; - - let payment_id = generate_id(20, "dummy_pay"); - match req.payment_method_data { - types::DummyConnectorPaymentMethodData::Card(card) => { - let card_number = card.number.peek(); - - match card_number.as_str() { - "4111111111111111" | "4242424242424242" => { - let timestamp = common_utils::date_time::now(); - let payment_data = types::DummyConnectorPaymentData::new( - types::DummyConnectorStatus::Succeeded, - req.amount, - req.amount, - req.currency, - timestamp.to_owned(), - types::PaymentMethodType::Card, - ); - let redis_conn = state.store.get_redis_conn(); - store_data_in_redis( - redis_conn, - payment_id.to_owned(), - payment_data, - state.conf.dummy_connector.payment_ttl, - ) - .await?; - Ok(api::ApplicationResponse::Json( - types::DummyConnectorPaymentResponse::new( - types::DummyConnectorStatus::Succeeded, - payment_id, - req.amount, - req.currency, - timestamp, - types::PaymentMethodType::Card, - ), - )) - } - _ => Err(report!(errors::DummyConnectorErrors::CardNotSupported) - .attach_printable("The card is not supported")), - } - } - } -} - -pub async fn payment_data( - state: &AppState, - req: types::DummyConnectorPaymentRetrieveRequest, -) -> types::DummyConnectorResponse { - let payment_id = req.payment_id; - tokio_mock_sleep( - state.conf.dummy_connector.payment_retrieve_duration, - state.conf.dummy_connector.payment_retrieve_tolerance, - ) - .await; - - let redis_conn = state.store.get_redis_conn(); - let payment_data = redis_conn - .get_and_deserialize_key::( - payment_id.as_str(), - "DummyConnectorPaymentData", - ) - .await - .change_context(errors::DummyConnectorErrors::PaymentNotFound)?; - - Ok(api::ApplicationResponse::Json( - types::DummyConnectorPaymentResponse::new( - payment_data.status, - payment_id, - payment_data.amount, - payment_data.currency, - payment_data.created, - payment_data.payment_method_type, - ), - )) -} - -pub async fn refund_payment( - state: &AppState, - req: types::DummyConnectorRefundRequest, -) -> types::DummyConnectorResponse { - tokio_mock_sleep( - state.conf.dummy_connector.refund_duration, - state.conf.dummy_connector.refund_tolerance, - ) - .await; - - let payment_id = req - .payment_id - .get_required_value("payment_id") - .change_context(errors::DummyConnectorErrors::MissingRequiredField { - field_name: "payment_id", - })?; - - let redis_conn = state.store.get_redis_conn(); - let mut payment_data = redis_conn - .get_and_deserialize_key::( - payment_id.as_str(), - "DummyConnectorPaymentData", - ) - .await - .change_context(errors::DummyConnectorErrors::PaymentNotFound)?; - - if payment_data.eligible_amount < req.amount { - return Err( - report!(errors::DummyConnectorErrors::RefundAmountExceedsPaymentAmount) - .attach_printable("Eligible amount is lesser than refund amount"), - ); - } - - if payment_data.status != types::DummyConnectorStatus::Succeeded { - return Err(report!(errors::DummyConnectorErrors::PaymentNotSuccessful) - .attach_printable("Payment is not successful to process the refund")); - } - - let refund_id = generate_id(20, "dummy_ref"); - payment_data.eligible_amount -= req.amount; - store_data_in_redis( - redis_conn.to_owned(), - payment_id, - payment_data.to_owned(), - state.conf.dummy_connector.payment_ttl, - ) - .await?; - - let refund_data = types::DummyConnectorRefundResponse::new( - types::DummyConnectorStatus::Succeeded, - refund_id.to_owned(), - payment_data.currency, - common_utils::date_time::now(), - payment_data.amount, - req.amount, - ); - - store_data_in_redis( - redis_conn, - refund_id, - refund_data.to_owned(), - state.conf.dummy_connector.refund_ttl, - ) - .await?; - Ok(api::ApplicationResponse::Json(refund_data)) -} - -pub async fn refund_data( - state: &AppState, - req: types::DummyConnectorRefundRetrieveRequest, -) -> types::DummyConnectorResponse { - let refund_id = req.refund_id; - tokio_mock_sleep( - state.conf.dummy_connector.refund_retrieve_duration, - state.conf.dummy_connector.refund_retrieve_tolerance, - ) - .await; - - let redis_conn = state.store.get_redis_conn(); - let refund_data = redis_conn - .get_and_deserialize_key::( - refund_id.as_str(), - "DummyConnectorRefundResponse", - ) - .await - .change_context(errors::DummyConnectorErrors::RefundNotFound)?; - Ok(api::ApplicationResponse::Json(refund_data)) -} - -async fn store_data_in_redis( - redis_conn: Arc, key: String, data: impl serde::Serialize + Debug, ttl: i64, -) -> Result<(), error_stack::Report> { +) -> types::DummyConnectorResult<()> { + let redis_conn = state.store.get_redis_conn(); + redis_conn .serialize_and_set_key_with_expiry(&key, data, ttl) .await @@ -203,3 +34,264 @@ async fn store_data_in_redis( .attach_printable("Failed to add data in redis")?; Ok(()) } + +pub async fn get_payment_data_from_payment_id( + state: &AppState, + payment_id: String, +) -> types::DummyConnectorResult { + let redis_conn = state.store.get_redis_conn(); + redis_conn + .get_and_deserialize_key::( + payment_id.as_str(), + "types DummyConnectorPaymentData", + ) + .await + .change_context(errors::DummyConnectorErrors::PaymentNotFound) +} + +pub async fn get_payment_data_by_attempt_id( + state: &AppState, + attempt_id: String, +) -> types::DummyConnectorResult { + let redis_conn = state.store.get_redis_conn(); + redis_conn + .get_and_deserialize_key::(attempt_id.as_str(), "String") + .await + .async_and_then(|payment_id| async move { + redis_conn + .get_and_deserialize_key::( + payment_id.as_str(), + "DummyConnectorPaymentData", + ) + .await + }) + .await + .change_context(errors::DummyConnectorErrors::PaymentNotFound) +} + +pub fn get_authorize_page( + payment_data: types::DummyConnectorPaymentData, + return_url: String, +) -> String { + let mode = payment_data.payment_method_type.get_name(); + let image = payment_data.payment_method_type.get_image_link(); + let connector_image = payment_data.connector.get_connector_image_link(); + let currency = payment_data.currency.to_string(); + + html! { + head { + title { "Authorize Payment" } + style { (consts::THREE_DS_CSS) } + } + body { + div.heading { + img.logo src="https://app.hyperswitch.io/assets/Dark/hyperswitchLogoIconWithText.svg" alt="Hyperswitch Logo" {} + h1 { "Test Payment Page" } + } + div.container { + div.payment_details { + img src=(image) {} + div.border {} + img src=(connector_image) {} + } + (maud::PreEscaped( + format!(r#" +

+ This is a test payment of {} using {} + +

+ "#, currency, mode, payment_data.amount) + ) + ) + p { b { "Real money will not be debited for the payment." } " You can choose to simulate successful or failed payment while testing this payment."} + div.user_action { + button.authorize onclick=(format!("window.location.href='{}?confirm=true'", return_url)) + { "Complete Payment" } + button.reject onclick=(format!("window.location.href='{}?confirm=false'", return_url)) + { "Reject Payment" } + } + } + div.container { + p.disclaimer { "What is this page?" } + p { "This page is just a simulation for integration and testing purpose.\ + In live mode, this page will not be displayed and the user will be taken to the\ + Bank page (or) Googlepay cards popup (or) original payment method’s page. " + a href=("https://hyperswitch.io/contact") { "Contact us" } " for any queries." } + } + } + } + .into_string() +} + +pub fn get_expired_page() -> String { + html! { + head { + title { "Authorize Payment" } + style { (consts::THREE_DS_CSS) } + } + body { + div.heading { + img.logo src="https://app.hyperswitch.io/assets/Dark/hyperswitchLogoIconWithText.svg" alt="Hyperswitch Logo" {} + h1 { "Test Payment Page" } + } + div.container { + p.disclaimer { "This link is not valid or it is expired" } + } + div.container { + p.disclaimer { "What is this page?" } + p { "This page is just a simulation for integration and testing purpose.\ + In live mode, this is not visible. " + a href=("https://hyperswitch.io/contact") { "Contact us" } " for any queries." } + } + } + } + .into_string() +} + +pub trait ProcessPaymentAttempt { + fn build_payment_data_from_payment_attempt( + self, + payment_attempt: types::DummyConnectorPaymentAttempt, + redirect_url: String, + ) -> types::DummyConnectorResult; +} + +impl ProcessPaymentAttempt for types::DummyConnectorCard { + fn build_payment_data_from_payment_attempt( + self, + payment_attempt: types::DummyConnectorPaymentAttempt, + redirect_url: String, + ) -> types::DummyConnectorResult { + match self.get_flow_from_card_number()? { + types::DummyConnectorCardFlow::NoThreeDS(status, error) => { + if let Some(error) = error { + Err(error).into_report()?; + } + Ok(payment_attempt.build_payment_data(status, None, None)) + } + types::DummyConnectorCardFlow::ThreeDS(_, _) => { + Ok(payment_attempt.clone().build_payment_data( + types::DummyConnectorStatus::Processing, + Some(types::DummyConnectorNextAction::RedirectToUrl(redirect_url)), + payment_attempt.payment_request.return_url, + )) + } + } + } +} + +impl types::DummyConnectorCard { + pub fn get_flow_from_card_number( + self, + ) -> types::DummyConnectorResult { + let card_number = self.number.peek(); + match card_number.as_str() { + "4111111111111111" | "4242424242424242" | "5555555555554444" | "38000000000006" + | "378282246310005" | "6011111111111117" => { + Ok(types::DummyConnectorCardFlow::NoThreeDS( + types::DummyConnectorStatus::Succeeded, + None, + )) + } + "5105105105105100" | "4000000000000002" => { + Ok(types::DummyConnectorCardFlow::NoThreeDS( + types::DummyConnectorStatus::Failed, + Some(errors::DummyConnectorErrors::PaymentDeclined { + message: "Card declined", + }), + )) + } + "4000000000009995" => Ok(types::DummyConnectorCardFlow::NoThreeDS( + types::DummyConnectorStatus::Failed, + Some(errors::DummyConnectorErrors::PaymentDeclined { + message: "Insufficient funds", + }), + )), + "4000000000009987" => Ok(types::DummyConnectorCardFlow::NoThreeDS( + types::DummyConnectorStatus::Failed, + Some(errors::DummyConnectorErrors::PaymentDeclined { + message: "Lost card", + }), + )), + "4000000000009979" => Ok(types::DummyConnectorCardFlow::NoThreeDS( + types::DummyConnectorStatus::Failed, + Some(errors::DummyConnectorErrors::PaymentDeclined { + message: "Stolen card", + }), + )), + "4000003800000446" => Ok(types::DummyConnectorCardFlow::ThreeDS( + types::DummyConnectorStatus::Succeeded, + None, + )), + _ => Err(report!(errors::DummyConnectorErrors::CardNotSupported) + .attach_printable("The card is not supported")), + } + } +} + +impl ProcessPaymentAttempt for types::DummyConnectorWallet { + fn build_payment_data_from_payment_attempt( + self, + payment_attempt: types::DummyConnectorPaymentAttempt, + redirect_url: String, + ) -> types::DummyConnectorResult { + Ok(payment_attempt.clone().build_payment_data( + types::DummyConnectorStatus::Processing, + Some(types::DummyConnectorNextAction::RedirectToUrl(redirect_url)), + payment_attempt.payment_request.return_url, + )) + } +} + +impl ProcessPaymentAttempt for types::DummyConnectorPayLater { + fn build_payment_data_from_payment_attempt( + self, + payment_attempt: types::DummyConnectorPaymentAttempt, + redirect_url: String, + ) -> types::DummyConnectorResult { + Ok(payment_attempt.clone().build_payment_data( + types::DummyConnectorStatus::Processing, + Some(types::DummyConnectorNextAction::RedirectToUrl(redirect_url)), + payment_attempt.payment_request.return_url, + )) + } +} + +impl ProcessPaymentAttempt for types::DummyConnectorPaymentMethodData { + fn build_payment_data_from_payment_attempt( + self, + payment_attempt: types::DummyConnectorPaymentAttempt, + redirect_url: String, + ) -> types::DummyConnectorResult { + match self { + Self::Card(card) => { + card.build_payment_data_from_payment_attempt(payment_attempt, redirect_url) + } + Self::Wallet(wallet) => { + wallet.build_payment_data_from_payment_attempt(payment_attempt, redirect_url) + } + Self::PayLater(pay_later) => { + pay_later.build_payment_data_from_payment_attempt(payment_attempt, redirect_url) + } + } + } +} + +impl types::DummyConnectorPaymentData { + pub fn process_payment_attempt( + state: &AppState, + payment_attempt: types::DummyConnectorPaymentAttempt, + ) -> types::DummyConnectorResult { + let redirect_url = format!( + "{}/dummy-connector/authorize/{}", + state.conf.server.base_url, payment_attempt.attempt_id + ); + payment_attempt + .clone() + .payment_request + .payment_method_data + .build_payment_data_from_payment_attempt(payment_attempt, redirect_url) + } +} diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index b3e599be96..b37be72d6c 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -189,6 +189,7 @@ refund_duration = 1000 refund_tolerance = 100 refund_retrieve_duration = 500 refund_retrieve_tolerance = 100 +authorize_ttl = 36000 [payouts] payout_eligibility = true