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:
Kashif
2023-07-19 23:32:05 +05:30
committed by GitHub
parent e0f4507b10
commit 763e2df3bd
86 changed files with 9326 additions and 221 deletions

View File

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