mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-03 05:17:02 +08:00
refactor(payouts): openAPI schemas and mintlify docs (#5284)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Co-authored-by: Vrishab Srivatsa <136090360+vsrivatsa-juspay@users.noreply.github.com> Co-authored-by: Prajjwal Kumar <prajjwal.kumar@juspay.in>
This commit is contained in:
@ -1,16 +1,14 @@
|
||||
use std::{fmt::Debug, marker::PhantomData, str::FromStr};
|
||||
|
||||
use api_models::payments::{
|
||||
Address, CustomerDetailsResponse, FrmMessage, PaymentChargeRequest, PaymentChargeResponse,
|
||||
RequestSurchargeDetails,
|
||||
Address, CustomerDetails, CustomerDetailsResponse, FrmMessage, PaymentChargeRequest,
|
||||
PaymentChargeResponse, RequestSurchargeDetails,
|
||||
};
|
||||
#[cfg(feature = "payouts")]
|
||||
use api_models::payouts::PayoutAttemptResponse;
|
||||
use common_enums::RequestIncrementalAuthorization;
|
||||
use common_utils::{consts::X_HS_LATENCY, fp_utils, pii::Email, types::MinorUnit};
|
||||
use diesel_models::ephemeral_key;
|
||||
use error_stack::{report, ResultExt};
|
||||
use hyperswitch_domain_models::payments::payment_intent::CustomerData;
|
||||
use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types};
|
||||
use masking::{ExposeInterface, Maskable, PeekInterface, Secret};
|
||||
use router_env::{instrument, metrics::add_attributes, tracing};
|
||||
|
||||
@ -1094,62 +1092,6 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "payouts")]
|
||||
impl ForeignFrom<(storage::Payouts, storage::PayoutAttempt, domain::Customer)>
|
||||
for api::PayoutCreateResponse
|
||||
{
|
||||
fn foreign_from(item: (storage::Payouts, storage::PayoutAttempt, domain::Customer)) -> Self {
|
||||
let (payout, payout_attempt, customer) = item;
|
||||
let attempt = PayoutAttemptResponse {
|
||||
attempt_id: payout_attempt.payout_attempt_id,
|
||||
status: payout_attempt.status,
|
||||
amount: payout.amount,
|
||||
currency: Some(payout.destination_currency),
|
||||
connector: payout_attempt.connector.clone(),
|
||||
error_code: payout_attempt.error_code.clone(),
|
||||
error_message: payout_attempt.error_message.clone(),
|
||||
payment_method: payout.payout_type,
|
||||
payout_method_type: None,
|
||||
connector_transaction_id: payout_attempt.connector_payout_id,
|
||||
cancellation_reason: None,
|
||||
unified_code: None,
|
||||
unified_message: None,
|
||||
};
|
||||
Self {
|
||||
payout_id: payout.payout_id,
|
||||
merchant_id: payout.merchant_id,
|
||||
amount: payout.amount,
|
||||
currency: payout.destination_currency,
|
||||
connector: payout_attempt.connector,
|
||||
payout_type: payout.payout_type,
|
||||
customer_id: customer.get_customer_id(),
|
||||
auto_fulfill: payout.auto_fulfill,
|
||||
email: customer.email,
|
||||
name: customer.name,
|
||||
phone: customer.phone,
|
||||
phone_country_code: customer.phone_country_code,
|
||||
return_url: payout.return_url,
|
||||
business_country: payout_attempt.business_country,
|
||||
business_label: payout_attempt.business_label,
|
||||
description: payout.description,
|
||||
entity_type: payout.entity_type,
|
||||
recurring: payout.recurring,
|
||||
metadata: payout.metadata,
|
||||
status: payout_attempt.status,
|
||||
error_message: payout_attempt.error_message,
|
||||
error_code: payout_attempt.error_code,
|
||||
profile_id: payout.profile_id,
|
||||
created: Some(payout.created_at),
|
||||
connector_transaction_id: attempt.connector_transaction_id.clone(),
|
||||
priority: payout.priority,
|
||||
attempts: Some(vec![attempt]),
|
||||
billing: None,
|
||||
client_secret: None,
|
||||
payout_link: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<ephemeral_key::EphemeralKey> for api::ephemeral_key::EphemeralKeyCreateResponse {
|
||||
fn foreign_from(from: ephemeral_key::EphemeralKey) -> Self {
|
||||
Self {
|
||||
@ -1962,3 +1904,15 @@ impl ForeignFrom<payments::FraudCheck> for FrmMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<CustomerDetails> for router_request_types::CustomerDetails {
|
||||
fn foreign_from(customer: CustomerDetails) -> Self {
|
||||
Self {
|
||||
customer_id: Some(customer.id),
|
||||
name: customer.name,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
phone_country_code: customer.phone_country_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ use std::{
|
||||
use actix_web::http::header;
|
||||
use api_models::payouts;
|
||||
use common_utils::{
|
||||
ext_traits::{Encode, OptionExt},
|
||||
ext_traits::{AsyncExt, Encode, OptionExt},
|
||||
link_utils,
|
||||
types::{AmountConvertor, StringMajorUnitForConnector},
|
||||
};
|
||||
@ -284,14 +284,20 @@ pub async fn filter_payout_methods(
|
||||
Some(&payout.profile_id),
|
||||
common_enums::ConnectorType::PayoutProcessor,
|
||||
);
|
||||
let address = db
|
||||
.find_address_by_address_id(key_manager_state, &payout.address_id.clone(), key_store)
|
||||
let address = payout
|
||||
.address_id
|
||||
.as_ref()
|
||||
.async_map(|address_id| async {
|
||||
db.find_address_by_address_id(key_manager_state, address_id, key_store)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.transpose()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable_lazy(|| {
|
||||
format!(
|
||||
"Failed while fetching address with address id {}",
|
||||
payout.address_id.clone()
|
||||
"Failed while fetching address [id - {:?}] for payout [id - {}]",
|
||||
payout.address_id, payout.payout_id
|
||||
)
|
||||
})?;
|
||||
|
||||
@ -326,7 +332,10 @@ pub async fn filter_payout_methods(
|
||||
payout_filter,
|
||||
request_payout_method_type,
|
||||
&payout.destination_currency,
|
||||
&address.country,
|
||||
address
|
||||
.as_ref()
|
||||
.and_then(|address| address.country)
|
||||
.as_ref(),
|
||||
)?;
|
||||
if currency_country_filter.unwrap_or(true) {
|
||||
match payment_method {
|
||||
@ -381,7 +390,7 @@ pub fn check_currency_country_filters(
|
||||
payout_method_filter: Option<&PaymentMethodFilters>,
|
||||
request_payout_method_type: &api_models::payment_methods::RequestPaymentMethodTypes,
|
||||
currency: &common_enums::Currency,
|
||||
country: &Option<common_enums::CountryAlpha2>,
|
||||
country: Option<&common_enums::CountryAlpha2>,
|
||||
) -> errors::RouterResult<Option<bool>> {
|
||||
if matches!(
|
||||
request_payout_method_type.payment_method_type,
|
||||
|
||||
@ -2,6 +2,7 @@ pub mod access_token;
|
||||
pub mod helpers;
|
||||
#[cfg(feature = "payout_retry")]
|
||||
pub mod retry;
|
||||
pub mod transformers;
|
||||
pub mod validator;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
@ -24,8 +25,6 @@ use diesel_models::{
|
||||
use error_stack::{report, ResultExt};
|
||||
#[cfg(feature = "olap")]
|
||||
use futures::future::join_all;
|
||||
#[cfg(feature = "olap")]
|
||||
use hyperswitch_domain_models::errors::StorageError;
|
||||
use masking::{PeekInterface, Secret};
|
||||
#[cfg(feature = "payout_retry")]
|
||||
use retry::GsmValidation;
|
||||
@ -49,7 +48,7 @@ use crate::{
|
||||
services,
|
||||
types::{
|
||||
self,
|
||||
api::{self, payouts},
|
||||
api::{self, payments as payment_api_types, payouts},
|
||||
domain,
|
||||
storage::{self, PaymentRoutingInfo},
|
||||
transformers::ForeignFrom,
|
||||
@ -90,7 +89,7 @@ pub async fn get_connector_choice(
|
||||
connector: Option<String>,
|
||||
routing_algorithm: Option<serde_json::Value>,
|
||||
payout_data: &mut PayoutData,
|
||||
eligible_connectors: Option<Vec<api_models::enums::PayoutConnectors>>,
|
||||
eligible_connectors: Option<Vec<api_enums::PayoutConnectors>>,
|
||||
) -> RouterResult<api::ConnectorCallType> {
|
||||
let eligible_routable_connectors = eligible_connectors.map(|connectors| {
|
||||
connectors
|
||||
@ -255,7 +254,9 @@ pub async fn make_connector_decision(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError)?,
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError).attach_printable({
|
||||
"only PreDetermined and Retryable ConnectorCallTypes are supported".to_string()
|
||||
})?,
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,7 +267,7 @@ pub async fn payouts_core(
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
payout_data: &mut PayoutData,
|
||||
routing_algorithm: Option<serde_json::Value>,
|
||||
eligible_connectors: Option<Vec<api_models::enums::PayoutConnectors>>,
|
||||
eligible_connectors: Option<Vec<api_enums::PayoutConnectors>>,
|
||||
) -> RouterResult<()> {
|
||||
let payout_attempt = &payout_data.payout_attempt;
|
||||
|
||||
@ -301,7 +302,7 @@ pub async fn payouts_create_core(
|
||||
req: payouts::PayoutCreateRequest,
|
||||
) -> RouterResponse<payouts::PayoutCreateResponse> {
|
||||
// Validate create request
|
||||
let (payout_id, payout_method_data, profile_id) =
|
||||
let (payout_id, payout_method_data, profile_id, customer) =
|
||||
validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?;
|
||||
|
||||
// Create DB entries
|
||||
@ -313,6 +314,7 @@ pub async fn payouts_create_core(
|
||||
&payout_id,
|
||||
&profile_id,
|
||||
payout_method_data.as_ref(),
|
||||
customer.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -320,18 +322,25 @@ pub async fn payouts_create_core(
|
||||
let payout_type = payout_data.payouts.payout_type.to_owned();
|
||||
|
||||
// Persist payout method data in temp locker
|
||||
payout_data.payout_method_data = helpers::make_payout_method_data(
|
||||
&state,
|
||||
req.payout_method_data.as_ref(),
|
||||
payout_attempt.payout_token.as_deref(),
|
||||
&payout_attempt.customer_id,
|
||||
&payout_attempt.merchant_id,
|
||||
payout_type,
|
||||
&key_store,
|
||||
Some(&mut payout_data),
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await?;
|
||||
if req.payout_method_data.is_some() {
|
||||
let customer_id = payout_data
|
||||
.payouts
|
||||
.customer_id
|
||||
.clone()
|
||||
.get_required_value("customer_id when payout_method_data is provided")?;
|
||||
payout_data.payout_method_data = helpers::make_payout_method_data(
|
||||
&state,
|
||||
req.payout_method_data.as_ref(),
|
||||
payout_attempt.payout_token.as_deref(),
|
||||
&customer_id,
|
||||
&payout_attempt.merchant_id,
|
||||
payout_type,
|
||||
&key_store,
|
||||
Some(&mut payout_data),
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(true) = payout_data.payouts.confirm {
|
||||
payouts_core(
|
||||
@ -380,8 +389,14 @@ pub async fn payouts_confirm_core(
|
||||
"confirm",
|
||||
)?;
|
||||
|
||||
helpers::update_payouts_and_payout_attempt(&mut payout_data, &merchant_account, &req, &state)
|
||||
.await?;
|
||||
helpers::update_payouts_and_payout_attempt(
|
||||
&mut payout_data,
|
||||
&merchant_account,
|
||||
&req,
|
||||
&state,
|
||||
&key_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let db = &*state.store;
|
||||
|
||||
@ -441,8 +456,14 @@ pub async fn payouts_update_core(
|
||||
),
|
||||
}));
|
||||
}
|
||||
helpers::update_payouts_and_payout_attempt(&mut payout_data, &merchant_account, &req, &state)
|
||||
.await?;
|
||||
helpers::update_payouts_and_payout_attempt(
|
||||
&mut payout_data,
|
||||
&merchant_account,
|
||||
&req,
|
||||
&state,
|
||||
&key_store,
|
||||
)
|
||||
.await?;
|
||||
let payout_attempt = payout_data.payout_attempt.to_owned();
|
||||
|
||||
if (req.connector.is_none(), payout_attempt.connector.is_some()) != (true, true) {
|
||||
@ -452,18 +473,25 @@ pub async fn payouts_update_core(
|
||||
};
|
||||
|
||||
// Update payout method data in temp locker
|
||||
payout_data.payout_method_data = helpers::make_payout_method_data(
|
||||
&state,
|
||||
req.payout_method_data.as_ref(),
|
||||
payout_attempt.payout_token.as_deref(),
|
||||
&payout_attempt.customer_id,
|
||||
&payout_attempt.merchant_id,
|
||||
payout_data.payouts.payout_type,
|
||||
&key_store,
|
||||
Some(&mut payout_data),
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await?;
|
||||
if req.payout_method_data.is_some() {
|
||||
let customer_id = payout_data
|
||||
.payouts
|
||||
.customer_id
|
||||
.clone()
|
||||
.get_required_value("customer_id when payout_method_data is provided")?;
|
||||
payout_data.payout_method_data = helpers::make_payout_method_data(
|
||||
&state,
|
||||
req.payout_method_data.as_ref(),
|
||||
payout_attempt.payout_token.as_deref(),
|
||||
&customer_id,
|
||||
&payout_attempt.merchant_id,
|
||||
payout_data.payouts.payout_type,
|
||||
&key_store,
|
||||
Some(&mut payout_data),
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(true) = payout_data.payouts.confirm {
|
||||
payouts_core(
|
||||
@ -672,12 +700,17 @@ pub async fn payouts_fulfill_core(
|
||||
};
|
||||
|
||||
// Trigger fulfillment
|
||||
let customer_id = payout_data
|
||||
.payouts
|
||||
.customer_id
|
||||
.clone()
|
||||
.get_required_value("customer_id")?;
|
||||
payout_data.payout_method_data = Some(
|
||||
helpers::make_payout_method_data(
|
||||
&state,
|
||||
None,
|
||||
payout_attempt.payout_token.as_deref(),
|
||||
&payout_attempt.customer_id,
|
||||
&customer_id,
|
||||
&payout_attempt.merchant_id,
|
||||
payout_data.payouts.payout_type,
|
||||
&key_store,
|
||||
@ -729,70 +762,77 @@ 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(|payouts| async {
|
||||
let collected_futures = payouts.into_iter().map(|payout| async {
|
||||
match db
|
||||
.find_payout_attempt_by_merchant_id_payout_attempt_id(
|
||||
merchant_id,
|
||||
&utils::get_payment_attempt_id(payouts.payout_id.clone(), payouts.attempt_count),
|
||||
&utils::get_payment_attempt_id(payout.payout_id.clone(), payout.attempt_count),
|
||||
storage_enums::MerchantStorageScheme::PostgresOnly,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(payout_attempt) => {
|
||||
match db
|
||||
.find_customer_by_customer_id_merchant_id(
|
||||
&(&state).into(),
|
||||
&payouts.customer_id,
|
||||
merchant_id,
|
||||
&key_store,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(customer) => Some(Ok((payouts, payout_attempt, customer))),
|
||||
Err(error) => {
|
||||
if matches!(
|
||||
error.current_context(),
|
||||
storage_impl::errors::StorageError::ValueNotFound(_)
|
||||
) {
|
||||
logger::warn!(
|
||||
?error,
|
||||
"customer missing for customer_id : {:?}",
|
||||
payouts.customer_id,
|
||||
Ok(ref payout_attempt) => match payout.customer_id.clone() {
|
||||
Some(ref customer_id) => {
|
||||
match db
|
||||
.find_customer_by_customer_id_merchant_id(
|
||||
&(&state).into(),
|
||||
customer_id,
|
||||
merchant_id,
|
||||
&key_store,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(customer) => Ok((payout, payout_attempt.to_owned(), Some(customer))),
|
||||
Err(err) => {
|
||||
let err_msg = format!(
|
||||
"failed while fetching customer for customer_id - {:?}",
|
||||
customer_id
|
||||
);
|
||||
return None;
|
||||
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))
|
||||
}
|
||||
}
|
||||
Some(Err(error.change_context(StorageError::ValueNotFound(
|
||||
format!(
|
||||
"customer missing for customer_id : {:?}",
|
||||
payouts.customer_id
|
||||
),
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
if matches!(error.current_context(), StorageError::ValueNotFound(_)) {
|
||||
logger::warn!(
|
||||
?error,
|
||||
"payout_attempt missing for payout_id : {}",
|
||||
payouts.payout_id,
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(Err(error))
|
||||
None => Ok((payout.to_owned(), payout_attempt.to_owned(), None)),
|
||||
},
|
||||
Err(err) => {
|
||||
let err_msg = format!(
|
||||
"failed while fetching payout_attempt for payout_id - {}",
|
||||
payout.payout_id.clone(),
|
||||
);
|
||||
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, domain::Customer)>,
|
||||
Vec<(
|
||||
storage::Payouts,
|
||||
storage::PayoutAttempt,
|
||||
Option<domain::Customer>,
|
||||
)>,
|
||||
_,
|
||||
> = join_all(collected_futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Result<Vec<(storage::Payouts, storage::PayoutAttempt, domain::Customer)>, _>>();
|
||||
.collect::<Result<
|
||||
Vec<(
|
||||
storage::Payouts,
|
||||
storage::PayoutAttempt,
|
||||
Option<domain::Customer>,
|
||||
)>,
|
||||
_,
|
||||
>>();
|
||||
|
||||
let data: Vec<api::PayoutCreateResponse> = pi_pa_tuple_vec
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?
|
||||
@ -822,7 +862,7 @@ pub async fn payouts_filtered_list_core(
|
||||
let list: Vec<(
|
||||
storage::Payouts,
|
||||
storage::PayoutAttempt,
|
||||
diesel_models::Customer,
|
||||
Option<diesel_models::Customer>,
|
||||
)> = db
|
||||
.filter_payouts_and_attempts(
|
||||
merchant_account.get_id(),
|
||||
@ -833,24 +873,23 @@ pub async fn payouts_filtered_list_core(
|
||||
.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 {
|
||||
match domain::Customer::convert_back(
|
||||
&(&state).into(),
|
||||
c,
|
||||
&key_store.key,
|
||||
key_store.merchant_id.clone().into(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(domain_cust) => Some((p, pa, domain_cust)),
|
||||
Err(err) => {
|
||||
logger::warn!(
|
||||
?err,
|
||||
"failed to convert customer for id: {:?}",
|
||||
p.customer_id
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
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);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.await;
|
||||
Some((p, pa, domain_cust))
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
@ -936,12 +975,16 @@ pub async fn call_connector_payout(
|
||||
|
||||
// Fetch / store payout_method_data
|
||||
if payout_data.payout_method_data.is_none() || payout_attempt.payout_token.is_none() {
|
||||
let customer_id = payouts
|
||||
.customer_id
|
||||
.clone()
|
||||
.get_required_value("customer_id")?;
|
||||
payout_data.payout_method_data = Some(
|
||||
helpers::make_payout_method_data(
|
||||
state,
|
||||
payout_data.payout_method_data.to_owned().as_ref(),
|
||||
payout_attempt.payout_token.as_deref(),
|
||||
&payout_attempt.customer_id,
|
||||
&customer_id,
|
||||
&payout_attempt.merchant_id,
|
||||
payouts.payout_type,
|
||||
key_store,
|
||||
@ -1975,22 +2018,6 @@ pub async fn fulfill_payout(
|
||||
.status
|
||||
.unwrap_or(payout_data.payout_attempt.status.to_owned());
|
||||
payout_data.payouts.status = status;
|
||||
if payout_data.payouts.recurring
|
||||
&& payout_data.payouts.payout_method_id.clone().is_none()
|
||||
&& !helpers::is_payout_err_state(status)
|
||||
{
|
||||
helpers::save_payout_data_to_locker(
|
||||
state,
|
||||
payout_data,
|
||||
&payout_data
|
||||
.payout_method_data
|
||||
.clone()
|
||||
.get_required_value("payout_method_data")?,
|
||||
merchant_account,
|
||||
key_store,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate {
|
||||
connector_payout_id: payout_response_data.connector_payout_id,
|
||||
status,
|
||||
@ -2024,6 +2051,31 @@ pub async fn fulfill_payout(
|
||||
serde_json::json!({"payout_status": status.to_string(), "error_message": payout_data.payout_attempt.error_message.as_ref(), "error_code": payout_data.payout_attempt.error_code.as_ref()})
|
||||
),
|
||||
}));
|
||||
} else if payout_data.payouts.recurring
|
||||
&& payout_data.payouts.payout_method_id.clone().is_none()
|
||||
{
|
||||
let payout_method_data = payout_data
|
||||
.payout_method_data
|
||||
.clone()
|
||||
.get_required_value("payout_method_data")?;
|
||||
payout_data
|
||||
.payouts
|
||||
.customer_id
|
||||
.clone()
|
||||
.async_map(|customer_id| async move {
|
||||
helpers::save_payout_data_to_locker(
|
||||
state,
|
||||
payout_data,
|
||||
&customer_id,
|
||||
&payout_method_data,
|
||||
merchant_account,
|
||||
key_store,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.transpose()
|
||||
.attach_printable("Failed to save payout data to locker")?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@ -2072,17 +2124,12 @@ pub async fn response_handler(
|
||||
let customer_details = payout_data.customer_details.to_owned();
|
||||
let customer_id = payouts.customer_id;
|
||||
|
||||
let (email, name, phone, phone_country_code) = customer_details
|
||||
.map_or((None, None, None, None), |c| {
|
||||
(c.email, c.name, c.phone, c.phone_country_code)
|
||||
});
|
||||
|
||||
let address = billing_address.as_ref().map(|a| {
|
||||
let phone_details = api_models::payments::PhoneDetails {
|
||||
let phone_details = payment_api_types::PhoneDetails {
|
||||
number: a.phone_number.to_owned().map(Encryptable::into_inner),
|
||||
country_code: a.country_code.to_owned(),
|
||||
};
|
||||
let address_details = api_models::payments::AddressDetails {
|
||||
let address_details = payment_api_types::AddressDetails {
|
||||
city: a.city.to_owned(),
|
||||
country: a.country.to_owned(),
|
||||
line1: a.line1.to_owned().map(Encryptable::into_inner),
|
||||
@ -2108,12 +2155,17 @@ pub async fn response_handler(
|
||||
connector: payout_attempt.connector.to_owned(),
|
||||
payout_type: payouts.payout_type.to_owned(),
|
||||
billing: address,
|
||||
customer_id,
|
||||
auto_fulfill: payouts.auto_fulfill,
|
||||
email,
|
||||
name,
|
||||
phone,
|
||||
phone_country_code,
|
||||
customer_id,
|
||||
email: customer_details.as_ref().and_then(|c| c.email.clone()),
|
||||
name: customer_details.as_ref().and_then(|c| c.name.clone()),
|
||||
phone: customer_details.as_ref().and_then(|c| c.phone.clone()),
|
||||
phone_country_code: customer_details
|
||||
.as_ref()
|
||||
.and_then(|c| c.phone_country_code.clone()),
|
||||
customer: customer_details
|
||||
.as_ref()
|
||||
.map(payment_api_types::CustomerDetailsResponse::foreign_from),
|
||||
client_secret: payouts.client_secret.to_owned(),
|
||||
return_url: payouts.return_url.to_owned(),
|
||||
business_country: payout_attempt.business_country,
|
||||
@ -2154,33 +2206,11 @@ pub async fn payout_create_db_entries(
|
||||
payout_id: &String,
|
||||
profile_id: &String,
|
||||
stored_payout_method_data: Option<&payouts::PayoutMethodData>,
|
||||
customer: Option<&domain::Customer>,
|
||||
) -> RouterResult<PayoutData> {
|
||||
let db = &*state.store;
|
||||
let merchant_id = merchant_account.get_id();
|
||||
|
||||
// Get or create customer
|
||||
let customer_details = payments::CustomerDetails {
|
||||
customer_id: req.customer_id.to_owned(),
|
||||
name: req.name.to_owned(),
|
||||
email: req.email.to_owned(),
|
||||
phone: req.phone.to_owned(),
|
||||
phone_country_code: req.phone_country_code.to_owned(),
|
||||
};
|
||||
let customer = helpers::get_or_create_customer_details(
|
||||
state,
|
||||
&customer_details,
|
||||
merchant_account,
|
||||
key_store,
|
||||
)
|
||||
.await?;
|
||||
let customer_id = customer
|
||||
.to_owned()
|
||||
.ok_or_else(|| {
|
||||
report!(errors::ApiErrorResponse::MissingRequiredField {
|
||||
field_name: "customer_id",
|
||||
})
|
||||
})?
|
||||
.get_customer_id();
|
||||
let customer_id = customer.map(|cust| cust.get_customer_id());
|
||||
|
||||
// Validate whether profile_id passed in request is valid and is linked to the merchant
|
||||
let business_profile =
|
||||
@ -2191,8 +2221,10 @@ pub async fn payout_create_db_entries(
|
||||
create_payout_link(
|
||||
state,
|
||||
&business_profile,
|
||||
&customer_id,
|
||||
merchant_account.get_id(),
|
||||
&customer_id
|
||||
.clone()
|
||||
.get_required_value("customer.id when payout_link is true")?,
|
||||
merchant_id,
|
||||
req,
|
||||
payout_id,
|
||||
)
|
||||
@ -2207,20 +2239,13 @@ pub async fn payout_create_db_entries(
|
||||
req.billing.as_ref(),
|
||||
None,
|
||||
merchant_id,
|
||||
Some(&customer_id.to_owned()),
|
||||
customer_id.as_ref(),
|
||||
key_store,
|
||||
payout_id,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await?;
|
||||
let address_id = billing_address
|
||||
.to_owned()
|
||||
.ok_or_else(|| {
|
||||
report!(errors::ApiErrorResponse::MissingRequiredField {
|
||||
field_name: "billing.address",
|
||||
})
|
||||
})?
|
||||
.address_id;
|
||||
let address_id = billing_address.to_owned().map(|address| address.address_id);
|
||||
|
||||
// Make payouts entry
|
||||
let currency = req.currency.to_owned().get_required_value("currency")?;
|
||||
@ -2251,7 +2276,7 @@ pub async fn payout_create_db_entries(
|
||||
let payouts_req = storage::PayoutsNew {
|
||||
payout_id: payout_id.to_string(),
|
||||
merchant_id: merchant_id.to_owned(),
|
||||
customer_id: customer_id.to_owned(),
|
||||
customer_id,
|
||||
address_id: address_id.to_owned(),
|
||||
payout_type,
|
||||
amount,
|
||||
@ -2288,9 +2313,7 @@ pub async fn payout_create_db_entries(
|
||||
let payout_attempt_req = storage::PayoutAttemptNew {
|
||||
payout_attempt_id: payout_attempt_id.to_string(),
|
||||
payout_id: payout_id.to_owned(),
|
||||
customer_id: customer_id.to_owned(),
|
||||
merchant_id: merchant_id.to_owned(),
|
||||
address_id: address_id.to_owned(),
|
||||
status,
|
||||
business_country: req.business_country.to_owned(),
|
||||
business_label: req.business_label.to_owned(),
|
||||
@ -2314,7 +2337,7 @@ pub async fn payout_create_db_entries(
|
||||
Ok(PayoutData {
|
||||
billing_address,
|
||||
business_profile,
|
||||
customer_details: customer,
|
||||
customer_details: customer.map(ToOwned::to_owned),
|
||||
merchant_connector_account: None,
|
||||
payouts,
|
||||
payout_attempt,
|
||||
@ -2365,28 +2388,41 @@ pub async fn make_payout_data(
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?;
|
||||
|
||||
let customer_id = payouts.customer_id.as_ref();
|
||||
let payout_id = &payouts.payout_id;
|
||||
let billing_address = payment_helpers::create_or_find_address_for_payment_by_request(
|
||||
state,
|
||||
None,
|
||||
Some(&payouts.address_id.to_owned()),
|
||||
payouts.address_id.as_deref(),
|
||||
merchant_id,
|
||||
Some(&payouts.customer_id.to_owned()),
|
||||
customer_id,
|
||||
key_store,
|
||||
&payouts.payout_id,
|
||||
payout_id,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let customer_details = db
|
||||
.find_customer_optional_by_customer_id_merchant_id(
|
||||
&state.into(),
|
||||
&payouts.customer_id.to_owned(),
|
||||
merchant_id,
|
||||
key_store,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
let customer_details = customer_id
|
||||
.async_map(|customer_id| async move {
|
||||
db.find_customer_optional_by_customer_id_merchant_id(
|
||||
&state.into(),
|
||||
customer_id,
|
||||
merchant_id,
|
||||
key_store,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.change_context(errors::ApiErrorResponse::InternalServerError))
|
||||
.attach_printable_lazy(|| {
|
||||
format!(
|
||||
"Failed while fetching optional customer [id - {:?}] for payout [id - {}]",
|
||||
customer_id, payout_id
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_or(None, |c| c);
|
||||
.transpose()?
|
||||
.and_then(|c| c);
|
||||
|
||||
let profile_id = payout_attempt.profile_id.clone();
|
||||
|
||||
@ -2401,7 +2437,7 @@ pub async fn make_payout_data(
|
||||
let customer_id = customer_details
|
||||
.as_ref()
|
||||
.map(|cd| cd.get_customer_id().to_owned())
|
||||
.get_required_value("customer")?;
|
||||
.get_required_value("customer_id when payout_token is sent")?;
|
||||
helpers::make_payout_method_data(
|
||||
state,
|
||||
None,
|
||||
|
||||
@ -20,6 +20,7 @@ use router_env::logger;
|
||||
|
||||
use super::PayoutData;
|
||||
use crate::{
|
||||
consts,
|
||||
core::{
|
||||
errors::{self, RouterResult, StorageErrorExt},
|
||||
payment_methods::{
|
||||
@ -28,8 +29,8 @@ use crate::{
|
||||
vault,
|
||||
},
|
||||
payments::{
|
||||
customers::get_connector_customer_details_if_present, route_connector_v1, routing,
|
||||
CustomerDetails,
|
||||
customers::get_connector_customer_details_if_present, helpers as payment_helpers,
|
||||
route_connector_v1, routing, CustomerDetails,
|
||||
},
|
||||
routing::TransactionData,
|
||||
},
|
||||
@ -194,11 +195,12 @@ pub async fn make_payout_method_data<'a>(
|
||||
pub async fn save_payout_data_to_locker(
|
||||
state: &SessionState,
|
||||
payout_data: &mut PayoutData,
|
||||
customer_id: &id_type::CustomerId,
|
||||
payout_method_data: &api::PayoutMethodData,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
) -> RouterResult<()> {
|
||||
let payout_attempt = &payout_data.payout_attempt;
|
||||
let payouts = &payout_data.payouts;
|
||||
let (mut locker_req, card_details, bank_details, wallet_details, payment_method_type) =
|
||||
match payout_method_data {
|
||||
payouts::PayoutMethodData::Card(card) => {
|
||||
@ -215,7 +217,7 @@ pub async fn save_payout_data_to_locker(
|
||||
};
|
||||
let payload = StoreLockerReq::LockerCard(StoreCardReq {
|
||||
merchant_id: merchant_account.get_id().clone(),
|
||||
merchant_customer_id: payout_attempt.customer_id.to_owned(),
|
||||
merchant_customer_id: customer_id.to_owned(),
|
||||
card: Card {
|
||||
card_number: card.card_number.to_owned(),
|
||||
name_on_card: card.card_holder_name.to_owned(),
|
||||
@ -271,7 +273,7 @@ pub async fn save_payout_data_to_locker(
|
||||
})?;
|
||||
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
|
||||
merchant_id: merchant_account.get_id().to_owned(),
|
||||
merchant_customer_id: payout_attempt.customer_id.to_owned(),
|
||||
merchant_customer_id: customer_id.to_owned(),
|
||||
enc_data,
|
||||
ttl: state.conf.locker.ttl_for_storage_in_secs,
|
||||
});
|
||||
@ -301,7 +303,7 @@ pub async fn save_payout_data_to_locker(
|
||||
let stored_resp = cards::call_to_locker_hs(
|
||||
state,
|
||||
&locker_req,
|
||||
&payout_attempt.customer_id,
|
||||
customer_id,
|
||||
api_enums::LockerChoice::HyperswitchCardVault,
|
||||
)
|
||||
.await
|
||||
@ -403,7 +405,7 @@ pub async fn save_payout_data_to_locker(
|
||||
card: card_details.clone(),
|
||||
wallet: None,
|
||||
metadata: None,
|
||||
customer_id: Some(payout_attempt.customer_id.to_owned()),
|
||||
customer_id: Some(customer_id.to_owned()),
|
||||
card_network: None,
|
||||
client_secret: None,
|
||||
payment_method_data: None,
|
||||
@ -490,7 +492,7 @@ pub async fn save_payout_data_to_locker(
|
||||
card: None,
|
||||
wallet: wallet_details,
|
||||
metadata: None,
|
||||
customer_id: Some(payout_attempt.customer_id.to_owned()),
|
||||
customer_id: Some(customer_id.to_owned()),
|
||||
card_network: None,
|
||||
client_secret: None,
|
||||
payment_method_data: None,
|
||||
@ -503,11 +505,11 @@ pub async fn save_payout_data_to_locker(
|
||||
|
||||
// Insert new entry in payment_methods table
|
||||
if should_insert_in_pm_table {
|
||||
let payment_method_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm");
|
||||
let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm");
|
||||
cards::create_payment_method(
|
||||
state,
|
||||
&new_payment_method,
|
||||
&payout_attempt.customer_id,
|
||||
customer_id,
|
||||
&payment_method_id,
|
||||
Some(stored_resp.card_reference.clone()),
|
||||
merchant_account.get_id(),
|
||||
@ -538,7 +540,7 @@ pub async fn save_payout_data_to_locker(
|
||||
// Delete from locker
|
||||
cards::delete_card_from_hs_locker(
|
||||
state,
|
||||
&payout_attempt.customer_id,
|
||||
customer_id,
|
||||
merchant_account.get_id(),
|
||||
card_reference,
|
||||
)
|
||||
@ -553,7 +555,7 @@ pub async fn save_payout_data_to_locker(
|
||||
let stored_resp = cards::call_to_locker_hs(
|
||||
state,
|
||||
&locker_req,
|
||||
&payout_attempt.customer_id,
|
||||
customer_id,
|
||||
api_enums::LockerChoice::HyperswitchCardVault,
|
||||
)
|
||||
.await
|
||||
@ -590,9 +592,9 @@ pub async fn save_payout_data_to_locker(
|
||||
};
|
||||
payout_data.payouts = db
|
||||
.update_payout(
|
||||
&payout_data.payouts,
|
||||
payouts,
|
||||
updated_payout,
|
||||
payout_attempt,
|
||||
&payout_data.payout_attempt,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
@ -603,7 +605,7 @@ pub async fn save_payout_data_to_locker(
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "customer_v2"))]
|
||||
pub async fn get_or_create_customer_details(
|
||||
pub(super) async fn get_or_create_customer_details(
|
||||
_state: &SessionState,
|
||||
_customer_details: &CustomerDetails,
|
||||
_merchant_account: &domain::MerchantAccount,
|
||||
@ -613,7 +615,7 @@ pub async fn get_or_create_customer_details(
|
||||
}
|
||||
|
||||
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))]
|
||||
pub async fn get_or_create_customer_details(
|
||||
pub(super) async fn get_or_create_customer_details(
|
||||
state: &SessionState,
|
||||
customer_details: &CustomerDetails,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
@ -641,56 +643,78 @@ pub async fn get_or_create_customer_details(
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?
|
||||
{
|
||||
// Customer found
|
||||
Some(customer) => Ok(Some(customer)),
|
||||
|
||||
// Customer not found
|
||||
// create only if atleast one of the fields were provided for customer creation or else throw error
|
||||
None => {
|
||||
let encrypted_data = crypto_operation(
|
||||
&state.into(),
|
||||
type_name!(domain::Customer),
|
||||
CryptoOperation::BatchEncrypt(CustomerRequestWithEmail::to_encryptable(
|
||||
CustomerRequestWithEmail {
|
||||
name: customer_details.name.clone(),
|
||||
email: customer_details.email.clone(),
|
||||
phone: customer_details.phone.clone(),
|
||||
},
|
||||
)),
|
||||
Identifier::Merchant(key_store.merchant_id.clone()),
|
||||
key,
|
||||
)
|
||||
.await
|
||||
.and_then(|val| val.try_into_batchoperation())
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||
let encryptable_customer =
|
||||
CustomerRequestWithEmail::from_encryptable(encrypted_data)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||
|
||||
let customer = domain::Customer {
|
||||
customer_id,
|
||||
merchant_id: merchant_id.to_owned().clone(),
|
||||
name: encryptable_customer.name,
|
||||
email: encryptable_customer.email,
|
||||
phone: encryptable_customer.phone,
|
||||
description: None,
|
||||
phone_country_code: customer_details.phone_country_code.to_owned(),
|
||||
metadata: None,
|
||||
connector_customer: None,
|
||||
created_at: common_utils::date_time::now(),
|
||||
modified_at: common_utils::date_time::now(),
|
||||
address_id: None,
|
||||
default_payment_method_id: None,
|
||||
updated_by: None,
|
||||
version: common_enums::ApiVersion::V1,
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
db.insert_customer(
|
||||
customer,
|
||||
key_manager_state,
|
||||
key_store,
|
||||
merchant_account.storage_scheme,
|
||||
if customer_details.name.is_some()
|
||||
|| customer_details.email.is_some()
|
||||
|| customer_details.phone.is_some()
|
||||
|| customer_details.phone_country_code.is_some()
|
||||
{
|
||||
let encrypted_data = crypto_operation(
|
||||
&state.into(),
|
||||
type_name!(domain::Customer),
|
||||
CryptoOperation::BatchEncrypt(CustomerRequestWithEmail::to_encryptable(
|
||||
CustomerRequestWithEmail {
|
||||
name: customer_details.name.clone(),
|
||||
email: customer_details.email.clone(),
|
||||
phone: customer_details.phone.clone(),
|
||||
},
|
||||
)),
|
||||
Identifier::Merchant(key_store.merchant_id.clone()),
|
||||
key,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?,
|
||||
))
|
||||
.and_then(|val| val.try_into_batchoperation())
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to encrypt customer")?;
|
||||
let encryptable_customer =
|
||||
CustomerRequestWithEmail::from_encryptable(encrypted_data)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to form EncryptableCustomer")?;
|
||||
|
||||
let customer = domain::Customer {
|
||||
customer_id: customer_id.clone(),
|
||||
merchant_id: merchant_id.to_owned().clone(),
|
||||
name: encryptable_customer.name,
|
||||
email: encryptable_customer.email,
|
||||
phone: encryptable_customer.phone,
|
||||
description: None,
|
||||
phone_country_code: customer_details.phone_country_code.to_owned(),
|
||||
metadata: None,
|
||||
connector_customer: None,
|
||||
created_at: common_utils::date_time::now(),
|
||||
modified_at: common_utils::date_time::now(),
|
||||
address_id: None,
|
||||
default_payment_method_id: None,
|
||||
updated_by: None,
|
||||
version: consts::API_VERSION,
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
db.insert_customer(
|
||||
customer,
|
||||
key_manager_state,
|
||||
key_store,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable_lazy(|| {
|
||||
format!(
|
||||
"Failed to insert customer [id - {:?}] for merchant [id - {:?}]",
|
||||
customer_id, merchant_id
|
||||
)
|
||||
})?,
|
||||
))
|
||||
} else {
|
||||
Err(report!(errors::ApiErrorResponse::InvalidRequestData {
|
||||
message: format!("customer for id - {:?} not found", customer_id),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -997,6 +1021,7 @@ pub async fn update_payouts_and_payout_attempt(
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
req: &payouts::PayoutCreateRequest,
|
||||
state: &SessionState,
|
||||
merchant_key_store: &domain::MerchantKeyStore,
|
||||
) -> CustomResult<(), errors::ApiErrorResponse> {
|
||||
let payout_attempt = payout_data.payout_attempt.to_owned();
|
||||
let status = payout_attempt.status;
|
||||
@ -1011,6 +1036,47 @@ pub async fn update_payouts_and_payout_attempt(
|
||||
}));
|
||||
}
|
||||
|
||||
// Fetch customer details from request and create new or else use existing customer that was attached
|
||||
let customer = get_customer_details_from_request(req);
|
||||
let customer_id = if customer.customer_id.is_some()
|
||||
|| customer.name.is_some()
|
||||
|| customer.email.is_some()
|
||||
|| customer.phone.is_some()
|
||||
|| customer.phone_country_code.is_some()
|
||||
{
|
||||
payout_data.customer_details =
|
||||
get_or_create_customer_details(state, &customer, merchant_account, merchant_key_store)
|
||||
.await?;
|
||||
payout_data
|
||||
.customer_details
|
||||
.as_ref()
|
||||
.map(|customer| customer.get_customer_id())
|
||||
} else {
|
||||
payout_data.payouts.customer_id.clone()
|
||||
};
|
||||
|
||||
// Fetch address details from request and create new or else use existing address that was attached
|
||||
let billing_address = payment_helpers::create_or_find_address_for_payment_by_request(
|
||||
state,
|
||||
req.billing.as_ref(),
|
||||
None,
|
||||
merchant_account.get_id(),
|
||||
customer_id.as_ref(),
|
||||
merchant_key_store,
|
||||
&payout_id,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await?;
|
||||
let address_id = if billing_address.is_some() {
|
||||
payout_data.billing_address = billing_address;
|
||||
payout_data
|
||||
.billing_address
|
||||
.as_ref()
|
||||
.map(|address| address.address_id.clone())
|
||||
} else {
|
||||
payout_data.payouts.address_id.clone()
|
||||
};
|
||||
|
||||
// Update DB with new data
|
||||
let payouts = payout_data.payouts.to_owned();
|
||||
let amount = MinorUnit::from(req.amount.unwrap_or(payouts.amount.into()));
|
||||
@ -1042,6 +1108,8 @@ pub async fn update_payouts_and_payout_attempt(
|
||||
.payout_type
|
||||
.to_owned()
|
||||
.or(payouts.payout_type.to_owned()),
|
||||
address_id: address_id.clone(),
|
||||
customer_id: customer_id.clone(),
|
||||
};
|
||||
let db = &*state.store;
|
||||
payout_data.payouts = db
|
||||
@ -1070,25 +1138,66 @@ pub async fn update_payouts_and_payout_attempt(
|
||||
.to_owned()
|
||||
.and_then(|nl| if nl != l { Some(nl) } else { None })
|
||||
});
|
||||
match (updated_business_country, updated_business_label) {
|
||||
(None, None) => Ok(()),
|
||||
(business_country, business_label) => {
|
||||
let payout_attempt = &payout_data.payout_attempt;
|
||||
let updated_payout_attempt = storage::PayoutAttemptUpdate::BusinessUpdate {
|
||||
business_country,
|
||||
business_label,
|
||||
};
|
||||
payout_data.payout_attempt = db
|
||||
.update_payout_attempt(
|
||||
payout_attempt,
|
||||
updated_payout_attempt,
|
||||
&payout_data.payouts,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error updating payout_attempt")?;
|
||||
Ok(())
|
||||
}
|
||||
if updated_business_country.is_some()
|
||||
|| updated_business_label.is_some()
|
||||
|| customer_id.is_some()
|
||||
|| address_id.is_some()
|
||||
{
|
||||
let payout_attempt = &payout_data.payout_attempt;
|
||||
let updated_payout_attempt = storage::PayoutAttemptUpdate::BusinessUpdate {
|
||||
business_country: updated_business_country,
|
||||
business_label: updated_business_label,
|
||||
address_id,
|
||||
customer_id,
|
||||
};
|
||||
payout_data.payout_attempt = db
|
||||
.update_payout_attempt(
|
||||
payout_attempt,
|
||||
updated_payout_attempt,
|
||||
&payout_data.payouts,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error updating payout_attempt")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn get_customer_details_from_request(
|
||||
request: &payouts::PayoutCreateRequest,
|
||||
) -> CustomerDetails {
|
||||
let customer_id = request.get_customer_id().map(ToOwned::to_owned);
|
||||
|
||||
let customer_name = request
|
||||
.customer
|
||||
.as_ref()
|
||||
.and_then(|customer_details| customer_details.name.clone())
|
||||
.or(request.name.clone());
|
||||
|
||||
let customer_email = request
|
||||
.customer
|
||||
.as_ref()
|
||||
.and_then(|customer_details| customer_details.email.clone())
|
||||
.or(request.email.clone());
|
||||
|
||||
let customer_phone = request
|
||||
.customer
|
||||
.as_ref()
|
||||
.and_then(|customer_details| customer_details.phone.clone())
|
||||
.or(request.phone.clone());
|
||||
|
||||
let customer_phone_code = request
|
||||
.customer
|
||||
.as_ref()
|
||||
.and_then(|customer_details| customer_details.phone_country_code.clone())
|
||||
.or(request.phone_country_code.clone());
|
||||
|
||||
CustomerDetails {
|
||||
customer_id,
|
||||
name: customer_name,
|
||||
email: customer_email,
|
||||
phone: customer_phone,
|
||||
phone_country_code: customer_phone_code,
|
||||
}
|
||||
}
|
||||
|
||||
76
crates/router/src/core/payouts/transformers.rs
Normal file
76
crates/router/src/core/payouts/transformers.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use crate::types::{
|
||||
api, domain, storage,
|
||||
transformers::{ForeignFrom, ForeignInto},
|
||||
};
|
||||
|
||||
impl
|
||||
ForeignFrom<(
|
||||
storage::Payouts,
|
||||
storage::PayoutAttempt,
|
||||
Option<domain::Customer>,
|
||||
)> for api::PayoutCreateResponse
|
||||
{
|
||||
fn foreign_from(
|
||||
item: (
|
||||
storage::Payouts,
|
||||
storage::PayoutAttempt,
|
||||
Option<domain::Customer>,
|
||||
),
|
||||
) -> Self {
|
||||
let (payout, payout_attempt, customer) = item;
|
||||
let attempt = api::PayoutAttemptResponse {
|
||||
attempt_id: payout_attempt.payout_attempt_id,
|
||||
status: payout_attempt.status,
|
||||
amount: payout.amount,
|
||||
currency: Some(payout.destination_currency),
|
||||
connector: payout_attempt.connector.clone(),
|
||||
error_code: payout_attempt.error_code.clone(),
|
||||
error_message: payout_attempt.error_message.clone(),
|
||||
payment_method: payout.payout_type,
|
||||
payout_method_type: None,
|
||||
connector_transaction_id: payout_attempt.connector_payout_id,
|
||||
cancellation_reason: None,
|
||||
unified_code: None,
|
||||
unified_message: None,
|
||||
};
|
||||
Self {
|
||||
payout_id: payout.payout_id,
|
||||
merchant_id: payout.merchant_id,
|
||||
amount: payout.amount,
|
||||
currency: payout.destination_currency,
|
||||
connector: payout_attempt.connector,
|
||||
payout_type: payout.payout_type,
|
||||
auto_fulfill: payout.auto_fulfill,
|
||||
customer_id: customer.as_ref().map(|cust| cust.get_customer_id()),
|
||||
customer: customer.as_ref().map(|cust| cust.foreign_into()),
|
||||
return_url: payout.return_url,
|
||||
business_country: payout_attempt.business_country,
|
||||
business_label: payout_attempt.business_label,
|
||||
description: payout.description,
|
||||
entity_type: payout.entity_type,
|
||||
recurring: payout.recurring,
|
||||
metadata: payout.metadata,
|
||||
status: payout_attempt.status,
|
||||
error_message: payout_attempt.error_message,
|
||||
error_code: payout_attempt.error_code,
|
||||
profile_id: payout.profile_id,
|
||||
created: Some(payout.created_at),
|
||||
connector_transaction_id: attempt.connector_transaction_id.clone(),
|
||||
priority: payout.priority,
|
||||
attempts: Some(vec![attempt]),
|
||||
billing: None,
|
||||
client_secret: None,
|
||||
payout_link: None,
|
||||
email: customer
|
||||
.as_ref()
|
||||
.and_then(|customer| customer.email.clone()),
|
||||
name: customer.as_ref().and_then(|customer| customer.name.clone()),
|
||||
phone: customer
|
||||
.as_ref()
|
||||
.and_then(|customer| customer.phone.clone()),
|
||||
phone_country_code: customer
|
||||
.as_ref()
|
||||
.and_then(|customer| customer.phone_country_code.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,12 +53,17 @@ pub async fn validate_create_request(
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
req: &payouts::PayoutCreateRequest,
|
||||
merchant_key_store: &domain::MerchantKeyStore,
|
||||
) -> RouterResult<(String, Option<payouts::PayoutMethodData>, String)> {
|
||||
) -> RouterResult<(
|
||||
String,
|
||||
Option<payouts::PayoutMethodData>,
|
||||
String,
|
||||
Option<domain::Customer>,
|
||||
)> {
|
||||
let merchant_id = merchant_account.get_id();
|
||||
|
||||
if let Some(payout_link) = &req.payout_link {
|
||||
if *payout_link {
|
||||
validate_payout_link_request(req.confirm)?;
|
||||
validate_payout_link_request(req)?;
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,28 +101,46 @@ pub async fn validate_create_request(
|
||||
None => Ok(()),
|
||||
}?;
|
||||
|
||||
// Payout token
|
||||
let payout_method_data = match req.payout_token.to_owned() {
|
||||
Some(payout_token) => {
|
||||
let customer_id = req
|
||||
.customer_id
|
||||
.to_owned()
|
||||
.unwrap_or_else(common_utils::generate_customer_id_of_default_length);
|
||||
// Fetch customer details (merge of loose fields + customer object) and create DB entry
|
||||
let customer_in_request = helpers::get_customer_details_from_request(req);
|
||||
let customer = if customer_in_request.customer_id.is_some()
|
||||
|| customer_in_request.name.is_some()
|
||||
|| customer_in_request.email.is_some()
|
||||
|| customer_in_request.phone.is_some()
|
||||
|| customer_in_request.phone_country_code.is_some()
|
||||
{
|
||||
helpers::get_or_create_customer_details(
|
||||
state,
|
||||
&customer_in_request,
|
||||
merchant_account,
|
||||
merchant_key_store,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// payout_token
|
||||
let payout_method_data = match (req.payout_token.as_ref(), customer.as_ref()) {
|
||||
(Some(_), None) => Err(report!(errors::ApiErrorResponse::MissingRequiredField {
|
||||
field_name: "customer or customer_id when payout_token is provided"
|
||||
})),
|
||||
(Some(payout_token), Some(customer)) => {
|
||||
helpers::make_payout_method_data(
|
||||
state,
|
||||
req.payout_method_data.as_ref(),
|
||||
Some(&payout_token),
|
||||
&customer_id,
|
||||
Some(payout_token),
|
||||
&customer.customer_id,
|
||||
merchant_account.get_id(),
|
||||
req.payout_type,
|
||||
merchant_key_store,
|
||||
None,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await?
|
||||
.await
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
_ => Ok(None),
|
||||
}?;
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "v1", feature = "v2"),
|
||||
@ -145,19 +168,24 @@ pub async fn validate_create_request(
|
||||
})
|
||||
.attach_printable("Profile id is a mandatory parameter")?;
|
||||
|
||||
Ok((payout_id, payout_method_data, profile_id))
|
||||
Ok((payout_id, payout_method_data, profile_id, customer))
|
||||
}
|
||||
|
||||
pub fn validate_payout_link_request(confirm: Option<bool>) -> Result<(), errors::ApiErrorResponse> {
|
||||
if let Some(cnf) = confirm {
|
||||
if cnf {
|
||||
return Err(errors::ApiErrorResponse::InvalidRequestData {
|
||||
message: "cannot confirm a payout while creating a payout link".to_string(),
|
||||
});
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
pub fn validate_payout_link_request(
|
||||
req: &payouts::PayoutCreateRequest,
|
||||
) -> Result<(), errors::ApiErrorResponse> {
|
||||
if req.confirm.unwrap_or(false) {
|
||||
return Err(errors::ApiErrorResponse::InvalidRequestData {
|
||||
message: "cannot confirm a payout while creating a payout link".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if req.customer_id.is_none() {
|
||||
return Err(errors::ApiErrorResponse::MissingRequiredField {
|
||||
field_name: "customer or customer_id when payout_link is true",
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user