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_customer]
|
||||||
connector_list = "stripe"
|
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
|
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))]
|
#[instrument(level = "DEBUG", skip(self))]
|
||||||
pub async fn get_key<V>(&self, key: &str) -> CustomResult<V, errors::RedisError>
|
pub async fn get_key<V>(&self, key: &str) -> CustomResult<V, errors::RedisError>
|
||||||
where
|
where
|
||||||
|
|||||||
@ -24,7 +24,8 @@ accounts_cache = []
|
|||||||
openapi = ["olap", "oltp"]
|
openapi = ["olap", "oltp"]
|
||||||
vergen = ["router_env/vergen"]
|
vergen = ["router_env/vergen"]
|
||||||
multiple_mca = ["api_models/multiple_mca"]
|
multiple_mca = ["api_models/multiple_mca"]
|
||||||
dummy_connector = []
|
dummy_connector = ["api_models/dummy_connector"]
|
||||||
|
external_access_dc = ["dummy_connector"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -67,6 +67,8 @@ pub struct Settings {
|
|||||||
pub file_upload_config: FileUploadConfig,
|
pub file_upload_config: FileUploadConfig,
|
||||||
pub tokenization: TokenizationConfig,
|
pub tokenization: TokenizationConfig,
|
||||||
pub connector_customer: ConnectorCustomer,
|
pub connector_customer: ConnectorCustomer,
|
||||||
|
#[cfg(feature = "dummy_connector")]
|
||||||
|
pub dummy_connector: DummyConnector,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Clone, Default)]
|
||||||
@ -93,6 +95,14 @@ where
|
|||||||
.collect())
|
.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)]
|
#[derive(Debug, Deserialize, Clone, Default)]
|
||||||
pub struct PaymentMethodTokenFilter {
|
pub struct PaymentMethodTokenFilter {
|
||||||
#[serde(deserialize_with = "pm_deser")]
|
#[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"))]
|
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||||
{
|
{
|
||||||
server_app = server_app
|
server_app = server_app
|
||||||
|
|||||||
@ -5,6 +5,8 @@ pub mod cards_info;
|
|||||||
pub mod configs;
|
pub mod configs;
|
||||||
pub mod customers;
|
pub mod customers;
|
||||||
pub mod disputes;
|
pub mod disputes;
|
||||||
|
#[cfg(feature = "dummy_connector")]
|
||||||
|
pub mod dummy_connector;
|
||||||
pub mod ephemeral_key;
|
pub mod ephemeral_key;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
@ -16,6 +18,8 @@ pub mod payouts;
|
|||||||
pub mod refunds;
|
pub mod refunds;
|
||||||
pub mod webhooks;
|
pub mod webhooks;
|
||||||
|
|
||||||
|
#[cfg(feature = "dummy_connector")]
|
||||||
|
pub use self::app::DummyConnector;
|
||||||
pub use self::app::{
|
pub use self::app::{
|
||||||
ApiKeys, AppState, Cards, Configs, Customers, Disputes, EphemeralKey, Files, Health, Mandates,
|
ApiKeys, AppState, Cards, Configs, Customers, Disputes, EphemeralKey, Files, Health, Mandates,
|
||||||
MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds,
|
MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds,
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use actix_web::{web, Scope};
|
use actix_web::{web, Scope};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
#[cfg(feature = "dummy_connector")]
|
||||||
|
use super::dummy_connector::*;
|
||||||
use super::health::*;
|
use super::health::*;
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
use super::{admin::*, api_keys::*, disputes::*, files::*};
|
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;
|
pub struct Payments;
|
||||||
|
|
||||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
#[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