mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(disputes): add filters for disputes list (#5637)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -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<i64>,
|
||||
pub struct DisputeListGetConstraints {
|
||||
/// The identifier for dispute
|
||||
pub dispute_id: Option<String>,
|
||||
/// The payment_id against which dispute is raised
|
||||
pub payment_id: Option<common_utils::id_type::PaymentId>,
|
||||
/// Limit on the number of objects to return
|
||||
pub limit: Option<u32>,
|
||||
/// The starting point within a list of object
|
||||
pub offset: Option<u32>,
|
||||
/// The identifier for business profile
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub profile_id: Option<common_utils::id_type::ProfileId>,
|
||||
/// status of the dispute
|
||||
pub dispute_status: Option<DisputeStatus>,
|
||||
/// stage of the dispute
|
||||
pub dispute_stage: Option<DisputeStage>,
|
||||
/// reason for the dispute
|
||||
/// The comma separated list of status of the disputes
|
||||
#[serde(default, deserialize_with = "parse_comma_separated")]
|
||||
pub dispute_status: Option<Vec<DisputeStatus>>,
|
||||
/// The comma separated list of stages of the disputes
|
||||
#[serde(default, deserialize_with = "parse_comma_separated")]
|
||||
pub dispute_stage: Option<Vec<DisputeStage>>,
|
||||
/// Reason for the dispute
|
||||
pub reason: Option<String>,
|
||||
/// connector linked to dispute
|
||||
pub connector: Option<String>,
|
||||
/// The time at which dispute is received
|
||||
#[schema(example = "2022-09-10T10:11:12Z")]
|
||||
pub received_time: Option<PrimitiveDateTime>,
|
||||
/// Time less than the dispute received time
|
||||
#[schema(example = "2022-09-10T10:11:12Z")]
|
||||
#[serde(rename = "received_time.lt")]
|
||||
pub received_time_lt: Option<PrimitiveDateTime>,
|
||||
/// Time greater than the dispute received time
|
||||
#[schema(example = "2022-09-10T10:11:12Z")]
|
||||
#[serde(rename = "received_time.gt")]
|
||||
pub received_time_gt: Option<PrimitiveDateTime>,
|
||||
/// 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<PrimitiveDateTime>,
|
||||
/// 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<PrimitiveDateTime>,
|
||||
/// The comma separated list of connectors linked to disputes
|
||||
#[serde(default, deserialize_with = "parse_comma_separated")]
|
||||
pub connector: Option<Vec<String>>,
|
||||
/// The comma separated list of currencies of the disputes
|
||||
#[serde(default, deserialize_with = "parse_comma_separated")]
|
||||
pub currency: Option<Vec<common_enums::Currency>>,
|
||||
/// The merchant connector id to filter the disputes list
|
||||
pub merchant_connector_id: Option<common_utils::id_type::MerchantConnectorAccountId>,
|
||||
/// 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<TimeRange>,
|
||||
}
|
||||
|
||||
#[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<String, Vec<MerchantConnectorInfo>>,
|
||||
/// The list of available currency filters
|
||||
pub currency: Vec<enums::Currency>,
|
||||
/// The list of available dispute status filters
|
||||
pub dispute_status: Vec<DisputeStatus>,
|
||||
/// The list of available dispute stage filters
|
||||
pub dispute_stage: Vec<DisputeStage>,
|
||||
}
|
||||
|
||||
#[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<DisputeStatus, i64>,
|
||||
}
|
||||
|
||||
fn parse_comma_separated<'de, D, T>(v: D) -> Result<Option<Vec<T>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
T: std::str::FromStr,
|
||||
<T as 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::<T>().map_err(D::Error::custom))
|
||||
.collect::<Result<_, _>>()
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ impl_api_event_type!(
|
||||
RetrievePaymentLinkRequest,
|
||||
PaymentLinkListConstraints,
|
||||
MandateId,
|
||||
DisputeListConstraints,
|
||||
DisputeListGetConstraints,
|
||||
RetrieveApiKeyResponse,
|
||||
ProfileResponse,
|
||||
ProfileUpdate,
|
||||
@ -171,3 +171,9 @@ impl ApiEventMetric for PaymentMethodIntentCreate {
|
||||
Some(ApiEventsType::PaymentMethodCreate)
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiEventMetric for DisputeListFilters {
|
||||
fn get_api_event_type(&self) -> Option<ApiEventsType> {
|
||||
Some(ApiEventsType::ResourceListAPI)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1817,6 +1817,7 @@ pub enum CardNetwork {
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::Display,
|
||||
strum::EnumIter,
|
||||
strum::EnumString,
|
||||
ToSchema,
|
||||
)]
|
||||
|
||||
89
crates/hyperswitch_domain_models/src/disputes.rs
Normal file
89
crates/hyperswitch_domain_models/src/disputes.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use crate::errors;
|
||||
|
||||
pub struct DisputeListConstraints {
|
||||
pub dispute_id: Option<String>,
|
||||
pub payment_id: Option<common_utils::id_type::PaymentId>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub profile_id: Option<Vec<common_utils::id_type::ProfileId>>,
|
||||
pub dispute_status: Option<Vec<common_enums::DisputeStatus>>,
|
||||
pub dispute_stage: Option<Vec<common_enums::DisputeStage>>,
|
||||
pub reason: Option<String>,
|
||||
pub connector: Option<Vec<String>>,
|
||||
pub merchant_connector_id: Option<common_utils::id_type::MerchantConnectorAccountId>,
|
||||
pub currency: Option<Vec<common_enums::Currency>>,
|
||||
pub time_range: Option<common_utils::types::TimeRange>,
|
||||
}
|
||||
|
||||
impl
|
||||
TryFrom<(
|
||||
api_models::disputes::DisputeListGetConstraints,
|
||||
Option<Vec<common_utils::id_type::ProfileId>>,
|
||||
)> for DisputeListConstraints
|
||||
{
|
||||
type Error = error_stack::Report<errors::api_error_response::ApiErrorResponse>;
|
||||
fn try_from(
|
||||
(value, auth_profile_id_list): (
|
||||
api_models::disputes::DisputeListGetConstraints,
|
||||
Option<Vec<common_utils::id_type::ProfileId>>,
|
||||
),
|
||||
) -> Result<Self, Self::Error> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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<Vec<common_utils::id_type::ProfileId>>,
|
||||
constraints: api_models::disputes::DisputeListConstraints,
|
||||
profile_id_list: Option<Vec<common_utils::id_type::ProfileId>>,
|
||||
constraints: api_models::disputes::DisputeListGetConstraints,
|
||||
) -> RouterResponse<Vec<api_models::disputes::DisputeResponse>> {
|
||||
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<Vec<common_utils::id_type::ProfileId>>,
|
||||
) -> RouterResponse<api_models::disputes::DisputeListFilters> {
|
||||
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<String, Vec<MerchantConnectorInfo>>, (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(
|
||||
|
||||
@ -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<storage::Dispute, errors::StorageError>;
|
||||
|
||||
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<Vec<storage::Dispute>, 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<Vec<storage::Dispute>, 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<Vec<storage::Dispute>, 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<storage::Dispute> = locked_disputes
|
||||
.iter()
|
||||
.filter(|d| {
|
||||
d.merchant_id == *merchant_id
|
||||
.filter(|dispute| {
|
||||
dispute.merchant_id == *merchant_id
|
||||
&& dispute_constraints
|
||||
.dispute_id
|
||||
.as_ref()
|
||||
.map_or(true, |id| &dispute.dispute_id == id)
|
||||
&& dispute_constraints
|
||||
.payment_id
|
||||
.as_ref()
|
||||
.map_or(true, |id| &dispute.payment_id == id)
|
||||
&& dispute_constraints
|
||||
.profile_id
|
||||
.as_ref()
|
||||
.map_or(true, |profile_ids| {
|
||||
dispute
|
||||
.profile_id
|
||||
.as_ref()
|
||||
.map_or(true, |id| profile_ids.contains(id))
|
||||
})
|
||||
&& dispute_constraints
|
||||
.dispute_status
|
||||
.as_ref()
|
||||
.map(|status| status == &d.dispute_status)
|
||||
.unwrap_or(true)
|
||||
.map_or(true, |statuses| statuses.contains(&dispute.dispute_status))
|
||||
&& dispute_constraints
|
||||
.dispute_stage
|
||||
.as_ref()
|
||||
.map(|stage| stage == &d.dispute_stage)
|
||||
.unwrap_or(true)
|
||||
&& dispute_constraints
|
||||
.reason
|
||||
.map_or(true, |stages| stages.contains(&dispute.dispute_stage))
|
||||
&& dispute_constraints.reason.as_ref().map_or(true, |reason| {
|
||||
dispute
|
||||
.connector_reason
|
||||
.as_ref()
|
||||
.and_then(|reason| {
|
||||
d.connector_reason
|
||||
.as_ref()
|
||||
.map(|connector_reason| connector_reason == reason)
|
||||
.map_or(true, |d_reason| d_reason == reason)
|
||||
})
|
||||
.unwrap_or(true)
|
||||
&& dispute_constraints
|
||||
.connector
|
||||
.as_ref()
|
||||
.map(|connector| connector == &d.connector)
|
||||
.unwrap_or(true)
|
||||
&& dispute_constraints
|
||||
.received_time
|
||||
.as_ref()
|
||||
.map(|received_time| received_time == &d.created_at)
|
||||
.unwrap_or(true)
|
||||
&& dispute_constraints
|
||||
.received_time_lt
|
||||
.as_ref()
|
||||
.map(|received_time_lt| received_time_lt > &d.created_at)
|
||||
.unwrap_or(true)
|
||||
&& dispute_constraints
|
||||
.received_time_gt
|
||||
.as_ref()
|
||||
.map(|received_time_gt| received_time_gt < &d.created_at)
|
||||
.unwrap_or(true)
|
||||
&& dispute_constraints
|
||||
.received_time_lte
|
||||
.as_ref()
|
||||
.map(|received_time_lte| received_time_lte >= &d.created_at)
|
||||
.unwrap_or(true)
|
||||
&& dispute_constraints
|
||||
.received_time_gte
|
||||
.as_ref()
|
||||
.map(|received_time_gte| received_time_gte <= &d.created_at)
|
||||
.unwrap_or(true)
|
||||
.map_or(true, |connectors| {
|
||||
connectors
|
||||
.iter()
|
||||
.any(|connector| dispute.connector.as_str() == *connector)
|
||||
})
|
||||
.take(
|
||||
dispute_constraints
|
||||
.limit
|
||||
.and_then(|limit| usize::try_from(limit).ok())
|
||||
.unwrap_or(usize::MAX),
|
||||
)
|
||||
&& dispute_constraints
|
||||
.merchant_connector_id
|
||||
.as_ref()
|
||||
.map_or(true, |id| {
|
||||
dispute.merchant_connector_id.as_ref() == Some(id)
|
||||
})
|
||||
&& dispute_constraints
|
||||
.currency
|
||||
.as_ref()
|
||||
.map_or(true, |currencies| {
|
||||
currencies
|
||||
.iter()
|
||||
.any(|currency| dispute.currency.as_str() == currency.to_string())
|
||||
})
|
||||
&& dispute_constraints
|
||||
.time_range
|
||||
.as_ref()
|
||||
.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)
|
||||
})
|
||||
})
|
||||
.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
|
||||
|
||||
@ -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<Vec<storage::Dispute>, errors::StorageError> {
|
||||
self.diesel_store
|
||||
.find_disputes_by_merchant_id(merchant_id, dispute_constraints)
|
||||
.find_disputes_by_constraints(merchant_id, dispute_constraints)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -87,10 +87,10 @@ pub async fn retrieve_dispute(
|
||||
pub async fn retrieve_disputes_list(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
payload: web::Query<dispute_models::DisputeListConstraints>,
|
||||
query: web::Query<dispute_models::DisputeListGetConstraints>,
|
||||
) -> 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<AppState>,
|
||||
req: HttpRequest,
|
||||
payload: web::Query<dispute_models::DisputeListConstraints>,
|
||||
payload: web::Query<dispute_models::DisputeListGetConstraints>,
|
||||
) -> 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<AppState>, 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<AppState>,
|
||||
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,
|
||||
|
||||
@ -172,6 +172,7 @@ impl From<Flow> for ApiIdentifier {
|
||||
|
||||
Flow::DisputesRetrieve
|
||||
| Flow::DisputesList
|
||||
| Flow::DisputesFilters
|
||||
| Flow::DisputesEvidenceSubmit
|
||||
| Flow::AttachDisputeEvidence
|
||||
| Flow::RetrieveDisputeEvidence
|
||||
|
||||
@ -381,7 +381,7 @@ pub async fn get_refunds_filters(state: web::Data<AppState>, 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),
|
||||
),
|
||||
|
||||
@ -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<Vec<Self>, 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<Vec<Self>, errors::DatabaseError> {
|
||||
let mut filter = <Self as HasTable>::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(received_time_lte) = dispute_list_constraints.received_time_lte {
|
||||
filter = filter.filter(dsl::created_at.le(received_time_lte));
|
||||
|
||||
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_gte) = dispute_list_constraints.received_time_gte {
|
||||
filter = filter.filter(dsl::created_at.ge(received_time_gte));
|
||||
if let Some(connector_list) = &dispute_list_constraints.connector {
|
||||
filter = filter.filter(dsl::connector.eq_any(connector_list.clone()));
|
||||
}
|
||||
if let Some(connector) = dispute_list_constraints.connector {
|
||||
filter = filter.filter(dsl::connector.eq(connector));
|
||||
|
||||
if let Some(reason) = &dispute_list_constraints.reason {
|
||||
filter = filter.filter(dsl::connector_reason.eq(reason.clone()));
|
||||
}
|
||||
if let Some(reason) = dispute_list_constraints.reason {
|
||||
filter = filter.filter(dsl::connector_reason.eq(reason));
|
||||
if let Some(dispute_stage) = &dispute_list_constraints.dispute_stage {
|
||||
filter = filter.filter(dsl::dispute_stage.eq_any(dispute_stage.clone()));
|
||||
}
|
||||
if let Some(dispute_stage) = dispute_list_constraints.dispute_stage {
|
||||
filter = filter.filter(dsl::dispute_stage.eq(dispute_stage));
|
||||
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_status) = dispute_list_constraints.dispute_status {
|
||||
filter = filter.filter(dsl::dispute_status.eq(dispute_status));
|
||||
|
||||
if let Some(currency_list) = &dispute_list_constraints.currency {
|
||||
let currency: Vec<String> = currency_list
|
||||
.iter()
|
||||
.map(|currency| currency.to_string())
|
||||
.collect();
|
||||
|
||||
filter = filter.filter(dsl::currency.eq_any(currency));
|
||||
}
|
||||
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::<diesel::pg::Pg, _>(&filter).to_string());
|
||||
|
||||
@ -274,6 +274,8 @@ pub enum Flow {
|
||||
DisputesRetrieve,
|
||||
/// Dispute List flow
|
||||
DisputesList,
|
||||
/// Dispute Filters flow
|
||||
DisputesFilters,
|
||||
/// Cards Info flow
|
||||
CardsInfo,
|
||||
/// Create File flow
|
||||
|
||||
Reference in New Issue
Block a user