diff --git a/crates/api_models/src/mandates.rs b/crates/api_models/src/mandates.rs index a5283b375e..2173304744 100644 --- a/crates/api_models/src/mandates.rs +++ b/crates/api_models/src/mandates.rs @@ -1,5 +1,6 @@ use masking::Secret; use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; use utoipa::ToSchema; use crate::{enums as api_enums, payments}; @@ -60,3 +61,33 @@ pub struct MandateCardDetails { /// A unique identifier alias to identify a particular card pub card_fingerprint: Option>, } + +#[derive(Clone, Debug, Deserialize, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct MandateListConstraints { + /// limit on the number of objects to return + pub limit: Option, + /// status of the mandate + pub mandate_status: Option, + /// connector linked to mandate + pub connector: Option, + /// The time at which mandate is created + #[schema(example = "2022-09-10T10:11:12Z")] + pub created_time: Option, + /// Time less than the mandate created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(rename = "created_time.lt")] + pub created_time_lt: Option, + /// Time greater than the mandate created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(rename = "created_time.gt")] + pub created_time_gt: Option, + /// Time less than or equals to the mandate created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(rename = "created_time.lte")] + pub created_time_lte: Option, + /// Time greater than or equals to the mandate created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(rename = "created_time.gte")] + pub created_time_gte: Option, +} diff --git a/crates/router/src/core/mandate.rs b/crates/router/src/core/mandate.rs index c07a649934..3273c5a579 100644 --- a/crates/router/src/core/mandate.rs +++ b/crates/router/src/core/mandate.rs @@ -1,5 +1,6 @@ use common_utils::{ext_traits::Encode, pii}; use error_stack::{report, ResultExt}; +use futures::future; use router_env::{instrument, logger, tracing}; use storage_models::enums as storage_enums; @@ -259,6 +260,25 @@ where Ok(resp) } +#[instrument(skip(state))] +pub async fn retrieve_mandates_list( + state: &AppState, + merchant_account: storage::MerchantAccount, + constraints: api_models::mandates::MandateListConstraints, +) -> RouterResponse> { + let mandates = state + .store + .find_mandates_by_merchant_id(&merchant_account.merchant_id, constraints) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to retrieve mandates")?; + let mandates_list = future::try_join_all(mandates.into_iter().map(|mandate| { + mandates::MandateResponse::from_db_mandate(state, mandate, &merchant_account) + })) + .await?; + Ok(services::ApplicationResponse::Json(mandates_list)) +} + impl ForeignTryFrom> for Option { diff --git a/crates/router/src/db/mandate.rs b/crates/router/src/db/mandate.rs index b3cbbbd078..2ac9595b77 100644 --- a/crates/router/src/db/mandate.rs +++ b/crates/router/src/db/mandate.rs @@ -4,7 +4,7 @@ use super::{MockDb, Store}; use crate::{ connection, core::errors::{self, CustomResult}, - types::storage, + types::storage::{self, MandateDbExt}, }; #[async_trait::async_trait] @@ -28,6 +28,12 @@ pub trait MandateInterface { mandate: storage::MandateUpdate, ) -> CustomResult; + async fn find_mandates_by_merchant_id( + &self, + merchant_id: &str, + mandate_constraints: api_models::mandates::MandateListConstraints, + ) -> CustomResult, errors::StorageError>; + async fn insert_mandate( &self, mandate: storage::MandateNew, @@ -73,6 +79,18 @@ impl MandateInterface for Store { .into_report() } + async fn find_mandates_by_merchant_id( + &self, + merchant_id: &str, + mandate_constraints: api_models::mandates::MandateListConstraints, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::Mandate::filter_by_constraints(&conn, merchant_id, mandate_constraints) + .await + .map_err(Into::into) + .into_report() + } + async fn insert_mandate( &self, mandate: storage::MandateNew, @@ -116,6 +134,15 @@ impl MandateInterface for MockDb { Err(errors::StorageError::MockDbError)? } + async fn find_mandates_by_merchant_id( + &self, + _merchant_id: &str, + _mandate_constraints: api_models::mandates::MandateListConstraints, + ) -> CustomResult, errors::StorageError> { + // [#172]: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + async fn insert_mandate( &self, _mandate: storage::MandateNew, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index dbfde139d2..d0062faf9e 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -365,6 +365,8 @@ impl Mandates { #[cfg(feature = "olap")] { + route = + route.service(web::resource("/list").route(web::get().to(retrieve_mandates_list))); route = route.service(web::resource("/{id}").route(web::get().to(get_mandate))); } #[cfg(feature = "oltp")] diff --git a/crates/router/src/routes/mandates.rs b/crates/router/src/routes/mandates.rs index b980f08212..a0e211f2dc 100644 --- a/crates/router/src/routes/mandates.rs +++ b/crates/router/src/routes/mandates.rs @@ -87,3 +87,44 @@ pub async fn revoke_mandate( ) .await } + +/// Mandates - List Mandates +#[utoipa::path( + get, + path = "/mandates/list", + params( + ("limit" = Option, Query, description = "The maximum number of Mandate Objects to include in the response"), + ("mandate_status" = Option, Query, description = "The status of mandate"), + ("connector" = Option, Query, description = "The connector linked to mandate"), + ("created_time" = Option, Query, description = "The time at which mandate is created"), + ("created_time.lt" = Option, Query, description = "Time less than the mandate created time"), + ("created_time.gt" = Option, Query, description = "Time greater than the mandate created time"), + ("created_time.lte" = Option, Query, description = "Time less than or equals to the mandate created time"), + ("created_time.gte" = Option, Query, description = "Time greater than or equals to the mandate created time"), + ), + responses( + (status = 200, description = "The mandate list was retrieved successfully", body = Vec), + (status = 401, description = "Unauthorized request") + ), + tag = "Mandates", + operation_id = "List Mandates", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::MandatesList))] +pub async fn retrieve_mandates_list( + state: web::Data, + req: HttpRequest, + payload: web::Query, +) -> HttpResponse { + let flow = Flow::MandatesList; + let payload = payload.into_inner(); + api::server_wrap( + flow, + state.get_ref(), + &req, + payload, + mandate::retrieve_mandates_list, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + ) + .await +} diff --git a/crates/router/src/types/storage/mandate.rs b/crates/router/src/types/storage/mandate.rs index bafecb42f2..0f1bdaae8f 100644 --- a/crates/router/src/types/storage/mandate.rs +++ b/crates/router/src/types/storage/mandate.rs @@ -1,3 +1,71 @@ +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::mandate::{ Mandate, MandateNew, MandateUpdate, MandateUpdateInternal, SingleUseMandate, }; +use storage_models::{errors, schema::mandate::dsl}; + +use crate::{connection::PgPooledConn, logger, types::transformers::ForeignInto}; + +#[async_trait::async_trait] +pub trait MandateDbExt: Sized { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + mandate_list_constraints: api_models::mandates::MandateListConstraints, + ) -> CustomResult, errors::DatabaseError>; +} + +#[async_trait::async_trait] +impl MandateDbExt for Mandate { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + mandate_list_constraints: api_models::mandates::MandateListConstraints, + ) -> CustomResult, errors::DatabaseError> { + let mut filter = ::table() + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .order(dsl::created_at.desc()) + .into_boxed(); + + if let Some(created_time) = mandate_list_constraints.created_time { + filter = filter.filter(dsl::created_at.eq(created_time)); + } + if let Some(created_time_lt) = mandate_list_constraints.created_time_lt { + filter = filter.filter(dsl::created_at.lt(created_time_lt)); + } + if let Some(created_time_gt) = mandate_list_constraints.created_time_gt { + filter = filter.filter(dsl::created_at.gt(created_time_gt)); + } + if let Some(created_time_lte) = mandate_list_constraints.created_time_lte { + filter = filter.filter(dsl::created_at.le(created_time_lte)); + } + if let Some(created_time_gte) = mandate_list_constraints.created_time_gte { + filter = filter.filter(dsl::created_at.ge(created_time_gte)); + } + if let Some(connector) = mandate_list_constraints.connector { + filter = filter.filter(dsl::connector.eq(connector)); + } + if let Some(mandate_status) = mandate_list_constraints.mandate_status { + let storage_mandate_status: storage_models::enums::MandateStatus = + mandate_status.foreign_into(); + filter = filter.filter(dsl::mandate_status.eq(storage_mandate_status)); + } + if let Some(limit) = mandate_list_constraints.limit { + filter = filter.limit(limit); + } + + logger::debug!(query = %diesel::debug_query::(&filter).to_string()); + + filter + .get_results_async(conn) + .await + .into_report() + // The query built here returns an empty Vec when no records are found, and if any error does occur, + // it would be an internal database error, due to which we are raising a DatabaseError::Unknown error + .change_context(errors::DatabaseError::Others) + .attach_printable("Error filtering mandates by specified constraints") + } +} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 7f5a7f1b55..822121803a 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -76,6 +76,12 @@ impl ForeignFrom for api_enums::MandateStatus { } } +impl ForeignFrom for storage_enums::MandateStatus { + fn foreign_from(status: api_enums::MandateStatus) -> Self { + frunk::labelled_convert_from(status) + } +} + impl ForeignFrom for storage_enums::PaymentMethod { fn foreign_from(pm_type: api_enums::PaymentMethod) -> Self { frunk::labelled_convert_from(pm_type) diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 12ebb2e205..0266373beb 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -96,6 +96,8 @@ pub enum Flow { MandatesRetrieve, /// Mandates revoke flow. MandatesRevoke, + /// Mandates list flow. + MandatesList, /// Payment methods create flow. PaymentMethodsCreate, /// Payment methods list flow.