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