Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(disputes): add filters for disputes list #5637

Merged
merged 25 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2a19a2b
feat(disputes): add filters for disputes list
apoorvdixit88 Aug 15, 2024
aeebef3
chore: run formatter
hyperswitch-bot[bot] Aug 15, 2024
530af00
fix: correct route function
apoorvdixit88 Aug 15, 2024
3744ebf
chore: run formatter
hyperswitch-bot[bot] Aug 15, 2024
98bfb3c
fix: add filter queries
apoorvdixit88 Aug 20, 2024
a31bcb1
Merge branch 'main' into add-filters-to-dispute-list
apoorvdixit88 Sep 1, 2024
7186d81
fix: resolve conflicts
apoorvdixit88 Sep 1, 2024
c6db00e
fix: add query parse struct withou vec
apoorvdixit88 Sep 1, 2024
f05e14c
chore: run formatter
hyperswitch-bot[bot] Sep 1, 2024
555741c
fix: types in api and diesel models
apoorvdixit88 Sep 13, 2024
d10f28a
Merge branch 'main' into add-filters-to-dispute-list
apoorvdixit88 Sep 13, 2024
6a30270
Merge branch 'main' into add-filters-to-dispute-list
apoorvdixit88 Sep 16, 2024
c7dde68
fix: resolve conflicts
apoorvdixit88 Sep 16, 2024
f1af42f
chore: run formatter
hyperswitch-bot[bot] Sep 16, 2024
ea2bcf8
fix: resolve parsing
apoorvdixit88 Sep 17, 2024
497070d
fix: resolve spell check
apoorvdixit88 Sep 17, 2024
653ab5a
chore: refactor mockdb impl for function
apoorvdixit88 Sep 17, 2024
f6a2d46
Merge branch 'main' into add-filters-to-dispute-list
apoorvdixit88 Sep 18, 2024
314e42a
fix: handle v2 checks
apoorvdixit88 Sep 18, 2024
e4051dc
Merge branch 'main' into add-filters-to-dispute-list
apoorvdixit88 Sep 18, 2024
b6e34e4
fix: add v1 flag to accept dispute
apoorvdixit88 Sep 18, 2024
4caebd2
Merge branch 'main' into add-filters-to-dispute-list
apoorvdixit88 Sep 18, 2024
37fc8e3
fix: typo in feature flag
apoorvdixit88 Sep 18, 2024
c521cdb
Merge branch 'main' into add-filters-to-dispute-list
apoorvdixit88 Sep 20, 2024
e30dadb
fix: resolve review comments
apoorvdixit88 Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 59 additions & 31 deletions crates/api_models/src/disputes.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need dispute_id in DisputeListGetConstraints? We can use Dispute retrieve for point query right?

Copy link
Contributor Author

@apoorvdixit88 apoorvdixit88 Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its good to have:

  • Users will be able to search for a specific dispute by its ID and see the result directly in the list and then it can be retrieved. If the dispute doesn't exist, the list page should display a message indicating that the dispute is not present.
  • Users will have the ability to search for disputes one by one, without needing to repeatedly navigate back to the retrieve page. They should be able to see disputes directly in list (if it exists), making the process more user-friendly and efficient.

/// 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")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the parse comma thing for backwards compatibility?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and it will support for open source users too.
Also it is because we will be passing list in query params, Get api, so parsing logic is required. For passing lists like this there is also other syntax where actix will handle things, list[]=1&list[]=2. But for now as dashboard frontend is passing comma separated, for all the similar get apis where list is required, using the same for it.
Later we can change this if required.

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)]
Expand Down Expand Up @@ -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()
}
8 changes: 7 additions & 1 deletion crates/api_models/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl_api_event_type!(
RetrievePaymentLinkRequest,
PaymentLinkListConstraints,
MandateId,
DisputeListConstraints,
DisputeListGetConstraints,
RetrieveApiKeyResponse,
ProfileResponse,
ProfileUpdate,
Expand Down Expand Up @@ -168,3 +168,9 @@ impl ApiEventMetric for PaymentMethodIntentCreate {
Some(ApiEventsType::PaymentMethodCreate)
}
}

impl ApiEventMetric for DisputeListFilters {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::ResourceListAPI)
}
}
1 change: 1 addition & 0 deletions crates/common_enums/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,7 @@ pub enum CardNetwork {
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumIter,
strum::EnumString,
ToSchema,
)]
Expand Down
89 changes: 89 additions & 0 deletions crates/hyperswitch_domain_models/src/disputes.rs
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain here with some code comments?

(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,
})
}
}
1 change: 1 addition & 0 deletions crates/hyperswitch_domain_models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
70 changes: 63 additions & 7 deletions crates/router/src/core/disputes.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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")?;
Expand All @@ -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(
Expand Down
Loading
Loading