diff --git a/crates/api_models/src/disputes.rs b/crates/api_models/src/disputes.rs index a1340b5aa5a..4ddb50af0f2 100644 --- a/crates/api_models/src/disputes.rs +++ b/crates/api_models/src/disputes.rs @@ -1,11 +1,13 @@ use std::collections::HashMap; +use common_utils::types::TimeRange; use masking::{Deserialize, Serialize}; +use serde::de::Error; use time::PrimitiveDateTime; use utoipa::ToSchema; use super::enums::{DisputeStage, DisputeStatus}; -use crate::files; +use crate::{admin::MerchantConnectorInfo, enums, files}; #[derive(Clone, Debug, Serialize, ToSchema, Eq, PartialEq)] pub struct DisputeResponse { @@ -108,41 +110,51 @@ pub struct DisputeEvidenceBlock { pub file_metadata_response: files::FileMetadataResponse, } -#[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] #[serde(deny_unknown_fields)] -pub struct DisputeListConstraints { - /// limit on the number of objects to return - pub limit: Option, +pub struct DisputeListGetConstraints { + /// The identifier for dispute + pub dispute_id: Option, + /// The payment_id against which dispute is raised + pub payment_id: Option, + /// Limit on the number of objects to return + pub limit: Option, + /// The starting point within a list of object + pub offset: Option, /// The identifier for business profile #[schema(value_type = Option)] pub profile_id: Option, - /// status of the dispute - pub dispute_status: Option, - /// stage of the dispute - pub dispute_stage: Option, - /// reason for the dispute + /// The comma separated list of status of the disputes + #[serde(default, deserialize_with = "parse_comma_separated")] + pub dispute_status: Option>, + /// The comma separated list of stages of the disputes + #[serde(default, deserialize_with = "parse_comma_separated")] + pub dispute_stage: Option>, + /// Reason for the dispute pub reason: Option, - /// connector linked to dispute - pub connector: Option, - /// The time at which dispute is received - #[schema(example = "2022-09-10T10:11:12Z")] - pub received_time: Option, - /// Time less than the dispute received time - #[schema(example = "2022-09-10T10:11:12Z")] - #[serde(rename = "received_time.lt")] - pub received_time_lt: Option, - /// Time greater than the dispute received time - #[schema(example = "2022-09-10T10:11:12Z")] - #[serde(rename = "received_time.gt")] - pub received_time_gt: Option, - /// Time less than or equals to the dispute received time - #[schema(example = "2022-09-10T10:11:12Z")] - #[serde(rename = "received_time.lte")] - pub received_time_lte: Option, - /// Time greater than or equals to the dispute received time - #[schema(example = "2022-09-10T10:11:12Z")] - #[serde(rename = "received_time.gte")] - pub received_time_gte: Option, + /// The comma separated list of connectors linked to disputes + #[serde(default, deserialize_with = "parse_comma_separated")] + pub connector: Option>, + /// The comma separated list of currencies of the disputes + #[serde(default, deserialize_with = "parse_comma_separated")] + pub currency: Option>, + /// The merchant connector id to filter the disputes list + pub merchant_connector_id: Option, + /// The time range for which objects are needed. TimeRange has two fields start_time and end_time from which objects can be filtered as per required scenarios (created_at, time less than, greater than etc). + #[serde(flatten)] + pub time_range: Option, +} + +#[derive(Clone, Debug, serde::Serialize, ToSchema)] +pub struct DisputeListFilters { + /// The map of available connector filters, where the key is the connector name and the value is a list of MerchantConnectorInfo instances + pub connector: HashMap>, + /// The list of available currency filters + pub currency: Vec, + /// The list of available dispute status filters + pub dispute_status: Vec, + /// The list of available dispute stage filters + pub dispute_stage: Vec, } #[derive(Default, Clone, Debug, Serialize, Deserialize, ToSchema)] @@ -216,3 +228,19 @@ pub struct DisputesAggregateResponse { /// Different status of disputes with their count pub status_with_count: HashMap, } + +fn parse_comma_separated<'de, D, T>(v: D) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, + T: std::str::FromStr, + ::Err: std::fmt::Debug + std::fmt::Display + std::error::Error, +{ + let output = Option::<&str>::deserialize(v)?; + output + .map(|s| { + s.split(",") + .map(|x| x.parse::().map_err(D::Error::custom)) + .collect::>() + }) + .transpose() +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index b10f8e32dba..fefc596fe73 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -73,7 +73,7 @@ impl_api_event_type!( RetrievePaymentLinkRequest, PaymentLinkListConstraints, MandateId, - DisputeListConstraints, + DisputeListGetConstraints, RetrieveApiKeyResponse, ProfileResponse, ProfileUpdate, @@ -168,3 +168,9 @@ impl ApiEventMetric for PaymentMethodIntentCreate { Some(ApiEventsType::PaymentMethodCreate) } } + +impl ApiEventMetric for DisputeListFilters { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 31cb43669b2..312a5bf092d 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1817,6 +1817,7 @@ pub enum CardNetwork { serde::Deserialize, serde::Serialize, strum::Display, + strum::EnumIter, strum::EnumString, ToSchema, )] diff --git a/crates/hyperswitch_domain_models/src/disputes.rs b/crates/hyperswitch_domain_models/src/disputes.rs new file mode 100644 index 00000000000..87ae6204ac7 --- /dev/null +++ b/crates/hyperswitch_domain_models/src/disputes.rs @@ -0,0 +1,89 @@ +use crate::errors; + +pub struct DisputeListConstraints { + pub dispute_id: Option, + pub payment_id: Option, + pub limit: Option, + pub offset: Option, + pub profile_id: Option>, + pub dispute_status: Option>, + pub dispute_stage: Option>, + pub reason: Option, + pub connector: Option>, + pub merchant_connector_id: Option, + pub currency: Option>, + pub time_range: Option, +} + +impl + TryFrom<( + api_models::disputes::DisputeListGetConstraints, + Option>, + )> for DisputeListConstraints +{ + type Error = error_stack::Report; + fn try_from( + (value, auth_profile_id_list): ( + api_models::disputes::DisputeListGetConstraints, + Option>, + ), + ) -> Result { + let api_models::disputes::DisputeListGetConstraints { + dispute_id, + payment_id, + limit, + offset, + profile_id, + dispute_status, + dispute_stage, + reason, + connector, + merchant_connector_id, + currency, + time_range, + } = value; + let profile_id_from_request_body = profile_id; + // Match both the profile ID from the request body and the list of authenticated profile IDs coming from auth layer + let profile_id_list = match (profile_id_from_request_body, auth_profile_id_list) { + (None, None) => None, + // Case when the request body profile ID is None, but authenticated profile IDs are available, return the auth list + (None, Some(auth_profile_id_list)) => Some(auth_profile_id_list), + // Case when the request body profile ID is provided, but the auth list is None, create a vector with the request body profile ID + (Some(profile_id_from_request_body), None) => Some(vec![profile_id_from_request_body]), + (Some(profile_id_from_request_body), Some(auth_profile_id_list)) => { + // Check if the profile ID from the request body is present in the authenticated profile ID list + let profile_id_from_request_body_is_available_in_auth_profile_id_list = + auth_profile_id_list.contains(&profile_id_from_request_body); + + if profile_id_from_request_body_is_available_in_auth_profile_id_list { + Some(vec![profile_id_from_request_body]) + } else { + // If the profile ID is not valid, return an error indicating access is not available + return Err(error_stack::Report::new( + errors::api_error_response::ApiErrorResponse::PreconditionFailed { + message: format!( + "Access not available for the given profile_id {:?}", + profile_id_from_request_body + ), + }, + )); + } + } + }; + + Ok(Self { + dispute_id, + payment_id, + limit, + offset, + profile_id: profile_id_list, + dispute_status, + dispute_stage, + reason, + connector, + merchant_connector_id, + currency, + time_range, + }) + } +} diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index b0a660ad5d3..c99fbd89cd5 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -3,6 +3,7 @@ pub mod behaviour; pub mod business_profile; pub mod consts; pub mod customer; +pub mod disputes; pub mod errors; pub mod mandates; pub mod merchant_account; diff --git a/crates/router/src/core/disputes.rs b/crates/router/src/core/disputes.rs index 30b73eb4c6a..8f083c3103a 100644 --- a/crates/router/src/core/disputes.rs +++ b/crates/router/src/core/disputes.rs @@ -1,11 +1,13 @@ -use api_models::{disputes as dispute_models, files as files_api_models}; +use std::collections::HashMap; + +use api_models::{ + admin::MerchantConnectorInfo, disputes as dispute_models, files as files_api_models, +}; use common_utils::ext_traits::{Encode, ValueExt}; use error_stack::ResultExt; use router_env::{instrument, tracing}; -pub mod transformers; -use std::collections::HashMap; - use strum::IntoEnumIterator; +pub mod transformers; use super::{ errors::{self, ConnectorErrorExt, RouterResponse, StorageErrorExt}, @@ -48,12 +50,13 @@ pub async fn retrieve_dispute( pub async fn retrieve_disputes_list( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id_list: Option>, - constraints: api_models::disputes::DisputeListConstraints, + profile_id_list: Option>, + constraints: api_models::disputes::DisputeListGetConstraints, ) -> RouterResponse> { + let dispute_list_constraints = &(constraints.clone(), profile_id_list.clone()).try_into()?; let disputes = state .store - .find_disputes_by_merchant_id(merchant_account.get_id(), constraints) + .find_disputes_by_constraints(merchant_account.get_id(), dispute_list_constraints) .await .to_not_found_response(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to retrieve disputes")?; @@ -76,6 +79,59 @@ pub async fn accept_dispute( todo!() } +#[instrument(skip(state))] +pub async fn get_filters_for_disputes( + state: SessionState, + merchant_account: domain::MerchantAccount, + profile_id_list: Option>, +) -> RouterResponse { + let merchant_connector_accounts = if let services::ApplicationResponse::Json(data) = + super::admin::list_payment_connectors( + state, + merchant_account.get_id().to_owned(), + profile_id_list, + ) + .await? + { + data + } else { + return Err(error_stack::report!( + errors::ApiErrorResponse::InternalServerError + )) + .attach_printable( + "Failed to retrieve merchant connector accounts while fetching dispute list filters.", + ); + }; + + let connector_map = merchant_connector_accounts + .into_iter() + .filter_map(|merchant_connector_account| { + merchant_connector_account + .connector_label + .clone() + .map(|label| { + let info = merchant_connector_account.to_merchant_connector_info(&label); + (merchant_connector_account.connector_name, info) + }) + }) + .fold( + HashMap::new(), + |mut map: HashMap>, (connector_name, info)| { + map.entry(connector_name).or_default().push(info); + map + }, + ); + + Ok(services::ApplicationResponse::Json( + api_models::disputes::DisputeListFilters { + connector: connector_map, + currency: storage_enums::Currency::iter().collect(), + dispute_status: storage_enums::DisputeStatus::iter().collect(), + dispute_stage: storage_enums::DisputeStage::iter().collect(), + }, + )) +} + #[cfg(feature = "v1")] #[instrument(skip(state))] pub async fn accept_dispute( diff --git a/crates/router/src/db/dispute.rs b/crates/router/src/db/dispute.rs index a44528fde43..d7f4af63015 100644 --- a/crates/router/src/db/dispute.rs +++ b/crates/router/src/db/dispute.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use error_stack::report; +use hyperswitch_domain_models::disputes; use router_env::{instrument, tracing}; use super::{MockDb, Store}; @@ -30,10 +31,10 @@ pub trait DisputeInterface { dispute_id: &str, ) -> CustomResult; - async fn find_disputes_by_merchant_id( + async fn find_disputes_by_constraints( &self, merchant_id: &common_utils::id_type::MerchantId, - dispute_constraints: api_models::disputes::DisputeListConstraints, + dispute_constraints: &disputes::DisputeListConstraints, ) -> CustomResult, errors::StorageError>; async fn find_disputes_by_merchant_id_payment_id( @@ -101,10 +102,10 @@ impl DisputeInterface for Store { } #[instrument(skip_all)] - async fn find_disputes_by_merchant_id( + async fn find_disputes_by_constraints( &self, merchant_id: &common_utils::id_type::MerchantId, - dispute_constraints: api_models::disputes::DisputeListConstraints, + dispute_constraints: &disputes::DisputeListConstraints, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; storage::Dispute::filter_by_constraints(&conn, merchant_id, dispute_constraints) @@ -238,75 +239,96 @@ impl DisputeInterface for MockDb { .into()) } - async fn find_disputes_by_merchant_id( + async fn find_disputes_by_constraints( &self, merchant_id: &common_utils::id_type::MerchantId, - dispute_constraints: api_models::disputes::DisputeListConstraints, + dispute_constraints: &disputes::DisputeListConstraints, ) -> CustomResult, errors::StorageError> { let locked_disputes = self.disputes.lock().await; - - Ok(locked_disputes + let limit_usize = dispute_constraints + .limit + .unwrap_or(u32::MAX) + .try_into() + .unwrap_or(usize::MAX); + let offset_usize = dispute_constraints + .offset + .unwrap_or(0) + .try_into() + .unwrap_or(usize::MIN); + let filtered_disputes: Vec = locked_disputes .iter() - .filter(|d| { - d.merchant_id == *merchant_id + .filter(|dispute| { + dispute.merchant_id == *merchant_id && dispute_constraints - .dispute_status + .dispute_id .as_ref() - .map(|status| status == &d.dispute_status) - .unwrap_or(true) + .map_or(true, |id| &dispute.dispute_id == id) && dispute_constraints - .dispute_stage + .payment_id .as_ref() - .map(|stage| stage == &d.dispute_stage) - .unwrap_or(true) + .map_or(true, |id| &dispute.payment_id == id) && dispute_constraints - .reason + .profile_id .as_ref() - .and_then(|reason| { - d.connector_reason + .map_or(true, |profile_ids| { + dispute + .profile_id .as_ref() - .map(|connector_reason| connector_reason == reason) + .map_or(true, |id| profile_ids.contains(id)) }) - .unwrap_or(true) && dispute_constraints - .connector + .dispute_status .as_ref() - .map(|connector| connector == &d.connector) - .unwrap_or(true) + .map_or(true, |statuses| statuses.contains(&dispute.dispute_status)) && dispute_constraints - .received_time + .dispute_stage .as_ref() - .map(|received_time| received_time == &d.created_at) - .unwrap_or(true) + .map_or(true, |stages| stages.contains(&dispute.dispute_stage)) + && dispute_constraints.reason.as_ref().map_or(true, |reason| { + dispute + .connector_reason + .as_ref() + .map_or(true, |d_reason| d_reason == reason) + }) && dispute_constraints - .received_time_lt + .connector .as_ref() - .map(|received_time_lt| received_time_lt > &d.created_at) - .unwrap_or(true) + .map_or(true, |connectors| { + connectors + .iter() + .any(|connector| dispute.connector.as_str() == *connector) + }) && dispute_constraints - .received_time_gt + .merchant_connector_id .as_ref() - .map(|received_time_gt| received_time_gt < &d.created_at) - .unwrap_or(true) + .map_or(true, |id| { + dispute.merchant_connector_id.as_ref() == Some(id) + }) && dispute_constraints - .received_time_lte + .currency .as_ref() - .map(|received_time_lte| received_time_lte >= &d.created_at) - .unwrap_or(true) + .map_or(true, |currencies| { + currencies + .iter() + .any(|currency| dispute.currency.as_str() == currency.to_string()) + }) && dispute_constraints - .received_time_gte + .time_range .as_ref() - .map(|received_time_gte| received_time_gte <= &d.created_at) - .unwrap_or(true) + .map_or(true, |range| { + let dispute_time = dispute.created_at; + dispute_time >= range.start_time + && range + .end_time + .map_or(true, |end_time| dispute_time <= end_time) + }) }) - .take( - dispute_constraints - .limit - .and_then(|limit| usize::try_from(limit).ok()) - .unwrap_or(usize::MAX), - ) + .skip(offset_usize) + .take(limit_usize) .cloned() - .collect()) + .collect(); + + Ok(filtered_disputes) } async fn find_disputes_by_merchant_id_payment_id( @@ -437,11 +459,11 @@ mod tests { mod mockdb_dispute_interface { use std::borrow::Cow; - use api_models::disputes::DisputeListConstraints; use diesel_models::{ dispute::DisputeNew, enums::{DisputeStage, DisputeStatus}, }; + use hyperswitch_domain_models::disputes::DisputeListConstraints; use masking::Secret; use redis_interface::RedisSettings; use serde_json::Value; @@ -648,20 +670,21 @@ mod tests { .unwrap(); let found_disputes = mockdb - .find_disputes_by_merchant_id( + .find_disputes_by_constraints( &merchant_id, - DisputeListConstraints { + &DisputeListConstraints { + dispute_id: None, + payment_id: None, + profile_id: None, + connector: None, + merchant_connector_id: None, + currency: None, limit: None, + offset: None, dispute_status: None, dispute_stage: None, reason: None, - connector: None, - received_time: None, - received_time_lt: None, - received_time_gt: None, - received_time_lte: None, - received_time_gte: None, - profile_id: None, + time_range: None, }, ) .await diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index ff7e8145c33..36b66244e18 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -14,6 +14,7 @@ use hyperswitch_domain_models::payouts::{ payout_attempt::PayoutAttemptInterface, payouts::PayoutsInterface, }; use hyperswitch_domain_models::{ + disputes, payments::{payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface}, refunds, }; @@ -590,13 +591,13 @@ impl DisputeInterface for KafkaStore { .await } - async fn find_disputes_by_merchant_id( + async fn find_disputes_by_constraints( &self, merchant_id: &id_type::MerchantId, - dispute_constraints: api_models::disputes::DisputeListConstraints, + dispute_constraints: &disputes::DisputeListConstraints, ) -> CustomResult, errors::StorageError> { self.diesel_store - .find_disputes_by_merchant_id(merchant_id, dispute_constraints) + .find_disputes_by_constraints(merchant_id, dispute_constraints) .await } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 01a48c56401..dafee54ae05 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1496,6 +1496,11 @@ impl Disputes { web::resource("/profile/list") .route(web::get().to(disputes::retrieve_disputes_list_profile)), ) + .service(web::resource("/filter").route(web::get().to(disputes::get_disputes_filters))) + .service( + web::resource("/profile/filter") + .route(web::get().to(disputes::get_disputes_filters_profile)), + ) .service( web::resource("/accept/{dispute_id}") .route(web::post().to(disputes::accept_dispute)), diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index 50a26ceae89..1a8c4410a53 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -87,10 +87,10 @@ pub async fn retrieve_dispute( pub async fn retrieve_disputes_list( state: web::Data, req: HttpRequest, - payload: web::Query, + query: web::Query, ) -> HttpResponse { let flow = Flow::DisputesList; - let payload = payload.into_inner(); + let payload = query.into_inner(); Box::pin(api::server_wrap( flow, state, @@ -140,7 +140,7 @@ pub async fn retrieve_disputes_list( pub async fn retrieve_disputes_list_profile( state: web::Data, req: HttpRequest, - payload: web::Query, + payload: web::Query, ) -> HttpResponse { let flow = Flow::DisputesList; let payload = payload.into_inner(); @@ -170,6 +170,81 @@ pub async fn retrieve_disputes_list_profile( .await } +/// Disputes - Disputes Filters +#[utoipa::path( + get, + path = "/disputes/filter", + responses( + (status = 200, description = "List of filters", body = DisputeListFilters), + ), + tag = "Disputes", + operation_id = "List all filters for disputes", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::DisputesFilters))] +pub async fn get_disputes_filters(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::DisputesFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, auth, _, _| disputes::get_filters_for_disputes(state, auth.merchant_account, None), + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::DisputeRead, + minimum_entity_level: EntityType::Merchant, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +/// Disputes - Disputes Filters Profile +#[utoipa::path( + get, + path = "/disputes/profile/filter", + responses( + (status = 200, description = "List of filters", body = DisputeListFilters), + ), + tag = "Disputes", + operation_id = "List all filters for disputes", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::DisputesFilters))] +pub async fn get_disputes_filters_profile( + state: web::Data, + req: HttpRequest, +) -> HttpResponse { + let flow = Flow::DisputesFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, auth, _, _| { + disputes::get_filters_for_disputes( + state, + auth.merchant_account, + auth.profile_id.map(|profile_id| vec![profile_id]), + ) + }, + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::DisputeRead, + minimum_entity_level: EntityType::Profile, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + /// Disputes - Accept Dispute #[utoipa::path( get, diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index d6d3ec8a952..482bc40c49f 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -172,6 +172,7 @@ impl From for ApiIdentifier { Flow::DisputesRetrieve | Flow::DisputesList + | Flow::DisputesFilters | Flow::DisputesEvidenceSubmit | Flow::AttachDisputeEvidence | Flow::RetrieveDisputeEvidence diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index 40184c5fcfa..b287b5e9790 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -381,7 +381,7 @@ pub async fn get_refunds_filters(state: web::Data, req: HttpRequest) - /// To list the refunds filters associated with list of connectors, currencies and payment statuses #[utoipa::path( get, - path = "/refunds/v2/filter/profile", + path = "/refunds/v2/profile/filter", responses( (status = 200, description = "List of static filters", body = RefundListFilters), ), diff --git a/crates/router/src/types/storage/dispute.rs b/crates/router/src/types/storage/dispute.rs index 45f2073708b..28a9573b333 100644 --- a/crates/router/src/types/storage/dispute.rs +++ b/crates/router/src/types/storage/dispute.rs @@ -4,6 +4,7 @@ use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; pub use diesel_models::dispute::{Dispute, DisputeNew, DisputeUpdate}; use diesel_models::{errors, query::generics::db_metrics, schema::dispute::dsl}; use error_stack::ResultExt; +use hyperswitch_domain_models::disputes; use crate::{connection::PgPooledConn, logger}; @@ -12,7 +13,7 @@ pub trait DisputeDbExt: Sized { async fn filter_by_constraints( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, - dispute_list_constraints: api_models::disputes::DisputeListConstraints, + dispute_list_constraints: &disputes::DisputeListConstraints, ) -> CustomResult, errors::DatabaseError>; async fn get_dispute_status_with_count( @@ -28,45 +29,77 @@ impl DisputeDbExt for Dispute { async fn filter_by_constraints( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, - dispute_list_constraints: api_models::disputes::DisputeListConstraints, + dispute_list_constraints: &disputes::DisputeListConstraints, ) -> CustomResult, errors::DatabaseError> { let mut filter = ::table() .filter(dsl::merchant_id.eq(merchant_id.to_owned())) .order(dsl::modified_at.desc()) .into_boxed(); - if let Some(profile_id) = dispute_list_constraints.profile_id { - filter = filter.filter(dsl::profile_id.eq(profile_id)); + let mut search_by_payment_or_dispute_id = false; + + if let (Some(payment_id), Some(dispute_id)) = ( + &dispute_list_constraints.payment_id, + &dispute_list_constraints.dispute_id, + ) { + search_by_payment_or_dispute_id = true; + filter = filter + .filter(dsl::payment_id.eq(payment_id.to_owned())) + .or_filter(dsl::dispute_id.eq(dispute_id.to_owned())); + }; + + if !search_by_payment_or_dispute_id { + if let Some(payment_id) = &dispute_list_constraints.payment_id { + filter = filter.filter(dsl::payment_id.eq(payment_id.to_owned())); + }; } - if let Some(received_time) = dispute_list_constraints.received_time { - filter = filter.filter(dsl::created_at.eq(received_time)); + if !search_by_payment_or_dispute_id { + if let Some(dispute_id) = &dispute_list_constraints.dispute_id { + filter = filter.filter(dsl::dispute_id.eq(dispute_id.clone())); + }; } - if let Some(received_time_lt) = dispute_list_constraints.received_time_lt { - filter = filter.filter(dsl::created_at.lt(received_time_lt)); + + if let Some(time_range) = dispute_list_constraints.time_range { + filter = filter.filter(dsl::created_at.ge(time_range.start_time)); + + if let Some(end_time) = time_range.end_time { + filter = filter.filter(dsl::created_at.le(end_time)); + } } - if let Some(received_time_gt) = dispute_list_constraints.received_time_gt { - filter = filter.filter(dsl::created_at.gt(received_time_gt)); + + if let Some(profile_id) = &dispute_list_constraints.profile_id { + filter = filter.filter(dsl::profile_id.eq_any(profile_id.clone())); } - if let Some(received_time_lte) = dispute_list_constraints.received_time_lte { - filter = filter.filter(dsl::created_at.le(received_time_lte)); + if let Some(connector_list) = &dispute_list_constraints.connector { + filter = filter.filter(dsl::connector.eq_any(connector_list.clone())); } - if let Some(received_time_gte) = dispute_list_constraints.received_time_gte { - filter = filter.filter(dsl::created_at.ge(received_time_gte)); + + if let Some(reason) = &dispute_list_constraints.reason { + filter = filter.filter(dsl::connector_reason.eq(reason.clone())); } - if let Some(connector) = dispute_list_constraints.connector { - filter = filter.filter(dsl::connector.eq(connector)); + if let Some(dispute_stage) = &dispute_list_constraints.dispute_stage { + filter = filter.filter(dsl::dispute_stage.eq_any(dispute_stage.clone())); } - if let Some(reason) = dispute_list_constraints.reason { - filter = filter.filter(dsl::connector_reason.eq(reason)); + if let Some(dispute_status) = &dispute_list_constraints.dispute_status { + filter = filter.filter(dsl::dispute_status.eq_any(dispute_status.clone())); } - if let Some(dispute_stage) = dispute_list_constraints.dispute_stage { - filter = filter.filter(dsl::dispute_stage.eq(dispute_stage)); + + if let Some(currency_list) = &dispute_list_constraints.currency { + let currency: Vec = currency_list + .iter() + .map(|currency| currency.to_string()) + .collect(); + + filter = filter.filter(dsl::currency.eq_any(currency)); } - if let Some(dispute_status) = dispute_list_constraints.dispute_status { - filter = filter.filter(dsl::dispute_status.eq(dispute_status)); + if let Some(merchant_connector_id) = &dispute_list_constraints.merchant_connector_id { + filter = filter.filter(dsl::merchant_connector_id.eq(merchant_connector_id.clone())) } if let Some(limit) = dispute_list_constraints.limit { - filter = filter.limit(limit); + filter = filter.limit(limit.into()); + } + if let Some(offset) = dispute_list_constraints.offset { + filter = filter.offset(offset.into()); } logger::debug!(query = %diesel::debug_query::(&filter).to_string()); diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 12e30f54549..603c5cc91c1 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -274,6 +274,8 @@ pub enum Flow { DisputesRetrieve, /// Dispute List flow DisputesList, + /// Dispute Filters flow + DisputesFilters, /// Cards Info flow CardsInfo, /// Create File flow