mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 01:57:45 +08:00 
			
		
		
		
	feat(user): generate and delete sample data (#2987)
Co-authored-by: Rachit Naithani <rachit.naithani@juspay.in> Co-authored-by: Mani Chandra Dulam <mani.dchandra@juspay.in>
This commit is contained in:
		| @ -4,6 +4,7 @@ use crate::services::ApplicationResponse; | ||||
|  | ||||
| pub type UserResult<T> = CustomResult<T, UserErrors>; | ||||
| pub type UserResponse<T> = CustomResult<ApplicationResponse<T>, UserErrors>; | ||||
| pub mod sample_data; | ||||
|  | ||||
| #[derive(Debug, thiserror::Error)] | ||||
| pub enum UserErrors { | ||||
|  | ||||
							
								
								
									
										73
									
								
								crates/router/src/core/errors/user/sample_data.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								crates/router/src/core/errors/user/sample_data.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| use api_models::errors::types::{ApiError, ApiErrorResponse}; | ||||
| use common_utils::errors::{CustomResult, ErrorSwitch, ErrorSwitchFrom}; | ||||
| use data_models::errors::StorageError; | ||||
|  | ||||
| pub type SampleDataResult<T> = CustomResult<T, SampleDataError>; | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, thiserror::Error)] | ||||
| pub enum SampleDataError { | ||||
|     #[error["Internal Server Error"]] | ||||
|     InternalServerError, | ||||
|     #[error("Data Does Not Exist")] | ||||
|     DataDoesNotExist, | ||||
|     #[error("Server Error")] | ||||
|     DatabaseError, | ||||
|     #[error("Merchant Id Not Found")] | ||||
|     MerchantIdNotFound, | ||||
|     #[error("Invalid Parameters")] | ||||
|     InvalidParameters, | ||||
|     #[error["Invalid Records"]] | ||||
|     InvalidRange, | ||||
| } | ||||
|  | ||||
| impl ErrorSwitch<ApiErrorResponse> for SampleDataError { | ||||
|     fn switch(&self) -> ApiErrorResponse { | ||||
|         match self { | ||||
|             Self::InternalServerError => ApiErrorResponse::InternalServerError(ApiError::new( | ||||
|                 "SD", | ||||
|                 0, | ||||
|                 "Something went wrong", | ||||
|                 None, | ||||
|             )), | ||||
|             Self::DatabaseError => ApiErrorResponse::InternalServerError(ApiError::new( | ||||
|                 "SD", | ||||
|                 1, | ||||
|                 "Server Error(DB is down)", | ||||
|                 None, | ||||
|             )), | ||||
|             Self::DataDoesNotExist => ApiErrorResponse::NotFound(ApiError::new( | ||||
|                 "SD", | ||||
|                 2, | ||||
|                 "Sample Data not present for given request", | ||||
|                 None, | ||||
|             )), | ||||
|             Self::MerchantIdNotFound => ApiErrorResponse::BadRequest(ApiError::new( | ||||
|                 "SD", | ||||
|                 3, | ||||
|                 "Merchant ID not provided", | ||||
|                 None, | ||||
|             )), | ||||
|             Self::InvalidParameters => ApiErrorResponse::BadRequest(ApiError::new( | ||||
|                 "SD", | ||||
|                 4, | ||||
|                 "Invalid parameters to generate Sample Data", | ||||
|                 None, | ||||
|             )), | ||||
|             Self::InvalidRange => ApiErrorResponse::BadRequest(ApiError::new( | ||||
|                 "SD", | ||||
|                 5, | ||||
|                 "Records to be generated should be between range 10 and 100", | ||||
|                 None, | ||||
|             )), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ErrorSwitchFrom<StorageError> for SampleDataError { | ||||
|     fn switch_from(error: &StorageError) -> Self { | ||||
|         match matches!(error, StorageError::ValueNotFound(_)) { | ||||
|             true => Self::DataDoesNotExist, | ||||
|             false => Self::DatabaseError, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -13,6 +13,8 @@ use crate::{ | ||||
|     types::domain, | ||||
|     utils, | ||||
| }; | ||||
| #[cfg(feature = "dummy_connector")] | ||||
| pub mod sample_data; | ||||
|  | ||||
| pub mod dashboard_metadata; | ||||
|  | ||||
|  | ||||
							
								
								
									
										82
									
								
								crates/router/src/core/user/sample_data.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								crates/router/src/core/user/sample_data.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | ||||
| use api_models::user::sample_data::SampleDataRequest; | ||||
| use common_utils::errors::ReportSwitchExt; | ||||
| use data_models::payments::payment_intent::PaymentIntentNew; | ||||
| use diesel_models::{user::sample_data::PaymentAttemptBatchNew, RefundNew}; | ||||
|  | ||||
| pub type SampleDataApiResponse<T> = SampleDataResult<ApplicationResponse<T>>; | ||||
|  | ||||
| use crate::{ | ||||
|     core::errors::sample_data::SampleDataResult, | ||||
|     routes::AppState, | ||||
|     services::{authentication::UserFromToken, ApplicationResponse}, | ||||
|     utils::user::sample_data::generate_sample_data, | ||||
| }; | ||||
|  | ||||
| pub async fn generate_sample_data_for_user( | ||||
|     state: AppState, | ||||
|     user_from_token: UserFromToken, | ||||
|     req: SampleDataRequest, | ||||
| ) -> SampleDataApiResponse<()> { | ||||
|     let sample_data = | ||||
|         generate_sample_data(&state, req, user_from_token.merchant_id.as_str()).await?; | ||||
|  | ||||
|     let (payment_intents, payment_attempts, refunds): ( | ||||
|         Vec<PaymentIntentNew>, | ||||
|         Vec<PaymentAttemptBatchNew>, | ||||
|         Vec<RefundNew>, | ||||
|     ) = sample_data.into_iter().fold( | ||||
|         (Vec::new(), Vec::new(), Vec::new()), | ||||
|         |(mut pi, mut pa, mut rf), (payment_intent, payment_attempt, refund)| { | ||||
|             pi.push(payment_intent); | ||||
|             pa.push(payment_attempt); | ||||
|             if let Some(refund) = refund { | ||||
|                 rf.push(refund); | ||||
|             } | ||||
|             (pi, pa, rf) | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     state | ||||
|         .store | ||||
|         .insert_payment_intents_batch_for_sample_data(payment_intents) | ||||
|         .await | ||||
|         .switch()?; | ||||
|     state | ||||
|         .store | ||||
|         .insert_payment_attempts_batch_for_sample_data(payment_attempts) | ||||
|         .await | ||||
|         .switch()?; | ||||
|     state | ||||
|         .store | ||||
|         .insert_refunds_batch_for_sample_data(refunds) | ||||
|         .await | ||||
|         .switch()?; | ||||
|  | ||||
|     Ok(ApplicationResponse::StatusOk) | ||||
| } | ||||
|  | ||||
| pub async fn delete_sample_data_for_user( | ||||
|     state: AppState, | ||||
|     user_from_token: UserFromToken, | ||||
|     _req: SampleDataRequest, | ||||
| ) -> SampleDataApiResponse<()> { | ||||
|     let merchant_id_del = user_from_token.merchant_id.as_str(); | ||||
|  | ||||
|     state | ||||
|         .store | ||||
|         .delete_payment_intents_for_sample_data(merchant_id_del) | ||||
|         .await | ||||
|         .switch()?; | ||||
|     state | ||||
|         .store | ||||
|         .delete_payment_attempts_for_sample_data(merchant_id_del) | ||||
|         .await | ||||
|         .switch()?; | ||||
|     state | ||||
|         .store | ||||
|         .delete_refunds_for_sample_data(merchant_id_del) | ||||
|         .await | ||||
|         .switch()?; | ||||
|  | ||||
|     Ok(ApplicationResponse::StatusOk) | ||||
| } | ||||
| @ -100,6 +100,7 @@ pub trait StorageInterface: | ||||
|     + gsm::GsmInterface | ||||
|     + user::UserInterface | ||||
|     + user_role::UserRoleInterface | ||||
|     + user::sample_data::BatchSampleDataInterface | ||||
|     + 'static | ||||
| { | ||||
|     fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>; | ||||
|  | ||||
| @ -23,7 +23,8 @@ use storage_impl::redis::kv_store::RedisConnInterface; | ||||
| use time::PrimitiveDateTime; | ||||
|  | ||||
| use super::{ | ||||
|     dashboard_metadata::DashboardMetadataInterface, user::UserInterface, | ||||
|     dashboard_metadata::DashboardMetadataInterface, | ||||
|     user::{sample_data::BatchSampleDataInterface, UserInterface}, | ||||
|     user_role::UserRoleInterface, | ||||
| }; | ||||
| use crate::{ | ||||
| @ -1951,3 +1952,118 @@ impl DashboardMetadataInterface for KafkaStore { | ||||
|             .await | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl BatchSampleDataInterface for KafkaStore { | ||||
|     async fn insert_payment_intents_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<data_models::payments::payment_intent::PaymentIntentNew>, | ||||
|     ) -> CustomResult<Vec<data_models::payments::PaymentIntent>, data_models::errors::StorageError> | ||||
|     { | ||||
|         let payment_intents_list = self | ||||
|             .diesel_store | ||||
|             .insert_payment_intents_batch_for_sample_data(batch) | ||||
|             .await?; | ||||
|  | ||||
|         for payment_intent in payment_intents_list.iter() { | ||||
|             let _ = self | ||||
|                 .kafka_producer | ||||
|                 .log_payment_intent(payment_intent, None) | ||||
|                 .await; | ||||
|         } | ||||
|         Ok(payment_intents_list) | ||||
|     } | ||||
|  | ||||
|     async fn insert_payment_attempts_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<diesel_models::user::sample_data::PaymentAttemptBatchNew>, | ||||
|     ) -> CustomResult< | ||||
|         Vec<data_models::payments::payment_attempt::PaymentAttempt>, | ||||
|         data_models::errors::StorageError, | ||||
|     > { | ||||
|         let payment_attempts_list = self | ||||
|             .diesel_store | ||||
|             .insert_payment_attempts_batch_for_sample_data(batch) | ||||
|             .await?; | ||||
|  | ||||
|         for payment_attempt in payment_attempts_list.iter() { | ||||
|             let _ = self | ||||
|                 .kafka_producer | ||||
|                 .log_payment_attempt(payment_attempt, None) | ||||
|                 .await; | ||||
|         } | ||||
|         Ok(payment_attempts_list) | ||||
|     } | ||||
|  | ||||
|     async fn insert_refunds_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<diesel_models::RefundNew>, | ||||
|     ) -> CustomResult<Vec<diesel_models::Refund>, data_models::errors::StorageError> { | ||||
|         let refunds_list = self | ||||
|             .diesel_store | ||||
|             .insert_refunds_batch_for_sample_data(batch) | ||||
|             .await?; | ||||
|  | ||||
|         for refund in refunds_list.iter() { | ||||
|             let _ = self.kafka_producer.log_refund(refund, None).await; | ||||
|         } | ||||
|         Ok(refunds_list) | ||||
|     } | ||||
|  | ||||
|     async fn delete_payment_intents_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<data_models::payments::PaymentIntent>, data_models::errors::StorageError> | ||||
|     { | ||||
|         let payment_intents_list = self | ||||
|             .diesel_store | ||||
|             .delete_payment_intents_for_sample_data(merchant_id) | ||||
|             .await?; | ||||
|  | ||||
|         for payment_intent in payment_intents_list.iter() { | ||||
|             let _ = self | ||||
|                 .kafka_producer | ||||
|                 .log_payment_intent_delete(payment_intent) | ||||
|                 .await; | ||||
|         } | ||||
|         Ok(payment_intents_list) | ||||
|     } | ||||
|  | ||||
|     async fn delete_payment_attempts_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult< | ||||
|         Vec<data_models::payments::payment_attempt::PaymentAttempt>, | ||||
|         data_models::errors::StorageError, | ||||
|     > { | ||||
|         let payment_attempts_list = self | ||||
|             .diesel_store | ||||
|             .delete_payment_attempts_for_sample_data(merchant_id) | ||||
|             .await?; | ||||
|  | ||||
|         for payment_attempt in payment_attempts_list.iter() { | ||||
|             let _ = self | ||||
|                 .kafka_producer | ||||
|                 .log_payment_attempt_delete(payment_attempt) | ||||
|                 .await; | ||||
|         } | ||||
|  | ||||
|         Ok(payment_attempts_list) | ||||
|     } | ||||
|  | ||||
|     async fn delete_refunds_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<diesel_models::Refund>, data_models::errors::StorageError> { | ||||
|         let refunds_list = self | ||||
|             .diesel_store | ||||
|             .delete_refunds_for_sample_data(merchant_id) | ||||
|             .await?; | ||||
|  | ||||
|         for refund in refunds_list.iter() { | ||||
|             let _ = self.kafka_producer.log_refund_delete(refund).await; | ||||
|         } | ||||
|  | ||||
|         Ok(refunds_list) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,6 +8,7 @@ use crate::{ | ||||
|     core::errors::{self, CustomResult}, | ||||
|     services::Store, | ||||
| }; | ||||
| pub mod sample_data; | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| pub trait UserInterface { | ||||
|  | ||||
							
								
								
									
										205
									
								
								crates/router/src/db/user/sample_data.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								crates/router/src/db/user/sample_data.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,205 @@ | ||||
| use data_models::{ | ||||
|     errors::StorageError, | ||||
|     payments::{payment_attempt::PaymentAttempt, payment_intent::PaymentIntentNew, PaymentIntent}, | ||||
| }; | ||||
| use diesel_models::{ | ||||
|     errors::DatabaseError, | ||||
|     query::user::sample_data as sample_data_queries, | ||||
|     refund::{Refund, RefundNew}, | ||||
|     user::sample_data::PaymentAttemptBatchNew, | ||||
| }; | ||||
| use error_stack::{Report, ResultExt}; | ||||
| use storage_impl::DataModelExt; | ||||
|  | ||||
| use crate::{connection::pg_connection_write, core::errors::CustomResult, services::Store}; | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| pub trait BatchSampleDataInterface { | ||||
|     async fn insert_payment_intents_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<PaymentIntentNew>, | ||||
|     ) -> CustomResult<Vec<PaymentIntent>, StorageError>; | ||||
|  | ||||
|     async fn insert_payment_attempts_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<PaymentAttemptBatchNew>, | ||||
|     ) -> CustomResult<Vec<PaymentAttempt>, StorageError>; | ||||
|  | ||||
|     async fn insert_refunds_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<RefundNew>, | ||||
|     ) -> CustomResult<Vec<Refund>, StorageError>; | ||||
|  | ||||
|     async fn delete_payment_intents_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<PaymentIntent>, StorageError>; | ||||
|  | ||||
|     async fn delete_payment_attempts_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<PaymentAttempt>, StorageError>; | ||||
|  | ||||
|     async fn delete_refunds_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<Refund>, StorageError>; | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl BatchSampleDataInterface for Store { | ||||
|     async fn insert_payment_intents_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<PaymentIntentNew>, | ||||
|     ) -> CustomResult<Vec<PaymentIntent>, StorageError> { | ||||
|         let conn = pg_connection_write(self) | ||||
|             .await | ||||
|             .change_context(StorageError::DatabaseConnectionError)?; | ||||
|         let new_intents = batch.into_iter().map(|i| i.to_storage_model()).collect(); | ||||
|         sample_data_queries::insert_payment_intents(&conn, new_intents) | ||||
|             .await | ||||
|             .map_err(diesel_error_to_data_error) | ||||
|             .map(|v| { | ||||
|                 v.into_iter() | ||||
|                     .map(PaymentIntent::from_storage_model) | ||||
|                     .collect() | ||||
|             }) | ||||
|     } | ||||
|  | ||||
|     async fn insert_payment_attempts_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<PaymentAttemptBatchNew>, | ||||
|     ) -> CustomResult<Vec<PaymentAttempt>, StorageError> { | ||||
|         let conn = pg_connection_write(self) | ||||
|             .await | ||||
|             .change_context(StorageError::DatabaseConnectionError)?; | ||||
|         sample_data_queries::insert_payment_attempts(&conn, batch) | ||||
|             .await | ||||
|             .map_err(diesel_error_to_data_error) | ||||
|             .map(|res| { | ||||
|                 res.into_iter() | ||||
|                     .map(PaymentAttempt::from_storage_model) | ||||
|                     .collect() | ||||
|             }) | ||||
|     } | ||||
|     async fn insert_refunds_batch_for_sample_data( | ||||
|         &self, | ||||
|         batch: Vec<RefundNew>, | ||||
|     ) -> CustomResult<Vec<Refund>, StorageError> { | ||||
|         let conn = pg_connection_write(self) | ||||
|             .await | ||||
|             .change_context(StorageError::DatabaseConnectionError)?; | ||||
|         sample_data_queries::insert_refunds(&conn, batch) | ||||
|             .await | ||||
|             .map_err(diesel_error_to_data_error) | ||||
|     } | ||||
|  | ||||
|     async fn delete_payment_intents_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<PaymentIntent>, StorageError> { | ||||
|         let conn = pg_connection_write(self) | ||||
|             .await | ||||
|             .change_context(StorageError::DatabaseConnectionError)?; | ||||
|         sample_data_queries::delete_payment_intents(&conn, merchant_id) | ||||
|             .await | ||||
|             .map_err(diesel_error_to_data_error) | ||||
|             .map(|v| { | ||||
|                 v.into_iter() | ||||
|                     .map(PaymentIntent::from_storage_model) | ||||
|                     .collect() | ||||
|             }) | ||||
|     } | ||||
|  | ||||
|     async fn delete_payment_attempts_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<PaymentAttempt>, StorageError> { | ||||
|         let conn = pg_connection_write(self) | ||||
|             .await | ||||
|             .change_context(StorageError::DatabaseConnectionError)?; | ||||
|         sample_data_queries::delete_payment_attempts(&conn, merchant_id) | ||||
|             .await | ||||
|             .map_err(diesel_error_to_data_error) | ||||
|             .map(|res| { | ||||
|                 res.into_iter() | ||||
|                     .map(PaymentAttempt::from_storage_model) | ||||
|                     .collect() | ||||
|             }) | ||||
|     } | ||||
|     async fn delete_refunds_for_sample_data( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<Refund>, StorageError> { | ||||
|         let conn = pg_connection_write(self) | ||||
|             .await | ||||
|             .change_context(StorageError::DatabaseConnectionError)?; | ||||
|         sample_data_queries::delete_refunds(&conn, merchant_id) | ||||
|             .await | ||||
|             .map_err(diesel_error_to_data_error) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl BatchSampleDataInterface for storage_impl::MockDb { | ||||
|     async fn insert_payment_intents_batch_for_sample_data( | ||||
|         &self, | ||||
|         _batch: Vec<PaymentIntentNew>, | ||||
|     ) -> CustomResult<Vec<PaymentIntent>, StorageError> { | ||||
|         Err(StorageError::MockDbError)? | ||||
|     } | ||||
|  | ||||
|     async fn insert_payment_attempts_batch_for_sample_data( | ||||
|         &self, | ||||
|         _batch: Vec<PaymentAttemptBatchNew>, | ||||
|     ) -> CustomResult<Vec<PaymentAttempt>, StorageError> { | ||||
|         Err(StorageError::MockDbError)? | ||||
|     } | ||||
|  | ||||
|     async fn insert_refunds_batch_for_sample_data( | ||||
|         &self, | ||||
|         _batch: Vec<RefundNew>, | ||||
|     ) -> CustomResult<Vec<Refund>, StorageError> { | ||||
|         Err(StorageError::MockDbError)? | ||||
|     } | ||||
|  | ||||
|     async fn delete_payment_intents_for_sample_data( | ||||
|         &self, | ||||
|         _merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<PaymentIntent>, StorageError> { | ||||
|         Err(StorageError::MockDbError)? | ||||
|     } | ||||
|     async fn delete_payment_attempts_for_sample_data( | ||||
|         &self, | ||||
|         _merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<PaymentAttempt>, StorageError> { | ||||
|         Err(StorageError::MockDbError)? | ||||
|     } | ||||
|     async fn delete_refunds_for_sample_data( | ||||
|         &self, | ||||
|         _merchant_id: &str, | ||||
|     ) -> CustomResult<Vec<Refund>, StorageError> { | ||||
|         Err(StorageError::MockDbError)? | ||||
|     } | ||||
| } | ||||
|  | ||||
| // TODO: This error conversion is re-used from storage_impl and is not DRY when it should be | ||||
| // Ideally the impl's here should be defined in that crate avoiding this re-definition | ||||
| fn diesel_error_to_data_error(diesel_error: Report<DatabaseError>) -> Report<StorageError> { | ||||
|     let new_err = match diesel_error.current_context() { | ||||
|         DatabaseError::DatabaseConnectionError => StorageError::DatabaseConnectionError, | ||||
|         DatabaseError::NotFound => StorageError::ValueNotFound("Value not found".to_string()), | ||||
|         DatabaseError::UniqueViolation => StorageError::DuplicateValue { | ||||
|             entity: "entity ", | ||||
|             key: None, | ||||
|         }, | ||||
|         DatabaseError::NoFieldsToUpdate => { | ||||
|             StorageError::DatabaseError("No fields to update".to_string()) | ||||
|         } | ||||
|         DatabaseError::QueryGenerationFailed => { | ||||
|             StorageError::DatabaseError("Query generation failed".to_string()) | ||||
|         } | ||||
|         DatabaseError::Others => StorageError::DatabaseError("Others".to_string()), | ||||
|     }; | ||||
|     diesel_error.change_context(new_err) | ||||
| } | ||||
| @ -820,8 +820,9 @@ pub struct User; | ||||
| #[cfg(feature = "olap")] | ||||
| impl User { | ||||
|     pub fn server(state: AppState) -> Scope { | ||||
|         web::scope("/user") | ||||
|             .app_data(web::Data::new(state)) | ||||
|         let mut route = web::scope("/user").app_data(web::Data::new(state)); | ||||
|  | ||||
|         route = route | ||||
|             .service(web::resource("/signin").route(web::post().to(user_connect_account))) | ||||
|             .service(web::resource("/signup").route(web::post().to(user_connect_account))) | ||||
|             .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) | ||||
| @ -842,7 +843,17 @@ impl User { | ||||
|             .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) | ||||
|             .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) | ||||
|             .service(web::resource("/role/list").route(web::get().to(list_roles))) | ||||
|             .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) | ||||
|             .service(web::resource("/role/{role_id}").route(web::get().to(get_role))); | ||||
|  | ||||
|         #[cfg(feature = "dummy_connector")] | ||||
|         { | ||||
|             route = route.service( | ||||
|                 web::resource("/sample_data") | ||||
|                     .route(web::post().to(generate_sample_data)) | ||||
|                     .route(web::delete().to(delete_sample_data)), | ||||
|             ) | ||||
|         } | ||||
|         route | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -155,7 +155,9 @@ impl From<Flow> for ApiIdentifier { | ||||
|             | Flow::VerifyPaymentConnector | ||||
|             | Flow::InternalUserSignup | ||||
|             | Flow::SwitchMerchant | ||||
|             | Flow::UserMerchantAccountCreate => Self::User, | ||||
|             | Flow::UserMerchantAccountCreate | ||||
|             | Flow::GenerateSampleData | ||||
|             | Flow::DeleteSampleData => Self::User, | ||||
|  | ||||
|             Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { | ||||
|                 Self::UserRole | ||||
|  | ||||
| @ -1,5 +1,10 @@ | ||||
| use actix_web::{web, HttpRequest, HttpResponse}; | ||||
| use api_models::{errors::types::ApiErrorResponse, user as user_api}; | ||||
| #[cfg(feature = "dummy_connector")] | ||||
| use api_models::user::sample_data::SampleDataRequest; | ||||
| use api_models::{ | ||||
|     errors::types::ApiErrorResponse, | ||||
|     user::{self as user_api}, | ||||
| }; | ||||
| use common_utils::errors::ReportSwitchExt; | ||||
| use router_env::Flow; | ||||
|  | ||||
| @ -158,3 +163,44 @@ pub async fn user_merchant_account_create( | ||||
|     )) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "dummy_connector")] | ||||
| pub async fn generate_sample_data( | ||||
|     state: web::Data<AppState>, | ||||
|     http_req: HttpRequest, | ||||
|     payload: web::Json<SampleDataRequest>, | ||||
| ) -> impl actix_web::Responder { | ||||
|     use crate::core::user::sample_data; | ||||
|  | ||||
|     let flow = Flow::GenerateSampleData; | ||||
|     Box::pin(api::server_wrap( | ||||
|         flow, | ||||
|         state, | ||||
|         &http_req, | ||||
|         payload.into_inner(), | ||||
|         sample_data::generate_sample_data_for_user, | ||||
|         &auth::JWTAuth(Permission::MerchantAccountWrite), | ||||
|         api_locking::LockAction::NotApplicable, | ||||
|     )) | ||||
|     .await | ||||
| } | ||||
| #[cfg(feature = "dummy_connector")] | ||||
| pub async fn delete_sample_data( | ||||
|     state: web::Data<AppState>, | ||||
|     http_req: HttpRequest, | ||||
|     payload: web::Json<SampleDataRequest>, | ||||
| ) -> impl actix_web::Responder { | ||||
|     use crate::core::user::sample_data; | ||||
|  | ||||
|     let flow = Flow::DeleteSampleData; | ||||
|     Box::pin(api::server_wrap( | ||||
|         flow, | ||||
|         state, | ||||
|         &http_req, | ||||
|         payload.into_inner(), | ||||
|         sample_data::delete_sample_data_for_user, | ||||
|         &auth::JWTAuth(Permission::MerchantAccountWrite), | ||||
|         api_locking::LockAction::NotApplicable, | ||||
|     )) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| @ -9,6 +9,8 @@ use crate::{ | ||||
|  | ||||
| pub mod dashboard_metadata; | ||||
| pub mod password; | ||||
| #[cfg(feature = "dummy_connector")] | ||||
| pub mod sample_data; | ||||
|  | ||||
| impl UserFromToken { | ||||
|     pub async fn get_merchant_account(&self, state: AppState) -> UserResult<MerchantAccount> { | ||||
|  | ||||
							
								
								
									
										291
									
								
								crates/router/src/utils/user/sample_data.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								crates/router/src/utils/user/sample_data.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,291 @@ | ||||
| use api_models::{ | ||||
|     enums::Connector::{DummyConnector4, DummyConnector7}, | ||||
|     user::sample_data::SampleDataRequest, | ||||
| }; | ||||
| use data_models::payments::payment_intent::PaymentIntentNew; | ||||
| use diesel_models::{user::sample_data::PaymentAttemptBatchNew, RefundNew}; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use rand::{prelude::SliceRandom, thread_rng, Rng}; | ||||
| use time::OffsetDateTime; | ||||
|  | ||||
| use crate::{ | ||||
|     consts, | ||||
|     core::errors::sample_data::{SampleDataError, SampleDataResult}, | ||||
|     AppState, | ||||
| }; | ||||
|  | ||||
| #[allow(clippy::type_complexity)] | ||||
| pub async fn generate_sample_data( | ||||
|     state: &AppState, | ||||
|     req: SampleDataRequest, | ||||
|     merchant_id: &str, | ||||
| ) -> SampleDataResult<Vec<(PaymentIntentNew, PaymentAttemptBatchNew, Option<RefundNew>)>> { | ||||
|     let merchant_id = merchant_id.to_string(); | ||||
|     let sample_data_size: usize = req.record.unwrap_or(100); | ||||
|  | ||||
|     if !(10..=100).contains(&sample_data_size) { | ||||
|         return Err(SampleDataError::InvalidRange.into()); | ||||
|     } | ||||
|  | ||||
|     let key_store = state | ||||
|         .store | ||||
|         .get_merchant_key_store_by_merchant_id( | ||||
|             merchant_id.as_str(), | ||||
|             &state.store.get_master_key().to_vec().into(), | ||||
|         ) | ||||
|         .await | ||||
|         .change_context(SampleDataError::DatabaseError)?; | ||||
|  | ||||
|     let merchant_from_db = state | ||||
|         .store | ||||
|         .find_merchant_account_by_merchant_id(merchant_id.as_str(), &key_store) | ||||
|         .await | ||||
|         .change_context::<SampleDataError>(SampleDataError::DataDoesNotExist)?; | ||||
|  | ||||
|     let merchant_parsed_details: Vec<api_models::admin::PrimaryBusinessDetails> = | ||||
|         serde_json::from_value(merchant_from_db.primary_business_details.clone()) | ||||
|             .into_report() | ||||
|             .change_context(SampleDataError::InternalServerError) | ||||
|             .attach_printable("Error while parsing primary business details")?; | ||||
|  | ||||
|     let business_country_default = merchant_parsed_details.get(0).map(|x| x.country); | ||||
|  | ||||
|     let business_label_default = merchant_parsed_details.get(0).map(|x| x.business.clone()); | ||||
|  | ||||
|     let profile_id = crate::core::utils::get_profile_id_from_business_details( | ||||
|         business_country_default, | ||||
|         business_label_default.as_ref(), | ||||
|         &merchant_from_db, | ||||
|         req.profile_id.as_ref(), | ||||
|         &*state.store, | ||||
|         false, | ||||
|     ) | ||||
|     .await | ||||
|     .change_context(SampleDataError::InternalServerError) | ||||
|     .attach_printable("Failed to get business profile")?; | ||||
|  | ||||
|     // 10 percent payments should be failed | ||||
|     #[allow(clippy::as_conversions)] | ||||
|     let failure_attempts = usize::try_from((sample_data_size as f32 / 10.0).round() as i64) | ||||
|         .into_report() | ||||
|         .change_context(SampleDataError::InvalidParameters)?; | ||||
|  | ||||
|     let failure_after_attempts = sample_data_size / failure_attempts; | ||||
|  | ||||
|     // 20 percent refunds for payments | ||||
|     #[allow(clippy::as_conversions)] | ||||
|     let number_of_refunds = usize::try_from((sample_data_size as f32 / 5.0).round() as i64) | ||||
|         .into_report() | ||||
|         .change_context(SampleDataError::InvalidParameters)?; | ||||
|  | ||||
|     let mut refunds_count = 0; | ||||
|  | ||||
|     let mut random_array: Vec<usize> = (1..=sample_data_size).collect(); | ||||
|  | ||||
|     // Shuffle the array | ||||
|     let mut rng = thread_rng(); | ||||
|     random_array.shuffle(&mut rng); | ||||
|  | ||||
|     let mut res: Vec<(PaymentIntentNew, PaymentAttemptBatchNew, Option<RefundNew>)> = Vec::new(); | ||||
|     let start_time = req | ||||
|         .start_time | ||||
|         .unwrap_or(common_utils::date_time::now() - time::Duration::days(7)) | ||||
|         .assume_utc() | ||||
|         .unix_timestamp(); | ||||
|     let end_time = req | ||||
|         .end_time | ||||
|         .unwrap_or_else(common_utils::date_time::now) | ||||
|         .assume_utc() | ||||
|         .unix_timestamp(); | ||||
|  | ||||
|     let current_time = common_utils::date_time::now().assume_utc().unix_timestamp(); | ||||
|  | ||||
|     let min_amount = req.min_amount.unwrap_or(100); | ||||
|     let max_amount = req.max_amount.unwrap_or(min_amount + 100); | ||||
|  | ||||
|     if min_amount > max_amount | ||||
|         || start_time > end_time | ||||
|         || start_time > current_time | ||||
|         || end_time > current_time | ||||
|     { | ||||
|         return Err(SampleDataError::InvalidParameters.into()); | ||||
|     }; | ||||
|  | ||||
|     let currency_vec = req.currency.unwrap_or(vec![common_enums::Currency::USD]); | ||||
|     let currency_vec_len = currency_vec.len(); | ||||
|  | ||||
|     let connector_vec = req | ||||
|         .connector | ||||
|         .unwrap_or(vec![DummyConnector4, DummyConnector7]); | ||||
|     let connector_vec_len = connector_vec.len(); | ||||
|  | ||||
|     let auth_type = req.auth_type.unwrap_or(vec![ | ||||
|         common_enums::AuthenticationType::ThreeDs, | ||||
|         common_enums::AuthenticationType::NoThreeDs, | ||||
|     ]); | ||||
|     let auth_type_len = auth_type.len(); | ||||
|  | ||||
|     if currency_vec_len == 0 || connector_vec_len == 0 || auth_type_len == 0 { | ||||
|         return Err(SampleDataError::InvalidParameters.into()); | ||||
|     } | ||||
|  | ||||
|     for num in 1..=sample_data_size { | ||||
|         let payment_id = common_utils::generate_id_with_default_len("test"); | ||||
|         let attempt_id = crate::utils::get_payment_attempt_id(&payment_id, 1); | ||||
|         let client_secret = common_utils::generate_id( | ||||
|             consts::ID_LENGTH, | ||||
|             format!("{}_secret", payment_id.clone()).as_str(), | ||||
|         ); | ||||
|         let amount = thread_rng().gen_range(min_amount..=max_amount); | ||||
|  | ||||
|         let created_at @ modified_at @ last_synced = | ||||
|             OffsetDateTime::from_unix_timestamp(thread_rng().gen_range(start_time..=end_time)) | ||||
|                 .map(common_utils::date_time::convert_to_pdt) | ||||
|                 .unwrap_or( | ||||
|                     req.start_time.unwrap_or_else(|| { | ||||
|                         common_utils::date_time::now() - time::Duration::days(7) | ||||
|                     }), | ||||
|                 ); | ||||
|  | ||||
|         // After some set of payments sample data will have a failed attempt | ||||
|         let is_failed_payment = | ||||
|             (random_array.get(num - 1).unwrap_or(&0) % failure_after_attempts) == 0; | ||||
|  | ||||
|         let payment_intent = PaymentIntentNew { | ||||
|             payment_id: payment_id.clone(), | ||||
|             merchant_id: merchant_id.clone(), | ||||
|             status: match is_failed_payment { | ||||
|                 true => common_enums::IntentStatus::Failed, | ||||
|                 _ => common_enums::IntentStatus::Succeeded, | ||||
|             }, | ||||
|             amount: amount * 100, | ||||
|             currency: Some( | ||||
|                 *currency_vec | ||||
|                     .get((num - 1) % currency_vec_len) | ||||
|                     .unwrap_or(&common_enums::Currency::USD), | ||||
|             ), | ||||
|             description: Some("This is a sample payment".to_string()), | ||||
|             created_at: Some(created_at), | ||||
|             modified_at: Some(modified_at), | ||||
|             last_synced: Some(last_synced), | ||||
|             client_secret: Some(client_secret), | ||||
|             business_country: business_country_default, | ||||
|             business_label: business_label_default.clone(), | ||||
|             active_attempt: data_models::RemoteStorageObject::ForeignID(attempt_id.clone()), | ||||
|             attempt_count: 1, | ||||
|             customer_id: Some("hs-dashboard-user".to_string()), | ||||
|             amount_captured: Some(amount * 100), | ||||
|             profile_id: Some(profile_id.clone()), | ||||
|             return_url: Default::default(), | ||||
|             metadata: Default::default(), | ||||
|             connector_id: Default::default(), | ||||
|             shipping_address_id: Default::default(), | ||||
|             billing_address_id: Default::default(), | ||||
|             statement_descriptor_name: Default::default(), | ||||
|             statement_descriptor_suffix: Default::default(), | ||||
|             setup_future_usage: Default::default(), | ||||
|             off_session: Default::default(), | ||||
|             order_details: Default::default(), | ||||
|             allowed_payment_method_types: Default::default(), | ||||
|             connector_metadata: Default::default(), | ||||
|             feature_metadata: Default::default(), | ||||
|             merchant_decision: Default::default(), | ||||
|             payment_link_id: Default::default(), | ||||
|             payment_confirm_source: Default::default(), | ||||
|             updated_by: merchant_from_db.storage_scheme.to_string(), | ||||
|             surcharge_applicable: Default::default(), | ||||
|             request_incremental_authorization: Default::default(), | ||||
|             incremental_authorization_allowed: Default::default(), | ||||
|         }; | ||||
|         let payment_attempt = PaymentAttemptBatchNew { | ||||
|             attempt_id: attempt_id.clone(), | ||||
|             payment_id: payment_id.clone(), | ||||
|             connector_transaction_id: Some(attempt_id.clone()), | ||||
|             merchant_id: merchant_id.clone(), | ||||
|             status: match is_failed_payment { | ||||
|                 true => common_enums::AttemptStatus::Failure, | ||||
|                 _ => common_enums::AttemptStatus::Charged, | ||||
|             }, | ||||
|             amount: amount * 100, | ||||
|             currency: payment_intent.currency, | ||||
|             connector: Some( | ||||
|                 (*connector_vec | ||||
|                     .get((num - 1) % connector_vec_len) | ||||
|                     .unwrap_or(&DummyConnector4)) | ||||
|                 .to_string(), | ||||
|             ), | ||||
|             payment_method: Some(common_enums::PaymentMethod::Card), | ||||
|             payment_method_type: Some(get_payment_method_type(thread_rng().gen_range(1..=2))), | ||||
|             authentication_type: Some( | ||||
|                 *auth_type | ||||
|                     .get((num - 1) % auth_type_len) | ||||
|                     .unwrap_or(&common_enums::AuthenticationType::NoThreeDs), | ||||
|             ), | ||||
|             error_message: match is_failed_payment { | ||||
|                 true => Some("This is a test payment which has a failed status".to_string()), | ||||
|                 _ => None, | ||||
|             }, | ||||
|             error_code: match is_failed_payment { | ||||
|                 true => Some("HS001".to_string()), | ||||
|                 _ => None, | ||||
|             }, | ||||
|             confirm: true, | ||||
|             created_at: Some(created_at), | ||||
|             modified_at: Some(modified_at), | ||||
|             last_synced: Some(last_synced), | ||||
|             amount_to_capture: Some(amount * 100), | ||||
|             connector_response_reference_id: Some(attempt_id.clone()), | ||||
|             updated_by: merchant_from_db.storage_scheme.to_string(), | ||||
|  | ||||
|             ..Default::default() | ||||
|         }; | ||||
|  | ||||
|         let refund = if refunds_count < number_of_refunds && !is_failed_payment { | ||||
|             refunds_count += 1; | ||||
|             Some(RefundNew { | ||||
|                 refund_id: common_utils::generate_id_with_default_len("test"), | ||||
|                 internal_reference_id: common_utils::generate_id_with_default_len("test"), | ||||
|                 external_reference_id: None, | ||||
|                 payment_id: payment_id.clone(), | ||||
|                 attempt_id: attempt_id.clone(), | ||||
|                 merchant_id: merchant_id.clone(), | ||||
|                 connector_transaction_id: attempt_id.clone(), | ||||
|                 connector_refund_id: None, | ||||
|                 description: Some("This is a sample refund".to_string()), | ||||
|                 created_at: Some(created_at), | ||||
|                 modified_at: Some(modified_at), | ||||
|                 refund_reason: Some("Sample Refund".to_string()), | ||||
|                 connector: payment_attempt | ||||
|                     .connector | ||||
|                     .clone() | ||||
|                     .unwrap_or(DummyConnector4.to_string()), | ||||
|                 currency: *currency_vec | ||||
|                     .get((num - 1) % currency_vec_len) | ||||
|                     .unwrap_or(&common_enums::Currency::USD), | ||||
|                 total_amount: amount * 100, | ||||
|                 refund_amount: amount * 100, | ||||
|                 refund_status: common_enums::RefundStatus::Success, | ||||
|                 sent_to_gateway: true, | ||||
|                 refund_type: diesel_models::enums::RefundType::InstantRefund, | ||||
|                 metadata: None, | ||||
|                 refund_arn: None, | ||||
|                 profile_id: payment_intent.profile_id.clone(), | ||||
|                 updated_by: merchant_from_db.storage_scheme.to_string(), | ||||
|                 merchant_connector_id: payment_attempt.merchant_connector_id.clone(), | ||||
|             }) | ||||
|         } else { | ||||
|             None | ||||
|         }; | ||||
|  | ||||
|         res.push((payment_intent, payment_attempt, refund)); | ||||
|     } | ||||
|     Ok(res) | ||||
| } | ||||
|  | ||||
| fn get_payment_method_type(num: u8) -> common_enums::PaymentMethodType { | ||||
|     let rem: u8 = (num) % 2; | ||||
|     match rem { | ||||
|         0 => common_enums::PaymentMethodType::Debit, | ||||
|         _ => common_enums::PaymentMethodType::Credit, | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Apoorv Dixit
					Apoorv Dixit