diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 14abcc9f9d..c3ca1e0fd9 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -1,4 +1,6 @@ +use common_utils::custom_serde; use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; use crate::enums; @@ -34,6 +36,35 @@ pub struct RefundResponse { pub error_message: Option, } +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct RefundListRequest { + pub payment_id: Option, + pub limit: Option, + #[serde(default, with = "custom_serde::iso8601::option")] + pub created: Option, + #[serde(default, rename = "created.lt", with = "custom_serde::iso8601::option")] + pub created_lt: Option, + #[serde(default, rename = "created.gt", with = "custom_serde::iso8601::option")] + pub created_gt: Option, + #[serde( + default, + rename = "created.lte", + with = "custom_serde::iso8601::option" + )] + pub created_lte: Option, + #[serde( + default, + rename = "created.gte", + with = "custom_serde::iso8601::option" + )] + pub created_gte: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct RefundListResponse { + pub data: Vec, +} + #[derive(Debug, Eq, Clone, PartialEq, Default, Deserialize, Serialize)] #[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 32d0cd4fdc..af0e3bb013 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -455,6 +455,40 @@ pub async fn validate_and_create_refund( Ok(refund.foreign_into()) } +// ********************************************** 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)] +pub async fn refund_list( + db: &dyn db::StorageInterface, + merchant_account: storage::merchant_account::MerchantAccount, + req: api_models::refunds::RefundListRequest, +) -> RouterResponse { + let limit = validator::validate_refund_list(req.limit)?; + let refund_list = db + .filter_refund_by_constraints( + &merchant_account.merchant_id, + &req, + merchant_account.storage_scheme, + limit, + ) + .await + .change_context(errors::ApiErrorResponse::RefundNotFound)?; + + let data: Vec = refund_list + .into_iter() + .map(ForeignInto::foreign_into) + .collect(); + utils::when(data.is_empty(), || { + Err(errors::ApiErrorResponse::RefundNotFound) + })?; + Ok(services::BachResponse::Json( + api_models::refunds::RefundListResponse { data }, + )) +} + // ********************************************** UTILS ********************************************** // FIXME: function should not have more than 3 arguments. diff --git a/crates/router/src/core/refunds/validator.rs b/crates/router/src/core/refunds/validator.rs index 593269b60f..ecd900b582 100644 --- a/crates/router/src/core/refunds/validator.rs +++ b/crates/router/src/core/refunds/validator.rs @@ -122,3 +122,19 @@ 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) { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "limit should be in between 1 and 100".to_string(), + } + .into()) + } else { + Ok(limit_val) + } + } + None => Ok(10), + } +} diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index 2cbb8cec7a..aec81c3706 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -55,6 +55,14 @@ pub trait RefundInterface { new: storage_types::RefundNew, storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult; + + async fn filter_refund_by_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + storage_scheme: enums::MerchantStorageScheme, + limit: i64, + ) -> CustomResult, errors::StorageError>; } #[cfg(not(feature = "kv_store"))] @@ -163,6 +171,25 @@ mod storage { .map_err(Into::into) .into_report() } + + async fn filter_refund_by_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + _storage_scheme: enums::MerchantStorageScheme, + limit: i64, + ) -> CustomResult, errors::StorageError> { + let conn = pg_connection(&self.master_pool).await; + ::filter_by_constraints( + &conn, + merchant_id, + refund_details, + limit, + ) + .await + .map_err(Into::into) + .into_report() + } } } @@ -546,6 +573,26 @@ mod storage { } } } + + async fn filter_refund_by_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + storage_scheme: enums::MerchantStorageScheme, + limit: i64, + ) -> CustomResult, errors::StorageError> { + match storage_scheme { + enums::MerchantStorageScheme::PostgresOnly => { + let conn = pg_connection(&self.master_pool).await; + ::filter_by_constraints(&conn, merchant_id, refund_details, limit) + .await + .map_err(Into::into) + .into_report() + } + + enums::MerchantStorageScheme::RedisKv => Err(errors::StorageError::KVError.into()), + } + } } } @@ -651,4 +698,15 @@ impl RefundInterface for MockDb { // [#172]: Implement function for `MockDb` Err(errors::StorageError::MockDbError)? } + + async fn filter_refund_by_constraints( + &self, + _merchant_id: &str, + _refund_details: &api_models::refunds::RefundListRequest, + _storage_scheme: enums::MerchantStorageScheme, + _limit: i64, + ) -> CustomResult, errors::StorageError> { + // [#172]: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index efe16f0569..f7b7d868ff 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -113,12 +113,12 @@ impl Refunds { web::scope("/refunds") .app_data(web::Data::new(state)) .service(web::resource("").route(web::post().to(refunds_create))) + .service(web::resource("/list").route(web::get().to(refunds_list))) .service( web::resource("/{id}") .route(web::get().to(refunds_retrieve)) .route(web::post().to(refunds_update)), ) - .service(web::resource("/list").route(web::get().to(refunds_list))) } } diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index 97ba2c8fca..c274ad6ccf 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -68,6 +68,17 @@ pub async fn refunds_update( #[instrument(skip_all, fields(flow = ?Flow::RefundsList))] // #[get("/list")] -pub async fn refunds_list() -> HttpResponse { - api::http_response_json("list") +pub async fn refunds_list( + state: web::Data, + req: HttpRequest, + payload: web::Query, +) -> HttpResponse { + api::server_wrap( + &state, + &req, + payload.into_inner(), + |state, merchant_account, req| refund_list(&*state.store, merchant_account, req), + api::MerchantAuthentication::ApiKey, + ) + .await } diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index a278d2fe13..a0ffb33c98 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -1,6 +1,72 @@ +use async_bb8_diesel::AsyncRunQueryDsl; +use common_utils::errors::CustomResult; +use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; +use error_stack::{IntoReport, ResultExt}; pub use storage_models::refund::{ Refund, RefundCoreWorkflow, RefundNew, RefundUpdate, RefundUpdateInternal, }; +use storage_models::{errors, schema::refund::dsl}; + +use crate::{connection::PgPooledConn, logger}; #[cfg(feature = "kv_store")] impl crate::utils::storage_partitioning::KvStorePartition for Refund {} + +#[async_trait::async_trait] +pub trait RefundDbExt: Sized { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + refund_list_details: &api_models::refunds::RefundListRequest, + limit: i64, + ) -> CustomResult, errors::DatabaseError>; +} + +#[async_trait::async_trait] +impl RefundDbExt for Refund { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + refund_list_details: &api_models::refunds::RefundListRequest, + limit: i64, + ) -> CustomResult, errors::DatabaseError> { + let mut filter = ::table() + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .order_by(dsl::id) + .into_boxed(); + + match &refund_list_details.payment_id { + Some(pid) => { + filter = filter.filter(dsl::payment_id.eq(pid.to_owned())); + } + None => { + filter = filter.limit(limit); + } + }; + + if let Some(created) = refund_list_details.created { + filter = filter.filter(dsl::created_at.eq(created)); + } + if let Some(created_lt) = refund_list_details.created_lt { + filter = filter.filter(dsl::created_at.lt(created_lt)); + } + if let Some(created_gt) = refund_list_details.created_gt { + filter = filter.filter(dsl::created_at.gt(created_gt)); + } + 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)); + } + + logger::debug!(query = %diesel::debug_query::(&filter).to_string()); + + filter + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::NotFound) + .attach_printable_lazy(|| "Error filtering records by predicate") + } +}