feat(router): add filters for refunds (#1501)

Co-authored-by: Sampras Lopes <lsampras@protonmail.com>
This commit is contained in:
Apoorv Dixit
2023-06-30 18:24:41 +05:30
committed by GitHub
parent 7bb0aa5ceb
commit 88860b9c0b
10 changed files with 342 additions and 60 deletions

View File

@ -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 {

View File

@ -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<String>,
/// Limit on the number of objects to return
pub limit: Option<i64>,
/// The time at which refund is created
#[serde(default, with = "custom_serde::iso8601::option")]
pub created: Option<PrimitiveDateTime>,
/// Time less than the refund created time
#[serde(default, rename = "created.lt", with = "custom_serde::iso8601::option")]
pub created_lt: Option<PrimitiveDateTime>,
/// Time greater than the refund created time
#[serde(default, rename = "created.gt", with = "custom_serde::iso8601::option")]
pub created_gt: Option<PrimitiveDateTime>,
/// Time less than or equals to the refund created time
#[serde(
default,
rename = "created.lte",
with = "custom_serde::iso8601::option"
)]
pub created_lte: Option<PrimitiveDateTime>,
/// Time greater than or equals to the refund created time
#[serde(
default,
rename = "created.gte",
with = "custom_serde::iso8601::option"
)]
pub created_gte: Option<PrimitiveDateTime>,
/// 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).
pub time_range: Option<TimeRange>,
/// The list of connectors to filter refunds list
pub connector: Option<Vec<String>>,
/// 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, 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<PrimitiveDateTime>,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
@ -165,9 +164,31 @@ pub struct RefundListResponse {
pub data: Vec<RefundResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, ToSchema)]
pub struct RefundListMetaData {
/// The list of available connector filters
pub connector: Vec<String>,
/// 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 status: Vec<enums::RefundStatus>,
}
/// 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 {

View File

@ -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<api_models::refunds::RefundListResponse> {
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<refunds::RefundResponse> = 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<api_models::refunds::RefundListMetaData> {
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<storage::Refund> for api::RefundResponse {
fn foreign_from(refund: storage::Refund) -> Self {
let refund = refund;

View File

@ -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<i64>) -> CustomResult<i64, errors::ApiErrorResponse> {
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<i64>) -> CustomResult<i64, errors::Api
Ok(limit_val)
}
}
None => Ok(10),
None => Ok(DEFAULT_LIMIT),
}
}

View File

@ -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<Vec<storage_models::refund::Refund>, 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<api_models::refunds::RefundListMetaData, errors::StorageError>;
}
#[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<Vec<storage_models::refund::Refund>, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?;
<storage_models::refund::Refund as storage_types::RefundDbExt>::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<api_models::refunds::RefundListMetaData, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?;
<storage_models::refund::Refund as storage_types::RefundDbExt>::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<Vec<storage_models::refund::Refund>, errors::StorageError> {
match storage_scheme {
enums::MerchantStorageScheme::PostgresOnly => {
let conn = connection::pg_connection_read(self).await?;
<storage_models::refund::Refund as storage_types::RefundDbExt>::filter_by_constraints(&conn, merchant_id, refund_details, limit)
<storage_models::refund::Refund as storage_types::RefundDbExt>::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<api_models::refunds::RefundListMetaData, errors::StorageError> {
match storage_scheme {
enums::MerchantStorageScheme::PostgresOnly => {
let conn = connection::pg_connection_read(self).await?;
<storage_models::refund::Refund as storage_types::RefundDbExt>::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<Vec<storage_models::refund::Refund>, 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::<Vec<_>>())
}
#[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<api_models::refunds::RefundListMetaData, errors::StorageError> {
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::<Vec<storage_models::refund::Refund>>();
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)
}
}

View File

@ -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,

View File

@ -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")]
{

View File

@ -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<AppState>,
req: HttpRequest,
payload: web::Query<api_models::refunds::RefundListRequest>,
payload: web::Json<api_models::refunds::RefundListRequest>,
) -> 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<AppState>,
req: HttpRequest,
payload: web::Json<api_models::refunds::TimeRange>,
) -> 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
}

View File

@ -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<Vec<Self>, errors::DatabaseError>;
async fn filter_by_meta_constraints(
conn: &PgPooledConn,
merchant_id: &str,
refund_list_details: &api_models::refunds::TimeRange,
) -> CustomResult<api_models::refunds::RefundListMetaData, errors::DatabaseError>;
}
#[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<Vec<Self>, errors::DatabaseError> {
let mut filter = <Self as HasTable>::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<Currency> = 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<RefundStatus> = 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::<diesel::pg::Pg, _>(&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<api_models::refunds::RefundListMetaData, errors::DatabaseError> {
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 = <Self as HasTable>::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<String> = 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<Currency> = 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<RefundStatus> = 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)
}
}

View File

@ -298,6 +298,12 @@ impl ForeignFrom<storage_enums::RefundStatus> for api_enums::RefundStatus {
}
}
impl ForeignFrom<api_enums::RefundStatus> for storage_enums::RefundStatus {
fn foreign_from(status: api_enums::RefundStatus) -> Self {
frunk::labelled_convert_from(status)
}
}
impl ForeignFrom<api_enums::CaptureMethod> for storage_enums::CaptureMethod {
fn foreign_from(capture_method: api_enums::CaptureMethod) -> Self {
frunk::labelled_convert_from(capture_method)