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:
Apoorv Dixit
2023-12-01 16:00:25 +05:30
committed by GitHub
parent c4bd47eca9
commit 092ec73b3c
22 changed files with 1152 additions and 19 deletions

View File

@ -1,5 +1,7 @@
use common_utils::events::{ApiEventMetric, ApiEventsType}; use common_utils::events::{ApiEventMetric, ApiEventsType};
#[cfg(feature = "dummy_connector")]
use crate::user::sample_data::SampleDataRequest;
use crate::user::{ use crate::user::{
dashboard_metadata::{ dashboard_metadata::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
@ -29,3 +31,6 @@ common_utils::impl_misc_api_event_type!(
CreateInternalUserRequest, CreateInternalUserRequest,
UserMerchantCreate UserMerchantCreate
); );
#[cfg(feature = "dummy_connector")]
common_utils::impl_misc_api_event_type!(SampleDataRequest);

View File

@ -1,6 +1,8 @@
use common_utils::pii; use common_utils::pii;
use masking::Secret; use masking::Secret;
pub mod dashboard_metadata; pub mod dashboard_metadata;
#[cfg(feature = "dummy_connector")]
pub mod sample_data;
#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)]
pub struct ConnectAccountRequest { pub struct ConnectAccountRequest {

View File

@ -0,0 +1,23 @@
use common_enums::{AuthenticationType, CountryAlpha2};
use common_utils::{self};
use time::PrimitiveDateTime;
use crate::enums::Connector;
#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct SampleDataRequest {
pub record: Option<usize>,
pub connector: Option<Vec<Connector>>,
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub start_time: Option<PrimitiveDateTime>,
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub end_time: Option<PrimitiveDateTime>,
// The amount for each sample will be between min_amount and max_amount (in dollars)
pub min_amount: Option<i64>,
pub max_amount: Option<i64>,
pub currency: Option<Vec<common_enums::Currency>>,
pub auth_type: Option<Vec<AuthenticationType>>,
pub business_country: Option<CountryAlpha2>,
pub business_label: Option<String>,
pub profile_id: Option<String>,
}

View File

@ -1,6 +1,7 @@
use diesel::{associations::HasTable, ExpressionMethods}; use diesel::{associations::HasTable, ExpressionMethods};
use error_stack::report; use error_stack::report;
use router_env::tracing::{self, instrument}; use router_env::tracing::{self, instrument};
pub mod sample_data;
use crate::{ use crate::{
errors::{self}, errors::{self},

View File

@ -0,0 +1,139 @@
use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::{associations::HasTable, debug_query, ExpressionMethods, TextExpressionMethods};
use error_stack::{IntoReport, ResultExt};
use router_env::logger;
use crate::{
errors,
schema::{
payment_attempt::dsl as payment_attempt_dsl, payment_intent::dsl as payment_intent_dsl,
refund::dsl as refund_dsl,
},
user::sample_data::PaymentAttemptBatchNew,
PaymentAttempt, PaymentIntent, PaymentIntentNew, PgPooledConn, Refund, RefundNew,
StorageResult,
};
pub async fn insert_payment_intents(
conn: &PgPooledConn,
batch: Vec<PaymentIntentNew>,
) -> StorageResult<Vec<PaymentIntent>> {
let query = diesel::insert_into(<PaymentIntent>::table()).values(batch);
logger::debug!(query = %debug_query::<diesel::pg::Pg,_>(&query).to_string());
query
.get_results_async(conn)
.await
.into_report()
.change_context(errors::DatabaseError::Others)
.attach_printable("Error while inserting payment intents")
}
pub async fn insert_payment_attempts(
conn: &PgPooledConn,
batch: Vec<PaymentAttemptBatchNew>,
) -> StorageResult<Vec<PaymentAttempt>> {
let query = diesel::insert_into(<PaymentAttempt>::table()).values(batch);
logger::debug!(query = %debug_query::<diesel::pg::Pg,_>(&query).to_string());
query
.get_results_async(conn)
.await
.into_report()
.change_context(errors::DatabaseError::Others)
.attach_printable("Error while inserting payment attempts")
}
pub async fn insert_refunds(
conn: &PgPooledConn,
batch: Vec<RefundNew>,
) -> StorageResult<Vec<Refund>> {
let query = diesel::insert_into(<Refund>::table()).values(batch);
logger::debug!(query = %debug_query::<diesel::pg::Pg,_>(&query).to_string());
query
.get_results_async(conn)
.await
.into_report()
.change_context(errors::DatabaseError::Others)
.attach_printable("Error while inserting refunds")
}
pub async fn delete_payment_intents(
conn: &PgPooledConn,
merchant_id: &str,
) -> StorageResult<Vec<PaymentIntent>> {
let query = diesel::delete(<PaymentIntent>::table())
.filter(payment_intent_dsl::merchant_id.eq(merchant_id.to_owned()))
.filter(payment_intent_dsl::payment_id.like("test_%"));
logger::debug!(query = %debug_query::<diesel::pg::Pg,_>(&query).to_string());
query
.get_results_async(conn)
.await
.into_report()
.change_context(errors::DatabaseError::Others)
.attach_printable("Error while deleting payment intents")
.and_then(|result| match result.len() {
n if n > 0 => {
logger::debug!("{n} records deleted");
Ok(result)
}
0 => Err(error_stack::report!(errors::DatabaseError::NotFound)
.attach_printable("No records deleted")),
_ => Ok(result),
})
}
pub async fn delete_payment_attempts(
conn: &PgPooledConn,
merchant_id: &str,
) -> StorageResult<Vec<PaymentAttempt>> {
let query = diesel::delete(<PaymentAttempt>::table())
.filter(payment_attempt_dsl::merchant_id.eq(merchant_id.to_owned()))
.filter(payment_attempt_dsl::payment_id.like("test_%"));
logger::debug!(query = %debug_query::<diesel::pg::Pg,_>(&query).to_string());
query
.get_results_async(conn)
.await
.into_report()
.change_context(errors::DatabaseError::Others)
.attach_printable("Error while deleting payment attempts")
.and_then(|result| match result.len() {
n if n > 0 => {
logger::debug!("{n} records deleted");
Ok(result)
}
0 => Err(error_stack::report!(errors::DatabaseError::NotFound)
.attach_printable("No records deleted")),
_ => Ok(result),
})
}
pub async fn delete_refunds(conn: &PgPooledConn, merchant_id: &str) -> StorageResult<Vec<Refund>> {
let query = diesel::delete(<Refund>::table())
.filter(refund_dsl::merchant_id.eq(merchant_id.to_owned()))
.filter(refund_dsl::payment_id.like("test_%"));
logger::debug!(query = %debug_query::<diesel::pg::Pg,_>(&query).to_string());
query
.get_results_async(conn)
.await
.into_report()
.change_context(errors::DatabaseError::Others)
.attach_printable("Error while deleting refunds")
.and_then(|result| match result.len() {
n if n > 0 => {
logger::debug!("{n} records deleted");
Ok(result)
}
0 => Err(error_stack::report!(errors::DatabaseError::NotFound)
.attach_printable("No records deleted")),
_ => Ok(result),
})
}

View File

@ -7,6 +7,7 @@ use crate::schema::users;
pub mod dashboard_metadata; pub mod dashboard_metadata;
pub mod sample_data;
#[derive(Clone, Debug, Identifiable, Queryable)] #[derive(Clone, Debug, Identifiable, Queryable)]
#[diesel(table_name = users)] #[diesel(table_name = users)]
pub struct User { pub struct User {

View File

@ -0,0 +1,119 @@
use common_enums::{
AttemptStatus, AuthenticationType, CaptureMethod, Currency, PaymentExperience, PaymentMethod,
PaymentMethodType,
};
use serde::{Deserialize, Serialize};
use time::PrimitiveDateTime;
use crate::{enums::MandateDataType, schema::payment_attempt, PaymentAttemptNew};
#[derive(
Clone, Debug, Default, diesel::Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize,
)]
#[diesel(table_name = payment_attempt)]
pub struct PaymentAttemptBatchNew {
pub payment_id: String,
pub merchant_id: String,
pub attempt_id: String,
pub status: AttemptStatus,
pub amount: i64,
pub currency: Option<Currency>,
pub save_to_locker: Option<bool>,
pub connector: Option<String>,
pub error_message: Option<String>,
pub offer_amount: Option<i64>,
pub surcharge_amount: Option<i64>,
pub tax_amount: Option<i64>,
pub payment_method_id: Option<String>,
pub payment_method: Option<PaymentMethod>,
pub capture_method: Option<CaptureMethod>,
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub capture_on: Option<PrimitiveDateTime>,
pub confirm: bool,
pub authentication_type: Option<AuthenticationType>,
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub created_at: Option<PrimitiveDateTime>,
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub modified_at: Option<PrimitiveDateTime>,
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub last_synced: Option<PrimitiveDateTime>,
pub cancellation_reason: Option<String>,
pub amount_to_capture: Option<i64>,
pub mandate_id: Option<String>,
pub browser_info: Option<serde_json::Value>,
pub payment_token: Option<String>,
pub error_code: Option<String>,
pub connector_metadata: Option<serde_json::Value>,
pub payment_experience: Option<PaymentExperience>,
pub payment_method_type: Option<PaymentMethodType>,
pub payment_method_data: Option<serde_json::Value>,
pub business_sub_label: Option<String>,
pub straight_through_algorithm: Option<serde_json::Value>,
pub preprocessing_step_id: Option<String>,
pub mandate_details: Option<MandateDataType>,
pub error_reason: Option<String>,
pub connector_response_reference_id: Option<String>,
pub connector_transaction_id: Option<String>,
pub multiple_capture_count: Option<i16>,
pub amount_capturable: i64,
pub updated_by: String,
pub merchant_connector_id: Option<String>,
pub authentication_data: Option<serde_json::Value>,
pub encoded_data: Option<String>,
pub unified_code: Option<String>,
pub unified_message: Option<String>,
}
#[allow(dead_code)]
impl PaymentAttemptBatchNew {
// Used to verify compatibility with PaymentAttemptTable
fn convert_into_normal_attempt_insert(self) -> PaymentAttemptNew {
PaymentAttemptNew {
payment_id: self.payment_id,
merchant_id: self.merchant_id,
attempt_id: self.attempt_id,
status: self.status,
amount: self.amount,
currency: self.currency,
save_to_locker: self.save_to_locker,
connector: self.connector,
error_message: self.error_message,
offer_amount: self.offer_amount,
surcharge_amount: self.surcharge_amount,
tax_amount: self.tax_amount,
payment_method_id: self.payment_method_id,
payment_method: self.payment_method,
capture_method: self.capture_method,
capture_on: self.capture_on,
confirm: self.confirm,
authentication_type: self.authentication_type,
created_at: self.created_at,
modified_at: self.modified_at,
last_synced: self.last_synced,
cancellation_reason: self.cancellation_reason,
amount_to_capture: self.amount_to_capture,
mandate_id: self.mandate_id,
browser_info: self.browser_info,
payment_token: self.payment_token,
error_code: self.error_code,
connector_metadata: self.connector_metadata,
payment_experience: self.payment_experience,
payment_method_type: self.payment_method_type,
payment_method_data: self.payment_method_data,
business_sub_label: self.business_sub_label,
straight_through_algorithm: self.straight_through_algorithm,
preprocessing_step_id: self.preprocessing_step_id,
mandate_details: self.mandate_details,
error_reason: self.error_reason,
multiple_capture_count: self.multiple_capture_count,
connector_response_reference_id: self.connector_response_reference_id,
amount_capturable: self.amount_capturable,
updated_by: self.updated_by,
merchant_connector_id: self.merchant_connector_id,
authentication_data: self.authentication_data,
encoded_data: self.encoded_data,
unified_code: self.unified_code,
unified_message: self.unified_message,
}
}
}

View File

@ -4,6 +4,7 @@ use crate::services::ApplicationResponse;
pub type UserResult<T> = CustomResult<T, UserErrors>; pub type UserResult<T> = CustomResult<T, UserErrors>;
pub type UserResponse<T> = CustomResult<ApplicationResponse<T>, UserErrors>; pub type UserResponse<T> = CustomResult<ApplicationResponse<T>, UserErrors>;
pub mod sample_data;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum UserErrors { pub enum UserErrors {

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

View File

@ -13,6 +13,8 @@ use crate::{
types::domain, types::domain,
utils, utils,
}; };
#[cfg(feature = "dummy_connector")]
pub mod sample_data;
pub mod dashboard_metadata; pub mod dashboard_metadata;

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

View File

@ -100,6 +100,7 @@ pub trait StorageInterface:
+ gsm::GsmInterface + gsm::GsmInterface
+ user::UserInterface + user::UserInterface
+ user_role::UserRoleInterface + user_role::UserRoleInterface
+ user::sample_data::BatchSampleDataInterface
+ 'static + 'static
{ {
fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>; fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>;

View File

@ -23,7 +23,8 @@ use storage_impl::redis::kv_store::RedisConnInterface;
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
use super::{ use super::{
dashboard_metadata::DashboardMetadataInterface, user::UserInterface, dashboard_metadata::DashboardMetadataInterface,
user::{sample_data::BatchSampleDataInterface, UserInterface},
user_role::UserRoleInterface, user_role::UserRoleInterface,
}; };
use crate::{ use crate::{
@ -1951,3 +1952,118 @@ impl DashboardMetadataInterface for KafkaStore {
.await .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)
}
}

View File

@ -8,6 +8,7 @@ use crate::{
core::errors::{self, CustomResult}, core::errors::{self, CustomResult},
services::Store, services::Store,
}; };
pub mod sample_data;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait UserInterface { pub trait UserInterface {

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

View File

@ -820,8 +820,9 @@ pub struct User;
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
impl User { impl User {
pub fn server(state: AppState) -> Scope { pub fn server(state: AppState) -> Scope {
web::scope("/user") let mut route = web::scope("/user").app_data(web::Data::new(state));
.app_data(web::Data::new(state))
route = route
.service(web::resource("/signin").route(web::post().to(user_connect_account))) .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("/signup").route(web::post().to(user_connect_account)))
.service(web::resource("/v2/signin").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("/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("/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/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
} }
} }

View File

@ -155,7 +155,9 @@ impl From<Flow> for ApiIdentifier {
| Flow::VerifyPaymentConnector | Flow::VerifyPaymentConnector
| Flow::InternalUserSignup | Flow::InternalUserSignup
| Flow::SwitchMerchant | Flow::SwitchMerchant
| Flow::UserMerchantAccountCreate => Self::User, | Flow::UserMerchantAccountCreate
| Flow::GenerateSampleData
| Flow::DeleteSampleData => Self::User,
Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => {
Self::UserRole Self::UserRole

View File

@ -1,5 +1,10 @@
use actix_web::{web, HttpRequest, HttpResponse}; 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 common_utils::errors::ReportSwitchExt;
use router_env::Flow; use router_env::Flow;
@ -158,3 +163,44 @@ pub async fn user_merchant_account_create(
)) ))
.await .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
}

View File

@ -9,6 +9,8 @@ use crate::{
pub mod dashboard_metadata; pub mod dashboard_metadata;
pub mod password; pub mod password;
#[cfg(feature = "dummy_connector")]
pub mod sample_data;
impl UserFromToken { impl UserFromToken {
pub async fn get_merchant_account(&self, state: AppState) -> UserResult<MerchantAccount> { pub async fn get_merchant_account(&self, state: AppState) -> UserResult<MerchantAccount> {

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

View File

@ -279,6 +279,10 @@ pub enum Flow {
UpdateUserRole, UpdateUserRole,
/// Create merchant account for user in a org /// Create merchant account for user in a org
UserMerchantAccountCreate, UserMerchantAccountCreate,
/// Generate Sample Data
GenerateSampleData,
/// Delete Sample Data
DeleteSampleData,
} }
/// ///

View File

@ -1,15 +1,21 @@
-- Your SQL goes here -- Your SQL goes here
CREATE TABLE IF NOT EXISTS dashboard_metadata (
id SERIAL PRIMARY KEY,
user_id VARCHAR(64),
merchant_id VARCHAR(64) NOT NULL,
org_id VARCHAR(64) NOT NULL,
data_key VARCHAR(64) NOT NULL,
data_value JSON NOT NULL,
created_by VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),
last_modified_by VARCHAR(64) NOT NULL,
last_modified_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS dashboard_metadata_index ON dashboard_metadata (COALESCE(user_id,'0'), merchant_id, org_id, data_key); CREATE TABLE IF NOT EXISTS dashboard_metadata (
id SERIAL PRIMARY KEY,
user_id VARCHAR(64),
merchant_id VARCHAR(64) NOT NULL,
org_id VARCHAR(64) NOT NULL,
data_key VARCHAR(64) NOT NULL,
data_value JSON NOT NULL,
created_by VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),
last_modified_by VARCHAR(64) NOT NULL,
last_modified_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS dashboard_metadata_index ON dashboard_metadata (
COALESCE(user_id, '0'),
merchant_id,
org_id,
data_key
);