Skip to content

Commit

Permalink
feat(core): [Payouts] Add billing address to payout list (#5004)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
Sakilmostak and hyperswitch-bot[bot] authored Sep 10, 2024
1 parent 71b5202 commit 49a60bf
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 91 deletions.
7 changes: 6 additions & 1 deletion crates/hyperswitch_domain_models/src/payouts/payouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ pub trait PayoutsInterface {
_filters: &PayoutFetchConstraints,
_storage_scheme: MerchantStorageScheme,
) -> error_stack::Result<
Vec<(Payouts, PayoutAttempt, Option<diesel_models::Customer>)>,
Vec<(
Payouts,
PayoutAttempt,
Option<diesel_models::Customer>,
Option<diesel_models::Address>,
)>,
errors::StorageError,
>;

Expand Down
172 changes: 103 additions & 69 deletions crates/router/src/core/payouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::{
Expand Down Expand Up @@ -770,81 +774,90 @@ 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,
&utils::get_payout_attempt_id(payout.payout_id.clone(), payout.attempt_count),
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();
Expand Down Expand Up @@ -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(),
Expand All @@ -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)
Expand Down
10 changes: 7 additions & 3 deletions crates/router/src/core/payouts/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -21,13 +21,15 @@ impl
storage::Payouts,
storage::PayoutAttempt,
Option<domain::Customer>,
Option<payments::Address>,
)> for api::PayoutCreateResponse
{
fn foreign_from(
item: (
storage::Payouts,
storage::PayoutAttempt,
Option<domain::Customer>,
Option<payments::Address>,
),
) -> Self {
todo!()
Expand All @@ -44,16 +46,18 @@ impl
storage::Payouts,
storage::PayoutAttempt,
Option<domain::Customer>,
Option<payments::Address>,
)> for api::PayoutCreateResponse
{
fn foreign_from(
item: (
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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/core/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/db/kafka_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2073,6 +2073,7 @@ impl PayoutsInterface for KafkaStore {
storage::Payouts,
storage::PayoutAttempt,
Option<diesel_models::Customer>,
Option<diesel_models::Address>,
)>,
errors::DataStorageError,
> {
Expand Down
8 changes: 8 additions & 0 deletions crates/router/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 46 additions & 0 deletions crates/router/src/types/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 49a60bf

Please sign in to comment.