mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +08:00 
			
		
		
		
	feat(core): [Payouts] Add billing address to payout list (#5004)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
		| @ -6,6 +6,8 @@ pub mod transformers; | ||||
| pub mod validator; | ||||
| use std::{collections::HashSet, vec::IntoIter}; | ||||
|  | ||||
| #[cfg(feature = "olap")] | ||||
| use api_models::payments as payment_enums; | ||||
| use api_models::{self, enums as api_enums, payouts::PayoutLinkResponse}; | ||||
| #[cfg(feature = "payout_retry")] | ||||
| use common_enums::PayoutRetryType; | ||||
| @ -33,6 +35,8 @@ use time::Duration; | ||||
|  | ||||
| #[cfg(feature = "olap")] | ||||
| use crate::types::domain::behaviour::Conversion; | ||||
| #[cfg(feature = "olap")] | ||||
| use crate::types::PayoutActionData; | ||||
| use crate::{ | ||||
|     core::{ | ||||
|         errors::{ | ||||
| @ -770,7 +774,9 @@ pub async fn payouts_list_core( | ||||
|     .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; | ||||
|     let payouts = core_utils::filter_objects_based_on_profile_id_list(profile_id_list, payouts); | ||||
|  | ||||
|     let collected_futures = payouts.into_iter().map(|payout| async { | ||||
|     let mut pi_pa_tuple_vec = PayoutActionData::new(); | ||||
|  | ||||
|     for payout in payouts { | ||||
|         match db | ||||
|             .find_payout_attempt_by_merchant_id_payout_attempt_id( | ||||
|                 merchant_id, | ||||
| @ -778,73 +784,80 @@ pub async fn payouts_list_core( | ||||
|                 storage_enums::MerchantStorageScheme::PostgresOnly, | ||||
|             ) | ||||
|             .await | ||||
|             .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         { | ||||
|             Ok(ref payout_attempt) => match payout.customer_id.clone() { | ||||
|                 Some(ref customer_id) => { | ||||
|             Ok(payout_attempt) => { | ||||
|                 let domain_customer = match payout.customer_id.clone() { | ||||
|                     #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] | ||||
|                     match db | ||||
|                     Some(customer_id) => db | ||||
|                         .find_customer_by_customer_id_merchant_id( | ||||
|                             &(&state).into(), | ||||
|                             customer_id, | ||||
|                             &customer_id, | ||||
|                             merchant_id, | ||||
|                             &key_store, | ||||
|                             merchant_account.storage_scheme, | ||||
|                         ) | ||||
|                         .await | ||||
|                     { | ||||
|                         Ok(customer) => Ok((payout, payout_attempt.to_owned(), Some(customer))), | ||||
|                         Err(err) => { | ||||
|                         .map_err(|err| { | ||||
|                             let err_msg = format!( | ||||
|                                 "failed while fetching customer for customer_id - {:?}", | ||||
|                                 customer_id | ||||
|                             ); | ||||
|                             logger::warn!(?err, err_msg); | ||||
|                             if err.current_context().is_db_not_found() { | ||||
|                                 Ok((payout, payout_attempt.to_owned(), None)) | ||||
|                             } else { | ||||
|                                 Err(err | ||||
|                                     .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|                                     .attach_printable(err_msg)) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 None => Ok((payout.to_owned(), payout_attempt.to_owned(), None)), | ||||
|             }, | ||||
|                         }) | ||||
|                         .ok(), | ||||
|                     _ => None, | ||||
|                 }; | ||||
|  | ||||
|                 let payout_id_as_payment_id_type = | ||||
|                     common_utils::id_type::PaymentId::wrap(payout.payout_id.clone()) | ||||
|                         .change_context(errors::ApiErrorResponse::InvalidRequestData { | ||||
|                             message: "payout_id contains invalid data".to_string(), | ||||
|                         }) | ||||
|                         .attach_printable("Error converting payout_id to PaymentId type")?; | ||||
|  | ||||
|                 let payment_addr = payment_helpers::create_or_find_address_for_payment_by_request( | ||||
|                     &state, | ||||
|                     None, | ||||
|                     payout.address_id.as_deref(), | ||||
|                     merchant_id, | ||||
|                     payout.customer_id.as_ref(), | ||||
|                     &key_store, | ||||
|                     &payout_id_as_payment_id_type, | ||||
|                     merchant_account.storage_scheme, | ||||
|                 ) | ||||
|                 .await | ||||
|                 .transpose() | ||||
|                 .and_then(|addr| { | ||||
|                     addr.map_err(|err| { | ||||
|                         let err_msg = format!( | ||||
|                             "billing_address missing for address_id : {:?}", | ||||
|                             payout.address_id | ||||
|                         ); | ||||
|                         logger::warn!(?err, err_msg); | ||||
|                     }) | ||||
|                     .ok() | ||||
|                     .map(payment_enums::Address::foreign_from) | ||||
|                 }); | ||||
|  | ||||
|                 pi_pa_tuple_vec.push(( | ||||
|                     payout.to_owned(), | ||||
|                     payout_attempt.to_owned(), | ||||
|                     domain_customer, | ||||
|                     payment_addr, | ||||
|                 )); | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 let err_msg = format!( | ||||
|                     "failed while fetching payout_attempt for payout_id - {}", | ||||
|                     payout.payout_id.clone(), | ||||
|                     "failed while fetching payout_attempt for payout_id - {:?}", | ||||
|                     payout.payout_id | ||||
|                 ); | ||||
|                 logger::warn!(?err, err_msg); | ||||
|                 Err(err | ||||
|                     .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|                     .attach_printable(err_msg)) | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     let pi_pa_tuple_vec: Result< | ||||
|         Vec<( | ||||
|             storage::Payouts, | ||||
|             storage::PayoutAttempt, | ||||
|             Option<domain::Customer>, | ||||
|         )>, | ||||
|         _, | ||||
|     > = join_all(collected_futures) | ||||
|         .await | ||||
|         .into_iter() | ||||
|         .collect::<Result< | ||||
|             Vec<( | ||||
|                 storage::Payouts, | ||||
|                 storage::PayoutAttempt, | ||||
|                 Option<domain::Customer>, | ||||
|             )>, | ||||
|             _, | ||||
|         >>(); | ||||
|     } | ||||
|  | ||||
|     let data: Vec<api::PayoutCreateResponse> = pi_pa_tuple_vec | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError)? | ||||
|         .into_iter() | ||||
|         .map(ForeignFrom::foreign_from) | ||||
|         .collect(); | ||||
| @ -874,6 +887,7 @@ pub async fn payouts_filtered_list_core( | ||||
|         storage::Payouts, | ||||
|         storage::PayoutAttempt, | ||||
|         Option<diesel_models::Customer>, | ||||
|         Option<diesel_models::Address>, | ||||
|     )> = db | ||||
|         .filter_payouts_and_attempts( | ||||
|             merchant_account.get_id(), | ||||
| @ -883,30 +897,50 @@ pub async fn payouts_filtered_list_core( | ||||
|         .await | ||||
|         .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; | ||||
|     let list = core_utils::filter_objects_based_on_profile_id_list(profile_id_list, list); | ||||
|     let data: Vec<api::PayoutCreateResponse> = join_all(list.into_iter().map(|(p, pa, c)| async { | ||||
|         let domain_cust = c | ||||
|             .async_and_then(|cust| async { | ||||
|                 domain::Customer::convert_back( | ||||
|                     &(&state).into(), | ||||
|                     cust, | ||||
|                     &key_store.key, | ||||
|                     key_store.merchant_id.clone().into(), | ||||
|                 ) | ||||
|                 .await | ||||
|                 .map_err(|err| { | ||||
|                     let msg = format!("failed to convert customer for id: {:?}", p.customer_id); | ||||
|                     logger::warn!(?err, msg); | ||||
|     let data: Vec<api::PayoutCreateResponse> = | ||||
|         join_all(list.into_iter().map(|(p, pa, customer, address)| async { | ||||
|             let customer: Option<domain::Customer> = customer | ||||
|                 .async_and_then(|cust| async { | ||||
|                     domain::Customer::convert_back( | ||||
|                         &(&state).into(), | ||||
|                         cust, | ||||
|                         &key_store.key, | ||||
|                         key_store.merchant_id.clone().into(), | ||||
|                     ) | ||||
|                     .await | ||||
|                     .map_err(|err| { | ||||
|                         let msg = format!("failed to convert customer for id: {:?}", p.customer_id); | ||||
|                         logger::warn!(?err, msg); | ||||
|                     }) | ||||
|                     .ok() | ||||
|                 }) | ||||
|                 .ok() | ||||
|             }) | ||||
|             .await; | ||||
|         Some((p, pa, domain_cust)) | ||||
|     })) | ||||
|     .await | ||||
|     .into_iter() | ||||
|     .flatten() | ||||
|     .map(ForeignFrom::foreign_from) | ||||
|     .collect(); | ||||
|                 .await; | ||||
|  | ||||
|             let payout_addr: Option<payment_enums::Address> = address | ||||
|                 .async_and_then(|addr| async { | ||||
|                     domain::Address::convert_back( | ||||
|                         &(&state).into(), | ||||
|                         addr, | ||||
|                         &key_store.key, | ||||
|                         key_store.merchant_id.clone().into(), | ||||
|                     ) | ||||
|                     .await | ||||
|                     .map(ForeignFrom::foreign_from) | ||||
|                     .map_err(|err| { | ||||
|                         let msg = format!("failed to convert address for id: {:?}", p.address_id); | ||||
|                         logger::warn!(?err, msg); | ||||
|                     }) | ||||
|                     .ok() | ||||
|                 }) | ||||
|                 .await; | ||||
|  | ||||
|             Some((p, pa, customer, payout_addr)) | ||||
|         })) | ||||
|         .await | ||||
|         .into_iter() | ||||
|         .flatten() | ||||
|         .map(ForeignFrom::foreign_from) | ||||
|         .collect(); | ||||
|  | ||||
|     let active_payout_ids = db | ||||
|         .filter_active_payout_ids_by_constraints(merchant_account.get_id(), &constraints) | ||||
|  | ||||
| @ -9,7 +9,7 @@ use common_utils::link_utils::EnabledPaymentMethod; | ||||
| ))] | ||||
| use crate::types::transformers::ForeignInto; | ||||
| #[cfg(feature = "olap")] | ||||
| use crate::types::{domain, storage}; | ||||
| use crate::types::{api::payments, domain, storage}; | ||||
| use crate::{ | ||||
|     settings::PayoutRequiredFields, | ||||
|     types::{api, transformers::ForeignFrom}, | ||||
| @ -21,6 +21,7 @@ impl | ||||
|         storage::Payouts, | ||||
|         storage::PayoutAttempt, | ||||
|         Option<domain::Customer>, | ||||
|         Option<payments::Address>, | ||||
|     )> for api::PayoutCreateResponse | ||||
| { | ||||
|     fn foreign_from( | ||||
| @ -28,6 +29,7 @@ impl | ||||
|             storage::Payouts, | ||||
|             storage::PayoutAttempt, | ||||
|             Option<domain::Customer>, | ||||
|             Option<payments::Address>, | ||||
|         ), | ||||
|     ) -> Self { | ||||
|         todo!() | ||||
| @ -44,6 +46,7 @@ impl | ||||
|         storage::Payouts, | ||||
|         storage::PayoutAttempt, | ||||
|         Option<domain::Customer>, | ||||
|         Option<payments::Address>, | ||||
|     )> for api::PayoutCreateResponse | ||||
| { | ||||
|     fn foreign_from( | ||||
| @ -51,9 +54,10 @@ impl | ||||
|             storage::Payouts, | ||||
|             storage::PayoutAttempt, | ||||
|             Option<domain::Customer>, | ||||
|             Option<payments::Address>, | ||||
|         ), | ||||
|     ) -> Self { | ||||
|         let (payout, payout_attempt, customer) = item; | ||||
|         let (payout, payout_attempt, customer, address) = item; | ||||
|         let attempt = api::PayoutAttemptResponse { | ||||
|             attempt_id: payout_attempt.payout_attempt_id, | ||||
|             status: payout_attempt.status, | ||||
| @ -95,7 +99,7 @@ impl | ||||
|             connector_transaction_id: attempt.connector_transaction_id.clone(), | ||||
|             priority: payout.priority, | ||||
|             attempts: Some(vec![attempt]), | ||||
|             billing: None, | ||||
|             billing: address, | ||||
|             client_secret: None, | ||||
|             payout_link: None, | ||||
|             email: customer | ||||
|  | ||||
| @ -1521,7 +1521,7 @@ impl GetProfileId for storage::Payouts { | ||||
|     } | ||||
| } | ||||
| #[cfg(feature = "payouts")] | ||||
| impl<T, F> GetProfileId for (storage::Payouts, T, F) { | ||||
| impl<T, F, R> GetProfileId for (storage::Payouts, T, F, R) { | ||||
|     fn get_profile_id(&self) -> Option<&common_utils::id_type::ProfileId> { | ||||
|         self.0.get_profile_id() | ||||
|     } | ||||
|  | ||||
| @ -2073,6 +2073,7 @@ impl PayoutsInterface for KafkaStore { | ||||
|             storage::Payouts, | ||||
|             storage::PayoutAttempt, | ||||
|             Option<diesel_models::Customer>, | ||||
|             Option<diesel_models::Address>, | ||||
|         )>, | ||||
|         errors::DataStorageError, | ||||
|     > { | ||||
|  | ||||
| @ -207,6 +207,14 @@ pub type PayoutsRouterData<F> = RouterData<F, PayoutsData, PayoutsResponseData>; | ||||
| pub type PayoutsResponseRouterData<F, R> = | ||||
|     ResponseRouterData<F, R, PayoutsData, PayoutsResponseData>; | ||||
|  | ||||
| #[cfg(feature = "payouts")] | ||||
| pub type PayoutActionData = Vec<( | ||||
|     storage::Payouts, | ||||
|     storage::PayoutAttempt, | ||||
|     Option<domain::Customer>, | ||||
|     Option<api_models::payments::Address>, | ||||
| )>; | ||||
|  | ||||
| #[cfg(feature = "payouts")] | ||||
| pub trait PayoutIndividualDetailsExt { | ||||
|     type Error; | ||||
|  | ||||
| @ -789,6 +789,52 @@ impl<'a> From<&'a domain::Address> for api_types::Address { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ForeignFrom<domain::Address> for api_types::Address { | ||||
|     fn foreign_from(address: domain::Address) -> Self { | ||||
|         // If all the fields of address are none, then pass the address as None | ||||
|         let address_details = if address.city.is_none() | ||||
|             && address.line1.is_none() | ||||
|             && address.line2.is_none() | ||||
|             && address.line3.is_none() | ||||
|             && address.state.is_none() | ||||
|             && address.country.is_none() | ||||
|             && address.zip.is_none() | ||||
|             && address.first_name.is_none() | ||||
|             && address.last_name.is_none() | ||||
|         { | ||||
|             None | ||||
|         } else { | ||||
|             Some(api_types::AddressDetails { | ||||
|                 city: address.city.clone(), | ||||
|                 country: address.country, | ||||
|                 line1: address.line1.clone().map(Encryptable::into_inner), | ||||
|                 line2: address.line2.clone().map(Encryptable::into_inner), | ||||
|                 line3: address.line3.clone().map(Encryptable::into_inner), | ||||
|                 state: address.state.clone().map(Encryptable::into_inner), | ||||
|                 zip: address.zip.clone().map(Encryptable::into_inner), | ||||
|                 first_name: address.first_name.clone().map(Encryptable::into_inner), | ||||
|                 last_name: address.last_name.clone().map(Encryptable::into_inner), | ||||
|             }) | ||||
|         }; | ||||
|  | ||||
|         // If all the fields of phone are none, then pass the phone as None | ||||
|         let phone_details = if address.phone_number.is_none() && address.country_code.is_none() { | ||||
|             None | ||||
|         } else { | ||||
|             Some(api_types::PhoneDetails { | ||||
|                 number: address.phone_number.clone().map(Encryptable::into_inner), | ||||
|                 country_code: address.country_code.clone(), | ||||
|             }) | ||||
|         }; | ||||
|  | ||||
|         Self { | ||||
|             address: address_details, | ||||
|             phone: phone_details, | ||||
|             email: address.email.clone().map(pii::Email::from), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl | ||||
|     ForeignFrom<( | ||||
|         diesel_models::api_keys::ApiKey, | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Sakil Mostak
					Sakil Mostak