mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 02:57:02 +08:00
feat: add payout service (#1665)
Co-authored-by: Kashif <mohammed.kashif@juspay.in> Co-authored-by: Manoj Ghorela <manoj.ghorela@juspay.in> Co-authored-by: Manoj Ghorela <118727120+manoj-juspay@users.noreply.github.com>
This commit is contained in:
@ -2,10 +2,17 @@ use std::marker::PhantomData;
|
||||
|
||||
use api_models::enums::{DisputeStage, DisputeStatus};
|
||||
use common_utils::errors::CustomResult;
|
||||
#[cfg(feature = "payouts")]
|
||||
use common_utils::{crypto::Encryptable, pii::Email};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use router_env::{instrument, tracing};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::payments::{helpers, PaymentAddress};
|
||||
#[cfg(feature = "payouts")]
|
||||
use super::payouts::PayoutData;
|
||||
#[cfg(feature = "payouts")]
|
||||
use crate::core::payments;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
consts,
|
||||
@ -16,14 +23,169 @@ use crate::{
|
||||
storage::{self, enums},
|
||||
ErrorResponse,
|
||||
},
|
||||
utils::{generate_id, OptionExt, ValueExt},
|
||||
utils::{generate_id, generate_uuid, OptionExt, ValueExt},
|
||||
};
|
||||
|
||||
const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW: &str =
|
||||
pub const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW: &str =
|
||||
"irrelevant_connector_request_reference_id_in_dispute_flow";
|
||||
const IRRELEVANT_PAYMENT_ID_IN_DISPUTE_FLOW: &str = "irrelevant_payment_id_in_dispute_flow";
|
||||
const IRRELEVANT_ATTEMPT_ID_IN_DISPUTE_FLOW: &str = "irrelevant_attempt_id_in_dispute_flow";
|
||||
|
||||
#[cfg(feature = "payouts")]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_mca_for_payout<'a>(
|
||||
state: &'a AppState,
|
||||
connector_id: &str,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
payout_data: &PayoutData,
|
||||
) -> RouterResult<helpers::MerchantConnectorAccountType> {
|
||||
let payout_attempt = &payout_data.payout_attempt;
|
||||
match payout_data.merchant_connector_account.to_owned() {
|
||||
Some(mca) => Ok(mca),
|
||||
None => {
|
||||
let (business_country, business_label) = helpers::get_business_details(
|
||||
payout_attempt.business_country,
|
||||
payout_attempt.business_label.as_ref(),
|
||||
merchant_account,
|
||||
)?;
|
||||
|
||||
let connector_label =
|
||||
helpers::get_connector_label(business_country, &business_label, None, connector_id);
|
||||
|
||||
let merchant_connector_account = helpers::get_merchant_connector_account(
|
||||
state,
|
||||
merchant_account.merchant_id.as_str(),
|
||||
&connector_label,
|
||||
None,
|
||||
key_store,
|
||||
)
|
||||
.await?;
|
||||
Ok(merchant_connector_account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "payouts")]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn construct_payout_router_data<'a, F>(
|
||||
state: &'a AppState,
|
||||
connector_id: &str,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
_request: &api_models::payouts::PayoutRequest,
|
||||
payout_data: &mut PayoutData,
|
||||
) -> RouterResult<types::PayoutsRouterData<F>> {
|
||||
let (business_country, _) = helpers::get_business_details(
|
||||
payout_data.payout_attempt.business_country,
|
||||
payout_data.payout_attempt.business_label.as_ref(),
|
||||
merchant_account,
|
||||
)?;
|
||||
let merchant_connector_account = get_mca_for_payout(
|
||||
state,
|
||||
connector_id,
|
||||
merchant_account,
|
||||
key_store,
|
||||
payout_data,
|
||||
)
|
||||
.await?;
|
||||
payout_data.merchant_connector_account = Some(merchant_connector_account.clone());
|
||||
let connector_auth_type: types::ConnectorAuthType = merchant_connector_account
|
||||
.get_connector_account_details()
|
||||
.parse_value("ConnectorAuthType")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||
|
||||
let billing = payout_data.billing_address.to_owned();
|
||||
|
||||
let address = PaymentAddress {
|
||||
shipping: None,
|
||||
billing: billing.map(|a| {
|
||||
let phone_details = api_models::payments::PhoneDetails {
|
||||
number: a.phone_number.clone().map(Encryptable::into_inner),
|
||||
country_code: a.country_code.to_owned(),
|
||||
};
|
||||
let address_details = api_models::payments::AddressDetails {
|
||||
city: a.city.to_owned(),
|
||||
country: a.country.to_owned(),
|
||||
line1: a.line1.clone().map(Encryptable::into_inner),
|
||||
line2: a.line2.clone().map(Encryptable::into_inner),
|
||||
line3: a.line3.clone().map(Encryptable::into_inner),
|
||||
zip: a.zip.clone().map(Encryptable::into_inner),
|
||||
first_name: a.first_name.clone().map(Encryptable::into_inner),
|
||||
last_name: a.last_name.clone().map(Encryptable::into_inner),
|
||||
state: a.state.map(Encryptable::into_inner),
|
||||
};
|
||||
|
||||
api_models::payments::Address {
|
||||
phone: Some(phone_details),
|
||||
address: Some(address_details),
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
let test_mode: Option<bool> = merchant_connector_account.is_test_mode_on();
|
||||
let payouts = &payout_data.payouts;
|
||||
let payout_attempt = &payout_data.payout_attempt;
|
||||
let customer_details = &payout_data.customer_details;
|
||||
let connector_customer_id = customer_details
|
||||
.as_ref()
|
||||
.and_then(|c| c.connector_customer.as_ref())
|
||||
.and_then(|cc| cc.get("id"))
|
||||
.and_then(|id| serde_json::from_value::<String>(id.to_owned()).ok());
|
||||
let router_data = types::RouterData {
|
||||
flow: PhantomData,
|
||||
merchant_id: merchant_account.merchant_id.to_owned(),
|
||||
customer_id: None,
|
||||
connector_customer: connector_customer_id,
|
||||
connector: connector_id.to_string(),
|
||||
payment_id: "".to_string(),
|
||||
attempt_id: "".to_string(),
|
||||
status: enums::AttemptStatus::Failure,
|
||||
payment_method: enums::PaymentMethod::default(),
|
||||
connector_auth_type,
|
||||
description: None,
|
||||
return_url: payouts.return_url.to_owned(),
|
||||
payment_method_id: None,
|
||||
address,
|
||||
auth_type: enums::AuthenticationType::default(),
|
||||
connector_meta_data: merchant_connector_account.get_metadata(),
|
||||
amount_captured: None,
|
||||
request: types::PayoutsData {
|
||||
payout_id: payouts.payout_id.to_owned(),
|
||||
amount: payouts.amount,
|
||||
connector_payout_id: Some(payout_attempt.connector_payout_id.to_owned()),
|
||||
destination_currency: payouts.destination_currency,
|
||||
source_currency: payouts.source_currency,
|
||||
entity_type: payouts.entity_type.to_owned(),
|
||||
payout_type: payouts.payout_type,
|
||||
country_code: business_country,
|
||||
customer_details: customer_details
|
||||
.to_owned()
|
||||
.map(|c| payments::CustomerDetails {
|
||||
customer_id: Some(c.customer_id),
|
||||
name: c.name.map(Encryptable::into_inner),
|
||||
email: c.email.map(Email::from),
|
||||
phone: c.phone.map(Encryptable::into_inner),
|
||||
phone_country_code: c.phone_country_code,
|
||||
}),
|
||||
},
|
||||
response: Ok(types::PayoutsResponseData::default()),
|
||||
access_token: None,
|
||||
session_token: None,
|
||||
reference_id: None,
|
||||
payment_method_token: None,
|
||||
recurring_mandate_payment_data: None,
|
||||
preprocessing_id: None,
|
||||
connector_request_reference_id: IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW
|
||||
.to_string(),
|
||||
payout_method_data: payout_data.payout_method_data.to_owned(),
|
||||
quote_id: None,
|
||||
test_mode,
|
||||
};
|
||||
|
||||
Ok(router_data)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn construct_refund_router_data<'a, F>(
|
||||
@ -120,6 +282,10 @@ pub async fn construct_refund_router_data<'a, F>(
|
||||
&merchant_account.merchant_id,
|
||||
payment_attempt,
|
||||
),
|
||||
#[cfg(feature = "payouts")]
|
||||
payout_method_data: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
quote_id: None,
|
||||
test_mode,
|
||||
};
|
||||
|
||||
@ -137,6 +303,16 @@ pub fn get_or_generate_id(
|
||||
.map_or(Ok(generate_id(consts::ID_LENGTH, prefix)), validate_id)
|
||||
}
|
||||
|
||||
pub fn get_or_generate_uuid(
|
||||
key: &str,
|
||||
provided_id: Option<&String>,
|
||||
) -> Result<String, errors::ApiErrorResponse> {
|
||||
let validate_id = |id: String| validate_uuid(id, key);
|
||||
provided_id
|
||||
.cloned()
|
||||
.map_or(Ok(generate_uuid()), validate_id)
|
||||
}
|
||||
|
||||
fn invalid_id_format_error(key: &str) -> errors::ApiErrorResponse {
|
||||
errors::ApiErrorResponse::InvalidDataFormat {
|
||||
field_name: key.to_string(),
|
||||
@ -155,6 +331,13 @@ pub fn validate_id(id: String, key: &str) -> Result<String, errors::ApiErrorResp
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_uuid(uuid: String, key: &str) -> Result<String, errors::ApiErrorResponse> {
|
||||
match (Uuid::parse_str(&uuid), uuid.len() > consts::MAX_ID_LENGTH) {
|
||||
(Ok(_), false) => Ok(uuid),
|
||||
(_, _) => Err(invalid_id_format_error(key)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -315,6 +498,10 @@ pub async fn construct_accept_dispute_router_data<'a>(
|
||||
&merchant_account.merchant_id,
|
||||
payment_attempt,
|
||||
),
|
||||
#[cfg(feature = "payouts")]
|
||||
payout_method_data: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
quote_id: None,
|
||||
test_mode,
|
||||
};
|
||||
Ok(router_data)
|
||||
@ -384,6 +571,10 @@ pub async fn construct_submit_evidence_router_data<'a>(
|
||||
&merchant_account.merchant_id,
|
||||
payment_attempt,
|
||||
),
|
||||
#[cfg(feature = "payouts")]
|
||||
payout_method_data: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
quote_id: None,
|
||||
test_mode,
|
||||
};
|
||||
Ok(router_data)
|
||||
@ -454,6 +645,10 @@ pub async fn construct_upload_file_router_data<'a>(
|
||||
&merchant_account.merchant_id,
|
||||
payment_attempt,
|
||||
),
|
||||
#[cfg(feature = "payouts")]
|
||||
payout_method_data: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
quote_id: None,
|
||||
test_mode,
|
||||
};
|
||||
Ok(router_data)
|
||||
@ -526,6 +721,10 @@ pub async fn construct_defend_dispute_router_data<'a>(
|
||||
&merchant_account.merchant_id,
|
||||
payment_attempt,
|
||||
),
|
||||
#[cfg(feature = "payouts")]
|
||||
payout_method_data: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
quote_id: None,
|
||||
test_mode,
|
||||
};
|
||||
Ok(router_data)
|
||||
@ -593,6 +792,10 @@ pub async fn construct_retrieve_file_router_data<'a>(
|
||||
preprocessing_id: None,
|
||||
connector_request_reference_id: IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW
|
||||
.to_string(),
|
||||
#[cfg(feature = "payouts")]
|
||||
payout_method_data: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
quote_id: None,
|
||||
test_mode,
|
||||
};
|
||||
Ok(router_data)
|
||||
|
||||
Reference in New Issue
Block a user