feat(dummy_connector): Add 3DS Flow, Wallets and Pay Later for Dummy Connector (#1781)

This commit is contained in:
Mani Chandra
2023-07-27 19:30:03 +05:30
committed by GitHub
parent b96687c3fa
commit 8186c778bd
14 changed files with 1105 additions and 297 deletions

View File

@ -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)]

View File

@ -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<const T: u8> ConnectorCommon for DummyConnector<T> {
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::<transformers::DummyConnectors>::into(T).get_dummy_connector_id()
}
fn common_get_content_type(&self) -> &'static str {
@ -94,7 +84,7 @@ impl<const T: u8> ConnectorCommon for DummyConnector<T> {
&self,
val: &types::ConnectorAuthType,
) -> Result<(), error_stack::Report<errors::ConnectorError>> {
dummyconnector::DummyConnectorAuthType::try_from(val)?;
transformers::DummyConnectorAuthType::try_from(val)?;
Ok(())
}
@ -106,7 +96,7 @@ impl<const T: u8> ConnectorCommon for DummyConnector<T> {
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<const T: u8> ConnectorCommon for DummyConnector<T> {
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: dummyconnector::DummyConnectorErrorResponse = res
let response: transformers::DummyConnectorErrorResponse = res
.response
.parse_struct("DummyConnectorErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -174,10 +164,12 @@ impl<const T: u8>
) -> CustomResult<String, errors::ConnectorError> {
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::<transformers::DummyConnectors>::into(T).get_dummy_connector_id(),
payment_experience: api::enums::PaymentExperience::RedirectToUrl.to_string(),
})),
}
}
@ -186,12 +178,12 @@ impl<const T: u8>
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let connector_request = dummyconnector::DummyConnectorPaymentsRequest::try_from(req)?;
let connector_request = transformers::DummyConnectorPaymentsRequest::<T>::try_from(req)?;
let dummmy_payments_request = types::RequestBody::log_and_get_request_body(
&connector_request,
utils::Encode::<dummyconnector::DummyConnectorPaymentsRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
&connector_request,
utils::Encode::<transformers::DummyConnectorPaymentsRequest::<T>>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(dummmy_payments_request))
}
@ -220,10 +212,11 @@ impl<const T: u8>
data: &types::PaymentsAuthorizeRouterData,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
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<const T: u8>
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
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<const T: u8>
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
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<const T: u8> ConnectorIntegration<api::Execute, types::RefundsData, types::
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Option<types::RequestBody>, 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::<dummyconnector::DummyConnectorRefundRequest>::encode_to_string_of_json,
utils::Encode::<transformers::DummyConnectorRefundRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(dummmy_refund_request))
@ -458,9 +451,9 @@ impl<const T: u8> ConnectorIntegration<api::Execute, types::RefundsData, types::
data: &types::RefundsRouterData<api::Execute>,
res: Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, 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<const T: u8> ConnectorIntegration<api::RSync, types::RefundsData, types::Re
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
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,

View File

@ -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<u8> 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<const T: u8> {
amount: i64,
currency: Currency,
payment_method_data: PaymentMethodData,
return_url: Option<String>,
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<String>,
number: cards::CardNumber,
expiry_month: Secret<String>,
expiry_year: Secret<String>,
cvc: Secret<String>,
complete: bool,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for DummyConnectorPaymentsRequest {
impl From<api_models::payments::Card> 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<api_models::payments::WalletData> for DummyConnectorWallet {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(value: api_models::payments::WalletData) -> Result<Self, Self::Error> {
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<api_models::payments::PayLaterData> for DummyConnectorPayLater {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(value: api_models::payments::PayLaterData) -> Result<Self, Self::Error> {
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<const T: u8> TryFrom<&types::PaymentsAuthorizeRouterData>
for DummyConnectorPaymentsRequest<T>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
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<PaymentMethodData, Self::Error> = 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::<DummyConnectors>::into(T),
})
}
}
@ -86,19 +203,28 @@ impl From<DummyConnectorPaymentStatus> 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<DummyConnectorNextAction>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum PaymentMethodType {
Card,
Wallet(DummyConnectorWallet),
PayLater(DummyConnectorPayLater),
}
impl<F, T> TryFrom<types::ResponseRouterData<F, PaymentsResponse, T, types::PaymentsResponseData>>
@ -108,11 +234,18 @@ impl<F, T> TryFrom<types::ResponseRouterData<F, PaymentsResponse, T, types::Paym
fn try_from(
item: types::ResponseRouterData<F, PaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
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<F, T> TryFrom<types::ResponseRouterData<F, PaymentsResponse, T, types::Paym
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum DummyConnectorNextAction {
RedirectToUrl(Url),
}
impl DummyConnectorNextAction {
fn get_url(&self) -> Option<Url> {
match self {
Self::RedirectToUrl(redirect_to_url) => Some(redirect_to_url.to_owned()),
}
}
}
// REFUND :
// Type definition for RefundRequest
#[derive(Default, Debug, Serialize)]

View File

@ -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)
}
}

View File

@ -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<app::AppState>,
req: actix_web::HttpRequest,
path: web::Path<String>,
) -> 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<app::AppState>,
req: actix_web::HttpRequest,
path: web::Path<String>,
json_payload: web::Query<types::DummyConnectorPaymentCompleteBody>,
) -> 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<app::AppState>,
req: actix_web::HttpRequest,
json_payload: web::Json<types::DummyConnectorPaymentRequest>,
) -> 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

View File

@ -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%;
}"#;

View File

@ -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<types::DummyConnectorPaymentResponse> {
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<types::DummyConnectorPaymentResponse> {
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<String> {
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<types::DummyConnectorRefundResponse> {
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<types::DummyConnectorRefundResponse> {
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::<types::DummyConnectorRefundResponse>(
refund_id.as_str(),
"DummyConnectorRefundResponse",
)
.await
.change_context(errors::DummyConnectorErrors::RefundNotFound)?;
Ok(api::ApplicationResponse::Json(refund_data))
}

View File

@ -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<api_models::errors::types::ApiErrorRespon
Self::PaymentStoringError => {
AER::InternalServerError(ApiError::new("DC", 7, self.error_message(), None))
}
Self::PaymentDeclined { message: _ } => {
AER::BadRequest(ApiError::new("DC", 8, self.error_message(), None))
}
}
}
}

View File

@ -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<DummyConnectorPaymentRequest> 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<DummyConnectorNextAction>,
return_url: Option<String>,
) -> 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<String>,
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<DummyConnectorPaymentMethodData> 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<String>,
pub expiry_year: Secret<String>,
pub cvc: Secret<String>,
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<DummyConnectorErrors>),
ThreeDS(DummyConnectorStatus, Option<DummyConnectorErrors>),
}
#[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<DummyConnectorNextAction>,
pub return_url: Option<String>,
}
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<DummyConnectorNextAction>,
}
impl From<DummyConnectorPaymentData> 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<T> =
CustomResult<services::ApplicationResponse<T>, DummyConnectorErrors>;
pub type DummyConnectorResult<T> = CustomResult<T, DummyConnectorErrors>;

View File

@ -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<types::DummyConnectorPaymentResponse> {
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<types::DummyConnectorPaymentResponse> {
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::<types::DummyConnectorPaymentData>(
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<types::DummyConnectorRefundResponse> {
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::<types::DummyConnectorPaymentData>(
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<types::DummyConnectorRefundResponse> {
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::<types::DummyConnectorRefundResponse>(
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<RedisConnectionPool>,
key: String,
data: impl serde::Serialize + Debug,
ttl: i64,
) -> Result<(), error_stack::Report<errors::DummyConnectorErrors>> {
) -> 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<types::DummyConnectorPaymentData> {
let redis_conn = state.store.get_redis_conn();
redis_conn
.get_and_deserialize_key::<types::DummyConnectorPaymentData>(
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<types::DummyConnectorPaymentData> {
let redis_conn = state.store.get_redis_conn();
redis_conn
.get_and_deserialize_key::<String>(attempt_id.as_str(), "String")
.await
.async_and_then(|payment_id| async move {
redis_conn
.get_and_deserialize_key::<types::DummyConnectorPaymentData>(
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#"
<p class="disclaimer">
This is a test payment of <span id="amount"></span> {} using {}
<script>
document.getElementById("amount").innerHTML = ({} / 100).toFixed(2);
</script>
</p>
"#, 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 methods 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<types::DummyConnectorPaymentData>;
}
impl ProcessPaymentAttempt for types::DummyConnectorCard {
fn build_payment_data_from_payment_attempt(
self,
payment_attempt: types::DummyConnectorPaymentAttempt,
redirect_url: String,
) -> types::DummyConnectorResult<types::DummyConnectorPaymentData> {
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<types::DummyConnectorCardFlow> {
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<types::DummyConnectorPaymentData> {
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<types::DummyConnectorPaymentData> {
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<types::DummyConnectorPaymentData> {
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<Self> {
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)
}
}