From 88860b9c0be0bc91bcdd6f89b60eb43a18b83b08 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 30 Jun 2023 18:24:41 +0530 Subject: [PATCH] feat(router): add filters for refunds (#1501) Co-authored-by: Sampras Lopes --- crates/api_models/src/enums.rs | 3 + crates/api_models/src/refunds.rs | 71 +++++++----- crates/router/src/core/refunds.rs | 26 ++++- crates/router/src/core/refunds/validator.rs | 9 +- crates/router/src/db/refund.rs | 118 +++++++++++++++++++- crates/router/src/openapi.rs | 1 + crates/router/src/routes/app.rs | 4 +- crates/router/src/routes/refunds.rs | 48 ++++++-- crates/router/src/types/storage/refund.rs | 116 ++++++++++++++++--- crates/router/src/types/transformers.rs | 6 + 10 files changed, 342 insertions(+), 60 deletions(-) diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index f2b2a9d4c8..09eba6e810 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -507,10 +507,13 @@ pub enum WalletIssuer { Debug, Default, Eq, + Hash, PartialEq, strum::Display, strum::EnumString, frunk::LabelledGeneric, + serde::Deserialize, + serde::Serialize, )] #[strum(serialize_all = "snake_case")] pub enum RefundStatus { diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 2108c8cb1a..d98ab4b317 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -1,4 +1,4 @@ -use common_utils::{custom_serde, pii}; +use common_utils::pii; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use utoipa::ToSchema; @@ -132,29 +132,28 @@ pub struct RefundListRequest { pub payment_id: Option, /// Limit on the number of objects to return pub limit: Option, - /// The time at which refund is created - #[serde(default, with = "custom_serde::iso8601::option")] - pub created: Option, - /// Time less than the refund created time - #[serde(default, rename = "created.lt", with = "custom_serde::iso8601::option")] - pub created_lt: Option, - /// Time greater than the refund created time - #[serde(default, rename = "created.gt", with = "custom_serde::iso8601::option")] - pub created_gt: Option, - /// Time less than or equals to the refund created time - #[serde( - default, - rename = "created.lte", - with = "custom_serde::iso8601::option" - )] - pub created_lte: Option, - /// Time greater than or equals to the refund created time - #[serde( - default, - rename = "created.gte", - with = "custom_serde::iso8601::option" - )] - pub created_gte: Option, + /// The starting point within a list of objects + pub offset: 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). + pub time_range: Option, + /// The list of connectors to filter refunds list + pub connector: Option>, + /// The list of currencies to filter refunds list + #[schema(value_type = Option>)] + pub currency: Option>, + /// The list of refund statuses to filter refunds list + #[schema(value_type = Option>)] + pub refund_status: Option>, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, ToSchema)] +pub struct TimeRange { + /// The start time to filter refunds list or to get list of filters. To get list of filters start time is needed to be passed + #[serde(with = "common_utils::custom_serde::iso8601")] + pub start_time: PrimitiveDateTime, + /// The end time to filter refunds list or to get list of filters. If not passed the default time is now + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub end_time: Option, } #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] @@ -165,9 +164,31 @@ pub struct RefundListResponse { pub data: Vec, } +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, ToSchema)] +pub struct RefundListMetaData { + /// The list of available connector filters + pub connector: Vec, + /// The list of available currency filters + #[schema(value_type = Vec)] + pub currency: Vec, + /// The list of available refund status filters + #[schema(value_type = Vec)] + pub status: Vec, +} + /// The status for refunds #[derive( - Debug, Eq, Clone, Copy, PartialEq, Default, Deserialize, Serialize, ToSchema, strum::Display, + Debug, + Eq, + Clone, + Copy, + PartialEq, + Default, + Deserialize, + Serialize, + ToSchema, + strum::Display, + strum::EnumIter, )] #[serde(rename_all = "snake_case")] pub enum RefundStatus { diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 710bef7235..be3c2cc3ac 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -24,6 +24,7 @@ use crate::{ }, utils::{self, OptionExt}, }; + // ********************************************** REFUND EXECUTE ********************************************** #[instrument(skip_all)] @@ -646,20 +647,24 @@ pub async fn refund_list( req: api_models::refunds::RefundListRequest, ) -> RouterResponse { let limit = validator::validate_refund_list(req.limit)?; + let offset = req.offset.unwrap_or_default(); + let refund_list = db .filter_refund_by_constraints( &merchant_account.merchant_id, &req, merchant_account.storage_scheme, limit, + offset, ) .await - .change_context(errors::ApiErrorResponse::RefundNotFound)?; + .to_not_found_response(errors::ApiErrorResponse::RefundNotFound)?; let data: Vec = refund_list .into_iter() .map(ForeignInto::foreign_into) .collect(); + Ok(services::ApplicationResponse::Json( api_models::refunds::RefundListResponse { size: data.len(), @@ -668,6 +673,25 @@ pub async fn refund_list( )) } +#[instrument(skip_all)] +#[cfg(feature = "olap")] +pub async fn refund_filter_list( + db: &dyn db::StorageInterface, + merchant_account: domain::MerchantAccount, + req: api_models::refunds::TimeRange, +) -> RouterResponse { + let filter_list = db + .filter_refund_by_meta_constraints( + &merchant_account.merchant_id, + &req, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::RefundNotFound)?; + + Ok(services::ApplicationResponse::Json(filter_list)) +} + impl ForeignFrom for api::RefundResponse { fn foreign_from(refund: storage::Refund) -> Self { let refund = refund; diff --git a/crates/router/src/core/refunds/validator.rs b/crates/router/src/core/refunds/validator.rs index 520874d217..2fa9c20920 100644 --- a/crates/router/src/core/refunds/validator.rs +++ b/crates/router/src/core/refunds/validator.rs @@ -10,6 +10,11 @@ use crate::{ utils::{self, OptionExt}, }; +// Limit constraints for refunds list flow +pub const LOWER_LIMIT: i64 = 1; +pub const UPPER_LIMIT: i64 = 100; +pub const DEFAULT_LIMIT: i64 = 10; + #[derive(Debug, thiserror::Error)] pub enum RefundValidationError { #[error("The payment attempt was not successful")] @@ -125,7 +130,7 @@ pub async fn validate_uniqueness_of_refund_id_against_merchant_id( pub fn validate_refund_list(limit: Option) -> CustomResult { match limit { Some(limit_val) => { - if !(1..=100).contains(&limit_val) { + if !(LOWER_LIMIT..=UPPER_LIMIT).contains(&limit_val) { Err(errors::ApiErrorResponse::InvalidRequestData { message: "limit should be in between 1 and 100".to_string(), } @@ -134,7 +139,7 @@ pub fn validate_refund_list(limit: Option) -> CustomResult Ok(10), + None => Ok(DEFAULT_LIMIT), } } diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index bb40bd7642..cd55344b7a 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -1,11 +1,19 @@ +#[cfg(feature = "olap")] +use std::collections::HashSet; + use storage_models::{errors::DatabaseError, refund::RefundUpdateInternal}; use super::MockDb; +#[cfg(feature = "olap")] +use crate::types::transformers::ForeignInto; use crate::{ core::errors::{self, CustomResult}, types::storage::{self as storage_types, enums}, }; +#[cfg(feature = "olap")] +const MAX_LIMIT: usize = 100; + #[async_trait::async_trait] pub trait RefundInterface { async fn find_refund_by_internal_reference_id_merchant_id( @@ -64,7 +72,16 @@ pub trait RefundInterface { refund_details: &api_models::refunds::RefundListRequest, storage_scheme: enums::MerchantStorageScheme, limit: i64, + offset: i64, ) -> CustomResult, errors::StorageError>; + + #[cfg(feature = "olap")] + async fn filter_refund_by_meta_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::TimeRange, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult; } #[cfg(not(feature = "kv_store"))] @@ -189,6 +206,7 @@ mod storage { refund_details: &api_models::refunds::RefundListRequest, _storage_scheme: enums::MerchantStorageScheme, limit: i64, + offset: i64, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; ::filter_by_constraints( @@ -196,6 +214,25 @@ mod storage { merchant_id, refund_details, limit, + offset, + ) + .await + .map_err(Into::into) + .into_report() + } + + #[cfg(feature = "olap")] + async fn filter_refund_by_meta_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::TimeRange, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + ::filter_by_meta_constraints( + &conn, + merchant_id, + refund_details, ) .await .map_err(Into::into) @@ -584,11 +621,32 @@ mod storage { refund_details: &api_models::refunds::RefundListRequest, storage_scheme: enums::MerchantStorageScheme, limit: i64, + offset: i64, ) -> CustomResult, errors::StorageError> { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => { let conn = connection::pg_connection_read(self).await?; - ::filter_by_constraints(&conn, merchant_id, refund_details, limit) + ::filter_by_constraints(&conn, merchant_id, refund_details, limit, offset) + .await + .map_err(Into::into) + .into_report() + } + + enums::MerchantStorageScheme::RedisKv => Err(errors::StorageError::KVError.into()), + } + } + + #[cfg(feature = "olap")] + async fn filter_refund_by_meta_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::TimeRange, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + match storage_scheme { + enums::MerchantStorageScheme::PostgresOnly => { + let conn = connection::pg_connection_read(self).await?; + ::filter_by_meta_constraints(&conn, merchant_id, refund_details) .await .map_err(Into::into) .into_report() @@ -760,14 +818,64 @@ impl RefundInterface for MockDb { _refund_details: &api_models::refunds::RefundListRequest, _storage_scheme: enums::MerchantStorageScheme, limit: i64, + offset: i64, ) -> CustomResult, errors::StorageError> { - let refunds = self.refunds.lock().await; - - Ok(refunds + Ok(self + .refunds + .lock() + .await .iter() .filter(|refund| refund.merchant_id == merchant_id) - .take(usize::try_from(limit).unwrap_or(usize::MAX)) + .skip(usize::try_from(offset).unwrap_or_default()) + .take(usize::try_from(limit).unwrap_or(MAX_LIMIT)) .cloned() .collect::>()) } + + #[cfg(feature = "olap")] + async fn filter_refund_by_meta_constraints( + &self, + _merchant_id: &str, + refund_details: &api_models::refunds::TimeRange, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let refunds = self.refunds.lock().await; + + let start_time = refund_details.start_time; + let end_time = refund_details + .end_time + .unwrap_or_else(common_utils::date_time::now); + + let filtered_refunds = refunds + .iter() + .filter(|refund| refund.created_at >= start_time && refund.created_at <= end_time) + .cloned() + .collect::>(); + + let mut refund_meta_data = api_models::refunds::RefundListMetaData { + connector: vec![], + currency: vec![], + status: vec![], + }; + + let mut unique_connectors = HashSet::new(); + let mut unique_currencies = HashSet::new(); + let mut unique_statuses = HashSet::new(); + + for refund in filtered_refunds.into_iter() { + unique_connectors.insert(refund.connector); + + let currency: api_models::enums::Currency = refund.currency.foreign_into(); + unique_currencies.insert(currency); + + let status: api_models::enums::RefundStatus = refund.refund_status.foreign_into(); + unique_statuses.insert(status); + } + + refund_meta_data.connector = unique_connectors.into_iter().collect(); + refund_meta_data.currency = unique_currencies.into_iter().collect(); + refund_meta_data.status = unique_statuses.into_iter().collect(); + + Ok(refund_meta_data) + } } diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 144b6ba6ee..d602496415 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -245,6 +245,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::RedirectResponse, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, + api_models::refunds::TimeRange, api_models::mandates::MandateRevokedResponse, api_models::mandates::MandateResponse, api_models::mandates::MandateCardDetails, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 507cdc3469..c077520390 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -243,7 +243,9 @@ impl Refunds { #[cfg(feature = "olap")] { - route = route.service(web::resource("/list").route(web::get().to(refunds_list))); + route = route + .service(web::resource("/list").route(web::post().to(refunds_list))) + .service(web::resource("/filter").route(web::post().to(refunds_filter_list))); } #[cfg(feature = "oltp")] { diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index 57ac968f54..781da3e0b6 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -179,17 +179,9 @@ pub async fn refunds_update( /// /// To list the refunds associated with a payment_id or with the merchant, if payment_id is not provided #[utoipa::path( - get, + post, path = "/refunds/list", - params( - ("payment_id" = String, Query, description = "The identifier for the payment"), - ("limit" = i64, Query, description = "Limit on the number of objects to return"), - ("created" = PrimitiveDateTime, Query, description = "The time at which refund is created"), - ("created_lt" = PrimitiveDateTime, Query, description = "Time less than the refund created time"), - ("created_gt" = PrimitiveDateTime, Query, description = "Time greater than the refund created time"), - ("created_lte" = PrimitiveDateTime, Query, description = "Time less than or equals to the refund created time"), - ("created_gte" = PrimitiveDateTime, Query, description = "Time greater than or equals to the refund created time") - ), + request_body=RefundListRequest, responses( (status = 200, description = "List of refunds", body = RefundListResponse), ), @@ -199,11 +191,10 @@ pub async fn refunds_update( )] #[instrument(skip_all, fields(flow = ?Flow::RefundsList))] #[cfg(feature = "olap")] -// #[get("/list")] pub async fn refunds_list( state: web::Data, req: HttpRequest, - payload: web::Query, + payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsList; api::server_wrap( @@ -216,3 +207,36 @@ pub async fn refunds_list( ) .await } + +/// Refunds - Filter +/// +/// To list the refunds filters associated with list of connectors, currencies and payment statuses +#[utoipa::path( + post, + path = "/refunds/filter", + request_body=TimeRange, + responses( + (status = 200, description = "List of filters", body = RefundListMetaData), + ), + tag = "Refunds", + operation_id = "List all filters for Refunds", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::RefundsList))] +#[cfg(feature = "olap")] +pub async fn refunds_filter_list( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::RefundsList; + api::server_wrap( + flow, + state.get_ref(), + &req, + payload.into_inner(), + |state, auth, req| refund_filter_list(&*state.store, auth.merchant_account, req), + &auth::ApiKeyAuth, + ) + .await +} diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index 277e0d8833..7f0b58101f 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -5,9 +5,13 @@ use error_stack::{IntoReport, ResultExt}; pub use storage_models::refund::{ Refund, RefundCoreWorkflow, RefundNew, RefundUpdate, RefundUpdateInternal, }; -use storage_models::{errors, schema::refund::dsl}; +use storage_models::{ + enums::{Currency, RefundStatus}, + errors, + schema::refund::dsl, +}; -use crate::{connection::PgPooledConn, logger}; +use crate::{connection::PgPooledConn, logger, types::transformers::ForeignInto}; #[cfg(feature = "kv_store")] impl crate::utils::storage_partitioning::KvStorePartition for Refund {} @@ -19,7 +23,14 @@ pub trait RefundDbExt: Sized { merchant_id: &str, refund_list_details: &api_models::refunds::RefundListRequest, limit: i64, + offset: i64, ) -> CustomResult, errors::DatabaseError>; + + async fn filter_by_meta_constraints( + conn: &PgPooledConn, + merchant_id: &str, + refund_list_details: &api_models::refunds::TimeRange, + ) -> CustomResult; } #[async_trait::async_trait] @@ -29,6 +40,7 @@ impl RefundDbExt for Refund { merchant_id: &str, refund_list_details: &api_models::refunds::RefundListRequest, limit: i64, + offset: i64, ) -> CustomResult, errors::DatabaseError> { let mut filter = ::table() .filter(dsl::merchant_id.eq(merchant_id.to_owned())) @@ -40,24 +52,36 @@ impl RefundDbExt for Refund { filter = filter.filter(dsl::payment_id.eq(pid.to_owned())); } None => { - filter = filter.limit(limit); + filter = filter.limit(limit).offset(offset); } }; - if let Some(created) = refund_list_details.created { - filter = filter.filter(dsl::created_at.eq(created)); + if let Some(time_range) = refund_list_details.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(created_lt) = refund_list_details.created_lt { - filter = filter.filter(dsl::created_at.lt(created_lt)); + + if let Some(connector) = refund_list_details.clone().connector { + filter = filter.filter(dsl::connector.eq_any(connector)); } - if let Some(created_gt) = refund_list_details.created_gt { - filter = filter.filter(dsl::created_at.gt(created_gt)); + + if let Some(filter_currency) = &refund_list_details.currency { + let currency: Vec = filter_currency + .iter() + .map(|currency| (*currency).foreign_into()) + .collect(); + filter = filter.filter(dsl::currency.eq_any(currency)); } - if let Some(created_lte) = refund_list_details.created_lte { - filter = filter.filter(dsl::created_at.le(created_lte)); - } - if let Some(created_gte) = refund_list_details.created_gte { - filter = filter.filter(dsl::created_at.gt(created_gte)); + + if let Some(filter_refund_status) = &refund_list_details.refund_status { + let refund_status: Vec = filter_refund_status + .iter() + .map(|refund_status| (*refund_status).foreign_into()) + .collect(); + filter = filter.filter(dsl::refund_status.eq_any(refund_status)); } logger::debug!(query = %diesel::debug_query::(&filter).to_string()); @@ -69,4 +93,68 @@ impl RefundDbExt for Refund { .change_context(errors::DatabaseError::NotFound) .attach_printable_lazy(|| "Error filtering records by predicate") } + + async fn filter_by_meta_constraints( + conn: &PgPooledConn, + merchant_id: &str, + refund_list_details: &api_models::refunds::TimeRange, + ) -> CustomResult { + let start_time = refund_list_details.start_time; + + let end_time = refund_list_details + .end_time + .unwrap_or_else(common_utils::date_time::now); + + let filter = ::table() + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .order(dsl::modified_at.desc()) + .filter(dsl::created_at.ge(start_time)) + .filter(dsl::created_at.le(end_time)); + + let filter_connector: Vec = filter + .clone() + .select(dsl::connector) + .distinct() + .order_by(dsl::connector.asc()) + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error filtering records by connector")?; + + let filter_currency: Vec = filter + .clone() + .select(dsl::currency) + .distinct() + .order_by(dsl::currency.asc()) + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error filtering records by currency")?; + + let filter_status: Vec = filter + .select(dsl::refund_status) + .distinct() + .order_by(dsl::refund_status.asc()) + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error filtering records by refund status")?; + + let meta = api_models::refunds::RefundListMetaData { + connector: filter_connector, + currency: filter_currency + .into_iter() + .map(|curr| curr.foreign_into()) + .collect(), + status: filter_status + .into_iter() + .map(|curr| curr.foreign_into()) + .collect(), + }; + + Ok(meta) + } } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 473685f4e1..d75313abbd 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -298,6 +298,12 @@ impl ForeignFrom for api_enums::RefundStatus { } } +impl ForeignFrom for storage_enums::RefundStatus { + fn foreign_from(status: api_enums::RefundStatus) -> Self { + frunk::labelled_convert_from(status) + } +} + impl ForeignFrom for storage_enums::CaptureMethod { fn foreign_from(capture_method: api_enums::CaptureMethod) -> Self { frunk::labelled_convert_from(capture_method)