feat(connector): add payment routes for dummy connector (#980)

This commit is contained in:
ThisIsMani
2023-05-08 15:04:50 +05:30
committed by GitHub
parent 9cb3fa216c
commit 4ece376b56
11 changed files with 320 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View 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
}

View 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)),
}
}
}

View 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>;

View 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(())
}