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:
Kashif
2024-08-09 18:18:43 +05:30
committed by GitHub
parent 3183a86ecd
commit 942e63d9cd
31 changed files with 1781 additions and 915 deletions

View File

@ -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,
}
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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,
}
}

View 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()),
}
}
}

View File

@ -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(())
}