mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +08:00 
			
		
		
		
	feat(router): add mandates list api (#1143)
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							bd8868efd0
						
					
				
				
					commit
					75ba3ff09f
				
			| @ -1,5 +1,6 @@ | |||||||
| use masking::Secret; | use masking::Secret; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  | use time::PrimitiveDateTime; | ||||||
| use utoipa::ToSchema; | use utoipa::ToSchema; | ||||||
|  |  | ||||||
| use crate::{enums as api_enums, payments}; | use crate::{enums as api_enums, payments}; | ||||||
| @ -60,3 +61,33 @@ pub struct MandateCardDetails { | |||||||
|     /// A unique identifier alias to identify a particular card |     /// A unique identifier alias to identify a particular card | ||||||
|     pub card_fingerprint: Option<Secret<String>>, |     pub card_fingerprint: Option<Secret<String>>, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, Deserialize, ToSchema)] | ||||||
|  | #[serde(deny_unknown_fields)] | ||||||
|  | pub struct MandateListConstraints { | ||||||
|  |     /// limit on the number of objects to return | ||||||
|  |     pub limit: Option<i64>, | ||||||
|  |     /// status of the mandate | ||||||
|  |     pub mandate_status: Option<api_enums::MandateStatus>, | ||||||
|  |     /// connector linked to mandate | ||||||
|  |     pub connector: Option<String>, | ||||||
|  |     /// The time at which mandate is created | ||||||
|  |     #[schema(example = "2022-09-10T10:11:12Z")] | ||||||
|  |     pub created_time: Option<PrimitiveDateTime>, | ||||||
|  |     /// Time less than the mandate created time | ||||||
|  |     #[schema(example = "2022-09-10T10:11:12Z")] | ||||||
|  |     #[serde(rename = "created_time.lt")] | ||||||
|  |     pub created_time_lt: Option<PrimitiveDateTime>, | ||||||
|  |     /// Time greater than the mandate created time | ||||||
|  |     #[schema(example = "2022-09-10T10:11:12Z")] | ||||||
|  |     #[serde(rename = "created_time.gt")] | ||||||
|  |     pub created_time_gt: Option<PrimitiveDateTime>, | ||||||
|  |     /// 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<PrimitiveDateTime>, | ||||||
|  |     /// 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<PrimitiveDateTime>, | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| use common_utils::{ext_traits::Encode, pii}; | use common_utils::{ext_traits::Encode, pii}; | ||||||
| use error_stack::{report, ResultExt}; | use error_stack::{report, ResultExt}; | ||||||
|  | use futures::future; | ||||||
| use router_env::{instrument, logger, tracing}; | use router_env::{instrument, logger, tracing}; | ||||||
| use storage_models::enums as storage_enums; | use storage_models::enums as storage_enums; | ||||||
|  |  | ||||||
| @ -259,6 +260,25 @@ where | |||||||
|     Ok(resp) |     Ok(resp) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[instrument(skip(state))] | ||||||
|  | pub async fn retrieve_mandates_list( | ||||||
|  |     state: &AppState, | ||||||
|  |     merchant_account: storage::MerchantAccount, | ||||||
|  |     constraints: api_models::mandates::MandateListConstraints, | ||||||
|  | ) -> RouterResponse<Vec<api_models::mandates::MandateResponse>> { | ||||||
|  |     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<Result<types::PaymentsResponseData, types::ErrorResponse>> | impl ForeignTryFrom<Result<types::PaymentsResponseData, types::ErrorResponse>> | ||||||
|     for Option<pii::SecretSerdeValue> |     for Option<pii::SecretSerdeValue> | ||||||
| { | { | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ use super::{MockDb, Store}; | |||||||
| use crate::{ | use crate::{ | ||||||
|     connection, |     connection, | ||||||
|     core::errors::{self, CustomResult}, |     core::errors::{self, CustomResult}, | ||||||
|     types::storage, |     types::storage::{self, MandateDbExt}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| @ -28,6 +28,12 @@ pub trait MandateInterface { | |||||||
|         mandate: storage::MandateUpdate, |         mandate: storage::MandateUpdate, | ||||||
|     ) -> CustomResult<storage::Mandate, errors::StorageError>; |     ) -> CustomResult<storage::Mandate, errors::StorageError>; | ||||||
|  |  | ||||||
|  |     async fn find_mandates_by_merchant_id( | ||||||
|  |         &self, | ||||||
|  |         merchant_id: &str, | ||||||
|  |         mandate_constraints: api_models::mandates::MandateListConstraints, | ||||||
|  |     ) -> CustomResult<Vec<storage::Mandate>, errors::StorageError>; | ||||||
|  |  | ||||||
|     async fn insert_mandate( |     async fn insert_mandate( | ||||||
|         &self, |         &self, | ||||||
|         mandate: storage::MandateNew, |         mandate: storage::MandateNew, | ||||||
| @ -73,6 +79,18 @@ impl MandateInterface for Store { | |||||||
|             .into_report() |             .into_report() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async fn find_mandates_by_merchant_id( | ||||||
|  |         &self, | ||||||
|  |         merchant_id: &str, | ||||||
|  |         mandate_constraints: api_models::mandates::MandateListConstraints, | ||||||
|  |     ) -> CustomResult<Vec<storage::Mandate>, 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( |     async fn insert_mandate( | ||||||
|         &self, |         &self, | ||||||
|         mandate: storage::MandateNew, |         mandate: storage::MandateNew, | ||||||
| @ -116,6 +134,15 @@ impl MandateInterface for MockDb { | |||||||
|         Err(errors::StorageError::MockDbError)? |         Err(errors::StorageError::MockDbError)? | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async fn find_mandates_by_merchant_id( | ||||||
|  |         &self, | ||||||
|  |         _merchant_id: &str, | ||||||
|  |         _mandate_constraints: api_models::mandates::MandateListConstraints, | ||||||
|  |     ) -> CustomResult<Vec<storage::Mandate>, errors::StorageError> { | ||||||
|  |         // [#172]: Implement function for `MockDb` | ||||||
|  |         Err(errors::StorageError::MockDbError)? | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async fn insert_mandate( |     async fn insert_mandate( | ||||||
|         &self, |         &self, | ||||||
|         _mandate: storage::MandateNew, |         _mandate: storage::MandateNew, | ||||||
|  | |||||||
| @ -365,6 +365,8 @@ impl Mandates { | |||||||
|  |  | ||||||
|         #[cfg(feature = "olap")] |         #[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))); |             route = route.service(web::resource("/{id}").route(web::get().to(get_mandate))); | ||||||
|         } |         } | ||||||
|         #[cfg(feature = "oltp")] |         #[cfg(feature = "oltp")] | ||||||
|  | |||||||
| @ -87,3 +87,44 @@ pub async fn revoke_mandate( | |||||||
|     ) |     ) | ||||||
|     .await |     .await | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Mandates - List Mandates | ||||||
|  | #[utoipa::path( | ||||||
|  |     get, | ||||||
|  |     path = "/mandates/list", | ||||||
|  |     params( | ||||||
|  |         ("limit" = Option<i64>, Query, description = "The maximum number of Mandate Objects to include in the response"), | ||||||
|  |         ("mandate_status" = Option<MandateStatus>, Query, description = "The status of mandate"), | ||||||
|  |         ("connector" = Option<String>, Query, description = "The connector linked to mandate"), | ||||||
|  |         ("created_time" = Option<PrimitiveDateTime>, Query, description = "The time at which mandate is created"), | ||||||
|  |         ("created_time.lt" = Option<PrimitiveDateTime>, Query, description = "Time less than the mandate created time"), | ||||||
|  |         ("created_time.gt" = Option<PrimitiveDateTime>, Query, description = "Time greater than the mandate created time"), | ||||||
|  |         ("created_time.lte" = Option<PrimitiveDateTime>, Query, description = "Time less than or equals to the mandate created time"), | ||||||
|  |         ("created_time.gte" = Option<PrimitiveDateTime>, Query, description = "Time greater than or equals to the mandate created time"), | ||||||
|  |     ), | ||||||
|  |     responses( | ||||||
|  |         (status = 200, description = "The mandate list was retrieved successfully", body = Vec<MandateResponse>), | ||||||
|  |         (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<AppState>, | ||||||
|  |     req: HttpRequest, | ||||||
|  |     payload: web::Query<api_models::mandates::MandateListConstraints>, | ||||||
|  | ) -> 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 | ||||||
|  | } | ||||||
|  | |||||||
| @ -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::{ | pub use storage_models::mandate::{ | ||||||
|     Mandate, MandateNew, MandateUpdate, MandateUpdateInternal, SingleUseMandate, |     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<Vec<Self>, 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<Vec<Self>, errors::DatabaseError> { | ||||||
|  |         let mut filter = <Self as HasTable>::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::<diesel::pg::Pg, _>(&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") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -76,6 +76,12 @@ impl ForeignFrom<storage_enums::MandateStatus> for api_enums::MandateStatus { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl ForeignFrom<api_enums::MandateStatus> for storage_enums::MandateStatus { | ||||||
|  |     fn foreign_from(status: api_enums::MandateStatus) -> Self { | ||||||
|  |         frunk::labelled_convert_from(status) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| impl ForeignFrom<api_enums::PaymentMethod> for storage_enums::PaymentMethod { | impl ForeignFrom<api_enums::PaymentMethod> for storage_enums::PaymentMethod { | ||||||
|     fn foreign_from(pm_type: api_enums::PaymentMethod) -> Self { |     fn foreign_from(pm_type: api_enums::PaymentMethod) -> Self { | ||||||
|         frunk::labelled_convert_from(pm_type) |         frunk::labelled_convert_from(pm_type) | ||||||
|  | |||||||
| @ -96,6 +96,8 @@ pub enum Flow { | |||||||
|     MandatesRetrieve, |     MandatesRetrieve, | ||||||
|     /// Mandates revoke flow. |     /// Mandates revoke flow. | ||||||
|     MandatesRevoke, |     MandatesRevoke, | ||||||
|  |     /// Mandates list flow. | ||||||
|  |     MandatesList, | ||||||
|     /// Payment methods create flow. |     /// Payment methods create flow. | ||||||
|     PaymentMethodsCreate, |     PaymentMethodsCreate, | ||||||
|     /// Payment methods list flow. |     /// Payment methods list flow. | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user