mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 12:15:40 +08:00
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>
This commit is contained in:
@ -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"
|
||||
|
||||
@ -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<Vec<RefundStatus>>)]
|
||||
pub refund_status: Option<Vec<enums::RefundStatus>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct RefundListRequest {
|
||||
/// The identifier for the payment
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub payment_id: Option<common_utils::id_type::GlobalPaymentId>,
|
||||
/// The identifier for the refund
|
||||
pub refund_id: Option<common_utils::id_type::GlobalRefundId>,
|
||||
/// Limit on the number of objects to return
|
||||
pub limit: Option<i64>,
|
||||
/// The starting point within a list of objects
|
||||
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)
|
||||
#[serde(flatten)]
|
||||
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
|
||||
pub connector: Option<Vec<String>>,
|
||||
/// The list of merchant connector ids to filter the refunds list for selected label
|
||||
#[schema(value_type = Option<Vec<String>>)]
|
||||
pub connector_id_list: Option<Vec<common_utils::id_type::MerchantConnectorAccountId>>,
|
||||
/// The list of currencies to filter refunds list
|
||||
#[schema(value_type = Option<Vec<Currency>>)]
|
||||
pub currency: Option<Vec<enums::Currency>>,
|
||||
/// The list of refund statuses to filter refunds list
|
||||
#[schema(value_type = Option<Vec<RefundStatus>>)]
|
||||
pub refund_status: Option<Vec<enums::RefundStatus>>,
|
||||
}
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, ToSchema)]
|
||||
pub struct RefundListResponse {
|
||||
/// The number of refunds included in the list
|
||||
|
||||
@ -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= []
|
||||
|
||||
|
||||
@ -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<common_utils::id_type::PaymentId>,
|
||||
pub refund_id: Option<String>,
|
||||
@ -14,6 +18,22 @@ pub struct RefundListConstraints {
|
||||
pub refund_status: Option<Vec<common_enums::RefundStatus>>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "refunds_v2"))]
|
||||
pub struct RefundListConstraints {
|
||||
pub payment_id: Option<common_utils::id_type::GlobalPaymentId>,
|
||||
pub refund_id: Option<common_utils::id_type::GlobalRefundId>,
|
||||
pub profile_id: common_utils::id_type::ProfileId,
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
pub time_range: Option<common_utils::types::TimeRange>,
|
||||
pub amount_filter: Option<api_models::payments::AmountFilter>,
|
||||
pub connector: Option<Vec<String>>,
|
||||
pub connector_id_list: Option<Vec<common_utils::id_type::MerchantConnectorAccountId>>,
|
||||
pub currency: Option<Vec<common_enums::Currency>>,
|
||||
pub refund_status: Option<Vec<common_enums::RefundStatus>>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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<refunds::RefundListResponse> {
|
||||
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<refunds::RefundResponse> = refund_list
|
||||
.into_iter()
|
||||
.map(refunds::RefundResponse::foreign_try_from)
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
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)]
|
||||
|
||||
@ -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<Vec<storage::Refund>, 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<i64, errors::StorageError> {
|
||||
self.diesel_store
|
||||
.get_total_count_of_refunds(merchant_id, refund_details, storage_scheme)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
||||
@ -91,6 +91,16 @@ pub trait RefundInterface {
|
||||
offset: i64,
|
||||
) -> CustomResult<Vec<diesel_models::refund::Refund>, 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<Vec<diesel_models::refund::Refund>, 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<i64, errors::StorageError>;
|
||||
|
||||
#[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<i64, errors::StorageError>;
|
||||
}
|
||||
|
||||
#[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<Vec<diesel_models::refund::Refund>, errors::StorageError> {
|
||||
let conn = connection::pg_connection_read(self).await?;
|
||||
<diesel_models::refund::Refund as storage_types::RefundDbExt>::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<i64, errors::StorageError> {
|
||||
let conn = connection::pg_connection_read(self).await?;
|
||||
<diesel_models::refund::Refund as storage_types::RefundDbExt>::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<Vec<diesel_models::refund::Refund>, 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::<Vec<_>>();
|
||||
|
||||
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<i64, 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)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let filtered_refunds_count = filtered_refunds.len().try_into().unwrap_or_default();
|
||||
|
||||
Ok(filtered_refunds_count)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
#[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
|
||||
}
|
||||
|
||||
@ -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<AppState>,
|
||||
req: HttpRequest,
|
||||
payload: web::Json<api_models::refunds::RefundListRequest>,
|
||||
) -> 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"),
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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<Vec<Self>, 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<Vec<Self>, 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<Vec<common_utils::id_type::ProfileId>>,
|
||||
time_range: &common_utils::types::TimeRange,
|
||||
) -> CustomResult<Vec<(RefundStatus, i64)>, 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<i64, errors::DatabaseError>;
|
||||
}
|
||||
|
||||
#[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<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(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::<diesel::pg::Pg, _>(&filter).to_string());
|
||||
|
||||
db_metrics::track_database_call::<<Self as HasTable>::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<i64, errors::DatabaseError> {
|
||||
let mut filter = <Self as HasTable>::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::<diesel::pg::Pg, _>(&filter).to_string());
|
||||
|
||||
filter
|
||||
.get_result_async::<i64>(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,
|
||||
|
||||
Reference in New Issue
Block a user