feat(refunds): update refunds filters (#4409)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Apoorv Dixit
2024-05-13 18:47:29 +05:30
committed by GitHub
parent 1602eb541d
commit cfab2af7d4
14 changed files with 254 additions and 12 deletions

View File

@ -523,7 +523,7 @@ pub struct MerchantConnectorWebhookDetails {
pub additional_secret: Option<Secret<String>>, pub additional_secret: Option<Secret<String>>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct MerchantConnectorInfo { pub struct MerchantConnectorInfo {
pub connector_label: String, pub connector_label: String,
pub merchant_connector_id: String, pub merchant_connector_id: String,

View File

@ -1,8 +1,8 @@
use common_utils::events::{ApiEventMetric, ApiEventsType}; use common_utils::events::{ApiEventMetric, ApiEventsType};
use crate::refunds::{ use crate::refunds::{
RefundListMetaData, RefundListRequest, RefundListResponse, RefundRequest, RefundResponse, RefundListFilters, RefundListMetaData, RefundListRequest, RefundListResponse, RefundRequest,
RefundUpdateRequest, RefundsRetrieveRequest, RefundResponse, RefundUpdateRequest, RefundsRetrieveRequest,
}; };
impl ApiEventMetric for RefundRequest { impl ApiEventMetric for RefundRequest {
@ -61,3 +61,9 @@ impl ApiEventMetric for RefundListMetaData {
Some(ApiEventsType::ResourceListAPI) Some(ApiEventsType::ResourceListAPI)
} }
} }
impl ApiEventMetric for RefundListFilters {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::ResourceListAPI)
}
}

View File

@ -3576,9 +3576,11 @@ pub struct PaymentListFiltersV2 {
pub authentication_type: Vec<enums::AuthenticationType>, pub authentication_type: Vec<enums::AuthenticationType>,
} }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
pub struct AmountFilter { pub struct AmountFilter {
/// The start amount to filter list of transactions which are greater than or equal to the start amount
pub start_amount: Option<i64>, pub start_amount: Option<i64>,
/// The end amount to filter list of transactions which are less than or equal to the end amount
pub end_amount: Option<i64>, pub end_amount: Option<i64>,
} }

View File

@ -1,10 +1,15 @@
use std::collections::HashMap;
use common_utils::pii; use common_utils::pii;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
use utoipa::ToSchema; use utoipa::ToSchema;
use super::payments::TimeRange; use super::payments::{AmountFilter, TimeRange};
use crate::{admin, enums}; use crate::{
admin::{self, MerchantConnectorInfo},
enums,
};
#[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] #[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@ -146,11 +151,15 @@ pub struct RefundListRequest {
pub limit: Option<i64>, pub limit: Option<i64>,
/// The starting point within a list of objects /// The starting point within a list of objects
pub offset: Option<i64>, pub offset: Option<i64>,
/// 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). /// 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)] #[serde(flatten)]
pub time_range: Option<TimeRange>, pub time_range: Option<TimeRange>,
/// The amount to filter reufnds list. Amount takes two option fields start_amount and end_amount from which objects can be filtered as per required scenarios (less_than, greater_than, equal_to and range)
pub amount_filter: Option<AmountFilter>,
/// The list of connectors to filter refunds list /// The list of connectors to filter refunds list
pub connector: Option<Vec<String>>, pub connector: Option<Vec<String>>,
/// The list of merchant connector ids to filter the refunds list for selected label
pub merchant_connector_id: Option<Vec<String>>,
/// The list of currencies to filter refunds list /// The list of currencies to filter refunds list
#[schema(value_type = Option<Vec<Currency>>)] #[schema(value_type = Option<Vec<Currency>>)]
pub currency: Option<Vec<enums::Currency>>, pub currency: Option<Vec<enums::Currency>>,
@ -181,6 +190,18 @@ pub struct RefundListMetaData {
pub refund_status: Vec<enums::RefundStatus>, pub refund_status: Vec<enums::RefundStatus>,
} }
#[derive(Clone, Debug, serde::Serialize, ToSchema)]
pub struct RefundListFilters {
/// 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
#[schema(value_type = Vec<Currency>)]
pub currency: Vec<enums::Currency>,
/// The list of available refund status filters
#[schema(value_type = Vec<RefundStatus>)]
pub refund_status: Vec<enums::RefundStatus>,
}
/// The status for refunds /// The status for refunds
#[derive( #[derive(
Debug, Debug,

View File

@ -1514,6 +1514,7 @@ pub enum PaymentType {
PartialEq, PartialEq,
strum::Display, strum::Display,
strum::EnumString, strum::EnumString,
strum::EnumIter,
serde::Serialize, serde::Serialize,
serde::Deserialize, serde::Deserialize,
)] )]

View File

@ -418,6 +418,7 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::refunds::RefundListRequest, api_models::refunds::RefundListRequest,
api_models::refunds::RefundListResponse, api_models::refunds::RefundListResponse,
api_models::payments::TimeRange, api_models::payments::TimeRange,
api_models::payments::AmountFilter,
api_models::mandates::MandateRevokedResponse, api_models::mandates::MandateRevokedResponse,
api_models::mandates::MandateResponse, api_models::mandates::MandateResponse,
api_models::mandates::MandateCardDetails, api_models::mandates::MandateCardDetails,

View File

@ -1,9 +1,16 @@
pub mod validator; pub mod validator;
#[cfg(feature = "olap")]
use std::collections::HashMap;
#[cfg(feature = "olap")]
use api_models::admin::MerchantConnectorInfo;
use common_utils::ext_traits::AsyncExt; use common_utils::ext_traits::AsyncExt;
use error_stack::{report, ResultExt}; use error_stack::{report, ResultExt};
use router_env::{instrument, tracing}; use router_env::{instrument, tracing};
use scheduler::{consumer::types::process_data, utils as process_tracker_utils}; use scheduler::{consumer::types::process_data, utils as process_tracker_utils};
#[cfg(feature = "olap")]
use strum::IntoEnumIterator;
use crate::{ use crate::{
consts, consts,
@ -767,6 +774,48 @@ pub async fn refund_filter_list(
Ok(services::ApplicationResponse::Json(filter_list)) Ok(services::ApplicationResponse::Json(filter_list))
} }
#[instrument(skip_all)]
#[cfg(feature = "olap")]
pub async fn get_filters_for_refunds(
state: AppState,
merchant_account: domain::MerchantAccount,
) -> RouterResponse<api_models::refunds::RefundListFilters> {
let merchant_connector_accounts = if let services::ApplicationResponse::Json(data) =
super::admin::list_payment_connectors(state, merchant_account.merchant_id).await?
{
data
} else {
return Err(errors::ApiErrorResponse::InternalServerError.into());
};
let connector_map = merchant_connector_accounts
.into_iter()
.filter_map(|merchant_connector_account| {
merchant_connector_account.connector_label.map(|label| {
let info = MerchantConnectorInfo {
connector_label: label,
merchant_connector_id: merchant_connector_account.merchant_connector_id,
};
(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::refunds::RefundListFilters {
connector: connector_map,
currency: enums::Currency::iter().collect(),
refund_status: enums::RefundStatus::iter().collect(),
},
))
}
impl ForeignFrom<storage::Refund> for api::RefundResponse { impl ForeignFrom<storage::Refund> for api::RefundResponse {
fn foreign_from(refund: storage::Refund) -> Self { fn foreign_from(refund: storage::Refund) -> Self {
let refund = refund; let refund = refund;

View File

@ -930,6 +930,7 @@ impl RefundInterface for MockDb {
offset: i64, offset: i64,
) -> CustomResult<Vec<diesel_models::refund::Refund>, errors::StorageError> { ) -> CustomResult<Vec<diesel_models::refund::Refund>, errors::StorageError> {
let mut unique_connectors = HashSet::new(); let mut unique_connectors = HashSet::new();
let mut unique_merchant_connector_ids = HashSet::new();
let mut unique_currencies = HashSet::new(); let mut unique_currencies = HashSet::new();
let mut unique_statuses = HashSet::new(); let mut unique_statuses = HashSet::new();
@ -940,6 +941,14 @@ impl RefundInterface for MockDb {
}); });
} }
if let Some(merchant_connector_ids) = &refund_details.merchant_connector_id {
merchant_connector_ids
.iter()
.for_each(|unique_merchant_connector_id| {
unique_merchant_connector_ids.insert(unique_merchant_connector_id);
});
}
if let Some(currencies) = &refund_details.currency { if let Some(currencies) = &refund_details.currency {
currencies.iter().for_each(|currency| { currencies.iter().for_each(|currency| {
unique_currencies.insert(currency); unique_currencies.insert(currency);
@ -982,9 +991,25 @@ impl RefundInterface for MockDb {
range.end_time.unwrap_or_else(common_utils::date_time::now) range.end_time.unwrap_or_else(common_utils::date_time::now)
}) })
}) })
.filter(|refund| {
refund_details
.amount_filter
.as_ref()
.map_or(true, |amount| {
refund.refund_amount >= amount.start_amount.unwrap_or(i64::MIN)
&& refund.refund_amount <= amount.end_amount.unwrap_or(i64::MAX)
})
})
.filter(|refund| { .filter(|refund| {
unique_connectors.is_empty() || unique_connectors.contains(&refund.connector) unique_connectors.is_empty() || unique_connectors.contains(&refund.connector)
}) })
.filter(|refund| {
unique_merchant_connector_ids.is_empty()
|| refund
.merchant_connector_id
.as_ref()
.map_or(false, |id| unique_merchant_connector_ids.contains(id))
})
.filter(|refund| { .filter(|refund| {
unique_currencies.is_empty() || unique_currencies.contains(&refund.currency) unique_currencies.is_empty() || unique_currencies.contains(&refund.currency)
}) })
@ -1054,6 +1079,7 @@ impl RefundInterface for MockDb {
_storage_scheme: enums::MerchantStorageScheme, _storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<i64, errors::StorageError> { ) -> CustomResult<i64, errors::StorageError> {
let mut unique_connectors = HashSet::new(); let mut unique_connectors = HashSet::new();
let mut unique_merchant_connector_ids = HashSet::new();
let mut unique_currencies = HashSet::new(); let mut unique_currencies = HashSet::new();
let mut unique_statuses = HashSet::new(); let mut unique_statuses = HashSet::new();
@ -1064,6 +1090,14 @@ impl RefundInterface for MockDb {
}); });
} }
if let Some(merchant_connector_ids) = &refund_details.merchant_connector_id {
merchant_connector_ids
.iter()
.for_each(|unique_merchant_connector_id| {
unique_merchant_connector_ids.insert(unique_merchant_connector_id);
});
}
if let Some(currencies) = &refund_details.currency { if let Some(currencies) = &refund_details.currency {
currencies.iter().for_each(|currency| { currencies.iter().for_each(|currency| {
unique_currencies.insert(currency); unique_currencies.insert(currency);
@ -1106,9 +1140,25 @@ impl RefundInterface for MockDb {
range.end_time.unwrap_or_else(common_utils::date_time::now) range.end_time.unwrap_or_else(common_utils::date_time::now)
}) })
}) })
.filter(|refund| {
refund_details
.amount_filter
.as_ref()
.map_or(true, |amount| {
refund.refund_amount >= amount.start_amount.unwrap_or(i64::MIN)
&& refund.refund_amount <= amount.end_amount.unwrap_or(i64::MAX)
})
})
.filter(|refund| { .filter(|refund| {
unique_connectors.is_empty() || unique_connectors.contains(&refund.connector) unique_connectors.is_empty() || unique_connectors.contains(&refund.connector)
}) })
.filter(|refund| {
unique_merchant_connector_ids.is_empty()
|| refund
.merchant_connector_id
.as_ref()
.map_or(false, |id| unique_merchant_connector_ids.contains(id))
})
.filter(|refund| { .filter(|refund| {
unique_currencies.is_empty() || unique_currencies.contains(&refund.currency) unique_currencies.is_empty() || unique_currencies.contains(&refund.currency)
}) })

View File

@ -333,7 +333,7 @@ impl Payments {
.route(web::post().to(payments_list_by_filter)), .route(web::post().to(payments_list_by_filter)),
) )
.service(web::resource("/filter").route(web::post().to(get_filters_for_payments))) .service(web::resource("/filter").route(web::post().to(get_filters_for_payments)))
.service(web::resource("/filter_v2").route(web::get().to(get_payment_filters))) .service(web::resource("/v2/filter").route(web::get().to(get_payment_filters)))
} }
#[cfg(feature = "oltp")] #[cfg(feature = "oltp")]
{ {
@ -716,7 +716,8 @@ impl Refunds {
{ {
route = route route = route
.service(web::resource("/list").route(web::post().to(refunds_list))) .service(web::resource("/list").route(web::post().to(refunds_list)))
.service(web::resource("/filter").route(web::post().to(refunds_filter_list))); .service(web::resource("/filter").route(web::post().to(refunds_filter_list)))
.service(web::resource("/v2/filter").route(web::get().to(get_refunds_filters)));
} }
#[cfg(feature = "oltp")] #[cfg(feature = "oltp")]
{ {

View File

@ -136,7 +136,8 @@ impl From<Flow> for ApiIdentifier {
| Flow::RefundsRetrieve | Flow::RefundsRetrieve
| Flow::RefundsRetrieveForceSync | Flow::RefundsRetrieveForceSync
| Flow::RefundsUpdate | Flow::RefundsUpdate
| Flow::RefundsList => Self::Refunds, | Flow::RefundsList
| Flow::RefundsFilters => Self::Refunds,
Flow::FrmFulfillment Flow::FrmFulfillment
| Flow::IncomingWebhookReceive | Flow::IncomingWebhookReceive

View File

@ -230,6 +230,7 @@ pub async fn refunds_list(
) )
.await .await
} }
/// Refunds - Filter /// Refunds - Filter
/// ///
/// To list the refunds filters associated with list of connectors, currencies and payment statuses /// To list the refunds filters associated with list of connectors, currencies and payment statuses
@ -267,3 +268,36 @@ pub async fn refunds_filter_list(
) )
.await .await
} }
/// Refunds - Filter V2
///
/// To list the refunds filters associated with list of connectors, currencies and payment statuses
#[utoipa::path(
get,
path = "/refunds/v2/filter",
responses(
(status = 200, description = "List of static filters", body = RefundListFilters),
),
tag = "Refunds",
operation_id = "List all filters for Refunds",
security(("api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::RefundsFilters))]
#[cfg(feature = "olap")]
pub async fn get_refunds_filters(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let flow = Flow::RefundsFilters;
api::server_wrap(
flow,
state,
&req,
(),
|state, auth, _, _| get_filters_for_refunds(state, auth.merchant_account),
auth::auth_type(
&auth::ApiKeyAuth,
&auth::JWTAuth(Permission::RefundRead),
req.headers(),
),
api_locking::LockAction::NotApplicable,
)
.await
}

View File

@ -1,3 +1,4 @@
use api_models::payments::AmountFilter;
use async_bb8_diesel::AsyncRunQueryDsl; use async_bb8_diesel::AsyncRunQueryDsl;
use common_utils::errors::CustomResult; use common_utils::errors::CustomResult;
use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; use diesel::{associations::HasTable, ExpressionMethods, QueryDsl};
@ -104,10 +105,30 @@ impl RefundDbExt for Refund {
} }
} }
if let Some(connector) = refund_list_details.clone().connector { filter = match refund_list_details.amount_filter {
Some(AmountFilter {
start_amount: Some(start),
end_amount: Some(end),
}) => filter.filter(dsl::refund_amount.between(start, end)),
Some(AmountFilter {
start_amount: Some(start),
end_amount: None,
}) => filter.filter(dsl::refund_amount.ge(start)),
Some(AmountFilter {
start_amount: None,
end_amount: Some(end),
}) => filter.filter(dsl::refund_amount.le(end)),
_ => filter,
};
if let Some(connector) = refund_list_details.connector.clone() {
filter = filter.filter(dsl::connector.eq_any(connector)); filter = filter.filter(dsl::connector.eq_any(connector));
} }
if let Some(merchant_connector_id) = refund_list_details.merchant_connector_id.clone() {
filter = filter.filter(dsl::merchant_connector_id.eq_any(merchant_connector_id));
}
if let Some(filter_currency) = &refund_list_details.currency { if let Some(filter_currency) = &refund_list_details.currency {
filter = filter.filter(dsl::currency.eq_any(filter_currency.clone())); filter = filter.filter(dsl::currency.eq_any(filter_currency.clone()));
} }
@ -227,10 +248,30 @@ impl RefundDbExt for Refund {
} }
} }
if let Some(connector) = refund_list_details.clone().connector { filter = match refund_list_details.amount_filter {
Some(AmountFilter {
start_amount: Some(start),
end_amount: Some(end),
}) => filter.filter(dsl::refund_amount.between(start, end)),
Some(AmountFilter {
start_amount: Some(start),
end_amount: None,
}) => filter.filter(dsl::refund_amount.ge(start)),
Some(AmountFilter {
start_amount: None,
end_amount: Some(end),
}) => filter.filter(dsl::refund_amount.le(end)),
_ => filter,
};
if let Some(connector) = refund_list_details.connector.clone() {
filter = filter.filter(dsl::connector.eq_any(connector)); filter = filter.filter(dsl::connector.eq_any(connector));
} }
if let Some(merchant_connector_id) = refund_list_details.merchant_connector_id.clone() {
filter = filter.filter(dsl::merchant_connector_id.eq_any(merchant_connector_id))
}
if let Some(filter_currency) = &refund_list_details.currency { if let Some(filter_currency) = &refund_list_details.currency {
filter = filter.filter(dsl::currency.eq_any(filter_currency.clone())); filter = filter.filter(dsl::currency.eq_any(filter_currency.clone()));
} }

View File

@ -190,6 +190,8 @@ pub enum Flow {
RefundsUpdate, RefundsUpdate,
/// Refunds list flow. /// Refunds list flow.
RefundsList, RefundsList,
/// Refunds filters flow
RefundsFilters,
// Retrieve forex flow. // Retrieve forex flow.
RetrieveForexFlow, RetrieveForexFlow,
/// Toggles recon service for a merchant. /// Toggles recon service for a merchant.

View File

@ -4910,6 +4910,23 @@
"AliPayRedirection": { "AliPayRedirection": {
"type": "object" "type": "object"
}, },
"AmountFilter": {
"type": "object",
"properties": {
"start_amount": {
"type": "integer",
"format": "int64",
"description": "The start amount to filter list of transactions which are greater than or equal to the start amount",
"nullable": true
},
"end_amount": {
"type": "integer",
"format": "int64",
"description": "The end amount to filter list of transactions which are less than or equal to the end amount",
"nullable": true
}
}
},
"AmountInfo": { "AmountInfo": {
"type": "object", "type": "object",
"required": [ "required": [
@ -16785,6 +16802,14 @@
"description": "The starting point within a list of objects", "description": "The starting point within a list of objects",
"nullable": true "nullable": true
}, },
"amount_filter": {
"allOf": [
{
"$ref": "#/components/schemas/AmountFilter"
}
],
"nullable": true
},
"connector": { "connector": {
"type": "array", "type": "array",
"items": { "items": {
@ -16793,6 +16818,14 @@
"description": "The list of connectors to filter refunds list", "description": "The list of connectors to filter refunds list",
"nullable": true "nullable": true
}, },
"merchant_connector_id": {
"type": "array",
"items": {
"type": "string"
},
"description": "The list of merchant connector ids to filter the refunds list for selected label",
"nullable": true
},
"currency": { "currency": {
"type": "array", "type": "array",
"items": { "items": {