From 839eb2e8fb436ec0a79fe0073923fcb060b42cfc Mon Sep 17 00:00:00 2001 From: Amey Wale <76102448+AmeyWale@users.noreply.github.com> Date: Mon, 12 May 2025 19:27:05 +0530 Subject: [PATCH] feat(refunds_v2): Add refunds list flow in v2 apis (#7966) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 51 +++- crates/api_models/src/refunds.rs | 31 +- crates/hyperswitch_domain_models/Cargo.toml | 1 + .../hyperswitch_domain_models/src/refunds.rs | 52 ++++ crates/openapi/src/openapi_v2.rs | 1 + crates/openapi/src/routes/refunds.rs | 18 ++ crates/router/Cargo.toml | 2 +- crates/router/src/core/refunds_v2.rs | 51 ++++ crates/router/src/db/kafka_store.rs | 32 ++ crates/router/src/db/refund.rs | 274 ++++++++++++++++++ crates/router/src/routes/app.rs | 20 +- crates/router/src/routes/refunds.rs | 31 ++ crates/router/src/types/api/refunds.rs | 4 +- crates/router/src/types/storage/refund.rs | 165 ++++++++++- 14 files changed, 716 insertions(+), 17 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index bcc9719563..c6b6f01fb0 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -3306,6 +3306,43 @@ ] } }, + "/v2/refunds/list": { + "get": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - List", + "description": "To list the refunds associated with a payment_id or with the merchant, if payment_id is not provided", + "operationId": "List all Refunds", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "List of refunds", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundListResponse" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/v2/process_tracker/revenue_recovery_workflow/{revenue_recovery_id}": { "get": { "tags": [ @@ -20502,13 +20539,11 @@ "nullable": true }, "refund_id": { - "type": "string", - "description": "The identifier for the refund", - "nullable": true - }, - "profile_id": { - "type": "string", - "description": "The identifier for business profile", + "allOf": [ + { + "$ref": "#/components/schemas/common_utils.id_type.GlobalRefundId" + } + ], "nullable": true }, "limit": { @@ -20539,7 +20574,7 @@ "description": "The list of connectors to filter refunds list", "nullable": true }, - "merchant_connector_id": { + "connector_id_list": { "type": "array", "items": { "type": "string" diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 775e0dfeef..bb20978e0d 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -330,6 +330,7 @@ pub struct RefundErrorDetails { pub message: String, } +#[cfg(feature = "v1")] #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] pub struct RefundListRequest { /// The identifier for the payment @@ -361,7 +362,35 @@ pub struct RefundListRequest { #[schema(value_type = Option>)] pub refund_status: Option>, } - +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct RefundListRequest { + /// The identifier for the payment + #[schema(value_type = Option)] + pub payment_id: Option, + /// The identifier for the refund + pub refund_id: Option, + /// Limit on the number of objects to return + pub limit: 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) + #[serde(flatten)] + pub time_range: Option, + /// 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, + /// The list of connectors to filter refunds list + pub connector: Option>, + /// The list of merchant connector ids to filter the refunds list for selected label + #[schema(value_type = Option>)] + pub connector_id_list: 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, Eq, PartialEq, Serialize, ToSchema)] pub struct RefundListResponse { /// The number of refunds included in the list diff --git a/crates/hyperswitch_domain_models/Cargo.toml b/crates/hyperswitch_domain_models/Cargo.toml index 39aadb90db..e889d2c16c 100644 --- a/crates/hyperswitch_domain_models/Cargo.toml +++ b/crates/hyperswitch_domain_models/Cargo.toml @@ -17,6 +17,7 @@ v2 = ["api_models/v2", "diesel_models/v2", "common_utils/v2", "common_types/v2"] v1 = ["api_models/v1", "diesel_models/v1", "common_utils/v1", "common_types/v1"] customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2"] payment_methods_v2 = ["api_models/payment_methods_v2", "diesel_models/payment_methods_v2"] +refunds_v2 = ["api_models/refunds_v2"] dummy_connector = [] revenue_recovery= [] diff --git a/crates/hyperswitch_domain_models/src/refunds.rs b/crates/hyperswitch_domain_models/src/refunds.rs index 4016754510..f8e7e0bad1 100644 --- a/crates/hyperswitch_domain_models/src/refunds.rs +++ b/crates/hyperswitch_domain_models/src/refunds.rs @@ -1,5 +1,9 @@ +#[cfg(all(feature = "v2", feature = "refunds_v2"))] +use crate::business_profile::Profile; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))] use crate::errors; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))] pub struct RefundListConstraints { pub payment_id: Option, pub refund_id: Option, @@ -14,6 +18,22 @@ pub struct RefundListConstraints { pub refund_status: Option>, } +#[cfg(all(feature = "v2", feature = "refunds_v2"))] +pub struct RefundListConstraints { + pub payment_id: Option, + pub refund_id: Option, + pub profile_id: common_utils::id_type::ProfileId, + pub limit: Option, + pub offset: Option, + pub time_range: Option, + pub amount_filter: Option, + pub connector: Option>, + pub connector_id_list: Option>, + pub currency: Option>, + pub refund_status: Option>, +} + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))] impl TryFrom<( api_models::refunds::RefundListRequest, @@ -80,3 +100,35 @@ impl }) } } + +#[cfg(all(feature = "v2", feature = "refunds_v2"))] +impl From<(api_models::refunds::RefundListRequest, Profile)> for RefundListConstraints { + fn from((value, profile): (api_models::refunds::RefundListRequest, Profile)) -> Self { + let api_models::refunds::RefundListRequest { + payment_id, + refund_id, + connector, + currency, + refund_status, + limit, + offset, + time_range, + amount_filter, + connector_id_list, + } = value; + + Self { + payment_id, + refund_id, + profile_id: profile.get_id().to_owned(), + limit, + offset, + time_range, + amount_filter, + connector, + connector_id_list, + currency, + refund_status, + } + } +} diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index ccc41bc78a..95b6c3d7df 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -149,6 +149,7 @@ Never share your secret api keys. Keep them guarded and secure. //Routes for refunds routes::refunds::refunds_create, routes::refunds::refunds_retrieve, + routes::refunds::refunds_list, // Routes for Revenue Recovery flow under Process Tracker routes::revenue_recovery::revenue_recovery_pt_retrieve_api diff --git a/crates/openapi/src/routes/refunds.rs b/crates/openapi/src/routes/refunds.rs index 5c55de2d17..8fb6d4f183 100644 --- a/crates/openapi/src/routes/refunds.rs +++ b/crates/openapi/src/routes/refunds.rs @@ -128,6 +128,7 @@ pub async fn refunds_update() {} operation_id = "List all Refunds", security(("api_key" = [])) )] +#[cfg(feature = "v1")] pub fn refunds_list() {} /// Refunds - List For the Given profiles @@ -233,3 +234,20 @@ pub async fn refunds_create() {} )] #[cfg(feature = "v2")] pub async fn refunds_retrieve() {} + +/// Refunds - List +/// +/// To list the refunds associated with a payment_id or with the merchant, if payment_id is not provided +#[utoipa::path( + get, + path = "/v2/refunds/list", + request_body=RefundListRequest, + responses( + (status = 200, description = "List of refunds", body = RefundListResponse), + ), + tag = "Refunds", + operation_id = "List all Refunds", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub fn refunds_list() {} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index e80b9672f2..d14eaaf83d 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -39,7 +39,7 @@ customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2", "hyperswit payment_methods_v2 = ["api_models/payment_methods_v2", "diesel_models/payment_methods_v2", "hyperswitch_domain_models/payment_methods_v2", "storage_impl/payment_methods_v2", "common_utils/payment_methods_v2"] dynamic_routing = ["external_services/dynamic_routing", "storage_impl/dynamic_routing", "api_models/dynamic_routing"] revenue_recovery =["api_models/revenue_recovery","hyperswitch_interfaces/revenue_recovery","hyperswitch_domain_models/revenue_recovery","hyperswitch_connectors/revenue_recovery"] -refunds_v2 = ["api_models/refunds_v2", "diesel_models/refunds_v2", "storage_impl/refunds_v2"] +refunds_v2 = ["api_models/refunds_v2", "diesel_models/refunds_v2", "storage_impl/refunds_v2", "hyperswitch_domain_models/refunds_v2"] # Partial Auth # The feature reduces the overhead of the router authenticating the merchant for every request, and trusts on `x-merchant-id` header to be present in the request. diff --git a/crates/router/src/core/refunds_v2.rs b/crates/router/src/core/refunds_v2.rs index b6ccfe8376..8d113d5a32 100644 --- a/crates/router/src/core/refunds_v2.rs +++ b/crates/router/src/core/refunds_v2.rs @@ -4,6 +4,7 @@ use api_models::{enums::Connector, refunds::RefundErrorDetails}; use common_utils::{id_type, types as common_utils_types}; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ + refunds::RefundListConstraints, router_data::{ErrorResponse, RouterData}, router_data_v2::RefundFlowData, }; @@ -734,6 +735,56 @@ pub fn build_refund_update_for_rsync( } } +// ********************************************** Refund list ********************************************** + +/// If payment_id is provided, lists all the refunds associated with that particular payment_id +/// If payment_id is not provided, lists the refunds associated with that particular merchant - to the limit specified,if no limits given, it is 10 by default +#[instrument(skip_all)] +#[cfg(feature = "olap")] +pub async fn refund_list( + state: SessionState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + req: refunds::RefundListRequest, +) -> errors::RouterResponse { + let db = state.store; + let limit = refunds_validator::validate_refund_list(req.limit)?; + let offset = req.offset.unwrap_or_default(); + + let refund_list = db + .filter_refund_by_constraints( + merchant_account.get_id(), + RefundListConstraints::from((req.clone(), profile.clone())), + merchant_account.storage_scheme, + limit, + offset, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::RefundNotFound)?; + + let data: Vec = refund_list + .into_iter() + .map(refunds::RefundResponse::foreign_try_from) + .collect::>()?; + + let total_count = db + .get_total_count_of_refunds( + merchant_account.get_id(), + RefundListConstraints::from((req, profile)), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)?; + + Ok(services::ApplicationResponse::Json( + api_models::refunds::RefundListResponse { + count: data.len(), + total_count, + data, + }, + )) +} + // ********************************************** VALIDATIONS ********************************************** #[instrument(skip_all)] diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 0d0ba623a4..be903ef3bc 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -2844,6 +2844,26 @@ impl RefundInterface for KafkaStore { .await } + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + async fn filter_refund_by_constraints( + &self, + merchant_id: &id_type::MerchantId, + refund_details: refunds::RefundListConstraints, + storage_scheme: MerchantStorageScheme, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .filter_refund_by_constraints( + merchant_id, + refund_details, + storage_scheme, + limit, + offset, + ) + .await + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "refunds_v2"), @@ -2892,6 +2912,18 @@ impl RefundInterface for KafkaStore { .get_total_count_of_refunds(merchant_id, refund_details, storage_scheme) .await } + + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + async fn get_total_count_of_refunds( + &self, + merchant_id: &id_type::MerchantId, + refund_details: refunds::RefundListConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_total_count_of_refunds(merchant_id, refund_details, storage_scheme) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index fbbe0a6d15..7d9a870316 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -91,6 +91,16 @@ pub trait RefundInterface { offset: i64, ) -> CustomResult, errors::StorageError>; + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + async fn filter_refund_by_constraints( + &self, + merchant_id: &common_utils::id_type::MerchantId, + refund_details: refunds::RefundListConstraints, + storage_scheme: enums::MerchantStorageScheme, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError>; + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "refunds_v2"), @@ -127,6 +137,14 @@ pub trait RefundInterface { refund_details: &refunds::RefundListConstraints, storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult; + + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + async fn get_total_count_of_refunds( + &self, + merchant_id: &common_utils::id_type::MerchantId, + refund_details: refunds::RefundListConstraints, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult; } #[cfg(not(feature = "kv_store"))] @@ -925,6 +943,28 @@ mod storage { .map_err(|error| report!(errors::StorageError::from(error))) } + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + #[instrument(skip_all)] + async fn filter_refund_by_constraints( + &self, + merchant_id: &common_utils::id_type::MerchantId, + refund_details: refunds::RefundListConstraints, + _storage_scheme: enums::MerchantStorageScheme, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + ::filter_by_constraints( + &conn, + merchant_id, + refund_details, + limit, + offset, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "refunds_v2"), @@ -983,6 +1023,24 @@ mod storage { .await .map_err(|error| report!(errors::StorageError::from(error))) } + + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + #[instrument(skip_all)] + async fn get_total_count_of_refunds( + &self, + merchant_id: &common_utils::id_type::MerchantId, + refund_details: refunds::RefundListConstraints, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + ::get_refunds_count( + &conn, + merchant_id, + refund_details, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } } } @@ -1369,6 +1427,115 @@ impl RefundInterface for MockDb { Ok(filtered_refunds) } + #[cfg(all(feature = "v2", feature = "refunds_v2", feature = "olap"))] + async fn filter_refund_by_constraints( + &self, + merchant_id: &common_utils::id_type::MerchantId, + refund_details: refunds::RefundListConstraints, + _storage_scheme: enums::MerchantStorageScheme, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + let mut unique_connectors = HashSet::new(); + let mut unique_connector_ids = HashSet::new(); + let mut unique_currencies = HashSet::new(); + let mut unique_statuses = HashSet::new(); + + // Fill the hash sets with data from refund_details + if let Some(connectors) = &refund_details.connector { + connectors.iter().for_each(|connector| { + unique_connectors.insert(connector); + }); + } + + if let Some(connector_id_list) = &refund_details.connector_id_list { + connector_id_list.iter().for_each(|unique_connector_id| { + unique_connector_ids.insert(unique_connector_id); + }); + } + + if let Some(currencies) = &refund_details.currency { + currencies.iter().for_each(|currency| { + unique_currencies.insert(currency); + }); + } + + if let Some(refund_statuses) = &refund_details.refund_status { + refund_statuses.iter().for_each(|refund_status| { + unique_statuses.insert(refund_status); + }); + } + + let refunds = self.refunds.lock().await; + let filtered_refunds = refunds + .iter() + .filter(|refund| refund.merchant_id == *merchant_id) + .filter(|refund| { + refund_details + .payment_id + .clone() + .map_or(true, |id| id == refund.payment_id) + }) + .filter(|refund| { + refund_details + .refund_id + .clone() + .map_or(true, |id| id == refund.id) + }) + .filter(|refund| { + refund + .profile_id + .as_ref() + .is_some_and(|profile_id| profile_id == &refund_details.profile_id) + }) + .filter(|refund| { + refund.created_at + >= refund_details.time_range.map_or( + common_utils::date_time::now() - time::Duration::days(60), + |range| range.start_time, + ) + && refund.created_at + <= refund_details + .time_range + .map_or(common_utils::date_time::now(), |range| { + 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 + >= MinorUnit::new(amount.start_amount.unwrap_or(i64::MIN)) + && refund.refund_amount + <= MinorUnit::new(amount.end_amount.unwrap_or(i64::MAX)) + }) + }) + .filter(|refund| { + unique_connectors.is_empty() || unique_connectors.contains(&refund.connector) + }) + .filter(|refund| { + unique_connector_ids.is_empty() + || refund + .connector_id + .as_ref() + .is_some_and(|id| unique_connector_ids.contains(id)) + }) + .filter(|refund| { + unique_currencies.is_empty() || unique_currencies.contains(&refund.currency) + }) + .filter(|refund| { + unique_statuses.is_empty() || unique_statuses.contains(&refund.refund_status) + }) + .skip(usize::try_from(offset).unwrap_or_default()) + .take(usize::try_from(limit).unwrap_or(MAX_LIMIT)) + .cloned() + .collect::>(); + + Ok(filtered_refunds) + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "refunds_v2"), @@ -1586,4 +1753,111 @@ impl RefundInterface for MockDb { Ok(filtered_refunds_count) } + + #[cfg(all(feature = "v2", feature = "refunds_v2", feature = "olap"))] + async fn get_total_count_of_refunds( + &self, + merchant_id: &common_utils::id_type::MerchantId, + refund_details: refunds::RefundListConstraints, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let mut unique_connectors = HashSet::new(); + let mut unique_connector_ids = HashSet::new(); + let mut unique_currencies = HashSet::new(); + let mut unique_statuses = HashSet::new(); + + // Fill the hash sets with data from refund_details + if let Some(connectors) = &refund_details.connector { + connectors.iter().for_each(|connector| { + unique_connectors.insert(connector); + }); + } + + if let Some(connector_id_list) = &refund_details.connector_id_list { + connector_id_list.iter().for_each(|unique_connector_id| { + unique_connector_ids.insert(unique_connector_id); + }); + } + + if let Some(currencies) = &refund_details.currency { + currencies.iter().for_each(|currency| { + unique_currencies.insert(currency); + }); + } + + if let Some(refund_statuses) = &refund_details.refund_status { + refund_statuses.iter().for_each(|refund_status| { + unique_statuses.insert(refund_status); + }); + } + + let refunds = self.refunds.lock().await; + let filtered_refunds = refunds + .iter() + .filter(|refund| refund.merchant_id == *merchant_id) + .filter(|refund| { + refund_details + .payment_id + .clone() + .map_or(true, |id| id == refund.payment_id) + }) + .filter(|refund| { + refund_details + .refund_id + .clone() + .map_or(true, |id| id == refund.id) + }) + .filter(|refund| { + refund + .profile_id + .as_ref() + .is_some_and(|profile_id| profile_id == &refund_details.profile_id) + }) + .filter(|refund| { + refund.created_at + >= refund_details.time_range.map_or( + common_utils::date_time::now() - time::Duration::days(60), + |range| range.start_time, + ) + && refund.created_at + <= refund_details + .time_range + .map_or(common_utils::date_time::now(), |range| { + 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 + >= MinorUnit::new(amount.start_amount.unwrap_or(i64::MIN)) + && refund.refund_amount + <= MinorUnit::new(amount.end_amount.unwrap_or(i64::MAX)) + }) + }) + .filter(|refund| { + unique_connectors.is_empty() || unique_connectors.contains(&refund.connector) + }) + .filter(|refund| { + unique_connector_ids.is_empty() + || refund + .connector_id + .as_ref() + .is_some_and(|id| unique_connector_ids.contains(id)) + }) + .filter(|refund| { + unique_currencies.is_empty() || unique_currencies.contains(&refund.currency) + }) + .filter(|refund| { + unique_statuses.is_empty() || unique_statuses.contains(&refund.refund_status) + }) + .cloned() + .collect::>(); + + let filtered_refunds_count = filtered_refunds.len().try_into().unwrap_or_default(); + + Ok(filtered_refunds_count) + } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 11e6349113..02ce2eeeb5 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1169,14 +1169,26 @@ impl Refunds { } } -#[cfg(all(feature = "v2", feature = "refunds_v2", feature = "oltp"))] +#[cfg(all( + feature = "v2", + feature = "refunds_v2", + any(feature = "olap", feature = "oltp") +))] impl Refunds { pub fn server(state: AppState) -> Scope { let mut route = web::scope("/v2/refunds").app_data(web::Data::new(state)); - route = route - .service(web::resource("").route(web::post().to(refunds::refunds_create))) - .service(web::resource("/{id}").route(web::get().to(refunds::refunds_retrieve))); + #[cfg(feature = "olap")] + { + route = + route.service(web::resource("/list").route(web::get().to(refunds::refunds_list))); + } + #[cfg(feature = "oltp")] + { + route = route + .service(web::resource("").route(web::post().to(refunds::refunds_create))) + .service(web::resource("/{id}").route(web::get().to(refunds::refunds_retrieve))); + } route } diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index 65ea8720f9..dc92545841 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -375,6 +375,37 @@ pub async fn refunds_list( .await } +#[cfg(all(feature = "v2", feature = "refunds_v2", feature = "olap"))] +#[instrument(skip_all, fields(flow = ?Flow::RefundsList))] +pub async fn refunds_list( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::RefundsList; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload.into_inner(), + |state, auth: auth::AuthenticationData, req, _| { + refund_list(state, auth.merchant_account, auth.profile, req) + }, + auth::auth_type( + &auth::V2ApiKeyAuth { + is_connected_allowed: false, + is_platform_allowed: false, + }, + &auth::JWTAuth { + permission: Permission::MerchantRefundRead, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "refunds_v2"), diff --git a/crates/router/src/types/api/refunds.rs b/crates/router/src/types/api/refunds.rs index 8beaf7010f..935cb14c1f 100644 --- a/crates/router/src/types/api/refunds.rs +++ b/crates/router/src/types/api/refunds.rs @@ -3,8 +3,8 @@ pub use api_models::refunds::RefundRequest; #[cfg(all(feature = "v2", feature = "refunds_v2"))] pub use api_models::refunds::RefundsCreateRequest; pub use api_models::refunds::{ - RefundResponse, RefundStatus, RefundType, RefundUpdateRequest, RefundsRetrieveBody, - RefundsRetrieveRequest, + RefundListRequest, RefundListResponse, RefundResponse, RefundStatus, RefundType, + RefundUpdateRequest, RefundsRetrieveBody, RefundsRetrieveRequest, }; pub use hyperswitch_domain_models::router_flow_types::refunds::{Execute, RSync}; pub use hyperswitch_interfaces::api::refunds::{Refund, RefundExecute, RefundSync}; diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index 646b2742d5..8f0472bb8f 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -5,11 +5,14 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Q pub use diesel_models::refund::{ Refund, RefundCoreWorkflow, RefundNew, RefundUpdate, RefundUpdateInternal, }; +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))] +use diesel_models::schema::refund::dsl; +#[cfg(all(feature = "v2", feature = "refunds_v2"))] +use diesel_models::schema_v2::refund::dsl; use diesel_models::{ enums::{Currency, RefundStatus}, errors, query::generics::db_metrics, - schema::refund::dsl, }; use error_stack::ResultExt; use hyperswitch_domain_models::refunds; @@ -27,6 +30,15 @@ pub trait RefundDbExt: Sized { offset: i64, ) -> CustomResult, errors::DatabaseError>; + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &common_utils::id_type::MerchantId, + refund_list_details: refunds::RefundListConstraints, + limit: i64, + offset: i64, + ) -> CustomResult, errors::DatabaseError>; + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))] async fn filter_by_meta_constraints( conn: &PgPooledConn, @@ -48,6 +60,13 @@ pub trait RefundDbExt: Sized { profile_id_list: Option>, time_range: &common_utils::types::TimeRange, ) -> CustomResult, errors::DatabaseError>; + + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + async fn get_refunds_count( + conn: &PgPooledConn, + merchant_id: &common_utils::id_type::MerchantId, + refund_list_details: refunds::RefundListConstraints, + ) -> CustomResult; } #[async_trait::async_trait] @@ -164,6 +183,82 @@ impl RefundDbExt for Refund { .attach_printable_lazy(|| "Error filtering records by predicate") } + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &common_utils::id_type::MerchantId, + refund_list_details: refunds::RefundListConstraints, + limit: i64, + offset: i64, + ) -> 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(payment_id) = &refund_list_details.payment_id { + filter = filter.filter(dsl::payment_id.eq(payment_id.to_owned())); + } + + if let Some(refund_id) = &refund_list_details.refund_id { + filter = filter.filter(dsl::id.eq(refund_id.to_owned())); + } + + 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)); + } + } + + 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 { + filter = filter.filter(dsl::connector.eq_any(connector)); + } + + if let Some(connector_id_list) = refund_list_details.connector_id_list { + filter = filter.filter(dsl::connector_id.eq_any(connector_id_list)); + } + + if let Some(filter_currency) = refund_list_details.currency { + filter = filter.filter(dsl::currency.eq_any(filter_currency)); + } + + if let Some(filter_refund_status) = refund_list_details.refund_status { + filter = filter.filter(dsl::refund_status.eq_any(filter_refund_status)); + } + + filter = filter.limit(limit).offset(offset); + + logger::debug!(query = %diesel::debug_query::(&filter).to_string()); + + db_metrics::track_database_call::<::Table, _, _>( + filter.get_results_async(conn), + db_metrics::DatabaseOperation::Filter, + ) + .await + .change_context(errors::DatabaseError::NotFound) + .attach_printable_lazy(|| "Error filtering records by predicate") + + // todo!() + } + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))] async fn filter_by_meta_constraints( conn: &PgPooledConn, @@ -309,6 +404,74 @@ impl RefundDbExt for Refund { .attach_printable_lazy(|| "Error filtering count of refunds") } + #[cfg(all(feature = "v2", feature = "refunds_v2"))] + async fn get_refunds_count( + conn: &PgPooledConn, + merchant_id: &common_utils::id_type::MerchantId, + refund_list_details: refunds::RefundListConstraints, + ) -> CustomResult { + let mut filter = ::table() + .count() + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .into_boxed(); + + if let Some(payment_id) = &refund_list_details.payment_id { + filter = filter.filter(dsl::payment_id.eq(payment_id.to_owned())); + } + + if let Some(refund_id) = &refund_list_details.refund_id { + filter = filter.filter(dsl::id.eq(refund_id.to_owned())); + } + + 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)); + } + } + + 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 { + filter = filter.filter(dsl::connector.eq_any(connector)); + } + + if let Some(connector_id_list) = refund_list_details.connector_id_list { + filter = filter.filter(dsl::connector_id.eq_any(connector_id_list)); + } + + if let Some(filter_currency) = refund_list_details.currency { + filter = filter.filter(dsl::currency.eq_any(filter_currency)); + } + + if let Some(filter_refund_status) = refund_list_details.refund_status { + filter = filter.filter(dsl::refund_status.eq_any(filter_refund_status)); + } + + logger::debug!(query = %diesel::debug_query::(&filter).to_string()); + + filter + .get_result_async::(conn) + .await + .change_context(errors::DatabaseError::NotFound) + .attach_printable_lazy(|| "Error filtering count of refunds") + } + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))] async fn get_refund_status_with_count( conn: &PgPooledConn,