#[cfg(feature = "payouts")] use api_models::payouts::PayoutMethodData; #[cfg(feature = "payouts")] use common_utils::pii::Email; use masking::Secret; use serde::{Deserialize, Serialize}; type Error = error_stack::Report; #[cfg(feature = "payouts")] use crate::{ connector::utils::{self, PayoutsData, RouterData}, types::{ api::payouts, storage::enums::{self as storage_enums, PayoutEntityType}, }, }; use crate::{core::errors, types}; pub struct WiseAuthType { pub(super) api_key: Secret, #[allow(dead_code)] pub(super) profile_id: Secret, } impl TryFrom<&types::ConnectorAuthType> for WiseAuthType { type Error = Error; fn try_from(auth_type: &types::ConnectorAuthType) -> Result { match auth_type { types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { api_key: api_key.to_owned(), profile_id: key1.to_owned(), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, } } } // Wise error response #[derive(Debug, Deserialize, Serialize)] pub struct ErrorResponse { pub timestamp: Option, pub errors: Option>, pub status: Option, pub error: Option, pub error_description: Option, pub message: Option, pub path: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct SubError { pub code: String, pub message: String, pub path: Option, pub field: Option, } // Payouts #[cfg(feature = "payouts")] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct WiseRecipientCreateRequest { currency: String, #[serde(rename = "type")] recipient_type: RecipientType, profile: Secret, account_holder_name: Secret, details: WiseBankDetails, } #[cfg(feature = "payouts")] #[derive(Debug, Serialize)] #[serde(rename_all = "snake_case")] #[allow(dead_code)] pub enum RecipientType { Aba, Iban, SortCode, SwiftCode, } #[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum AccountType { Checking, } #[cfg(feature = "payouts")] #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WiseBankDetails { legal_type: LegalType, account_type: Option, address: Option, post_code: Option, nationality: Option, account_holder_name: Option>, email: Option, account_number: Option>, city: Option, sort_code: Option>, iban: Option>, bic: Option>, transit_number: Option>, routing_number: Option>, abartn: Option>, swift_code: Option>, payin_reference: Option, psp_reference: Option, tax_id: Option, order_id: Option, job: Option, } #[cfg(feature = "payouts")] #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum LegalType { Business, #[default] Private, } #[cfg(feature = "payouts")] #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WiseAddressDetails { country: Option, country_code: Option, first_line: Option>, post_code: Option>, city: Option, state: Option>, } #[allow(dead_code)] #[cfg(feature = "payouts")] #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WiseRecipientCreateResponse { id: i64, business: Option, profile: Option, account_holder_name: Secret, currency: String, country: String, #[serde(rename = "type")] request_type: String, details: Option, } #[cfg(feature = "payouts")] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct WisePayoutQuoteRequest { source_currency: String, target_currency: String, source_amount: Option, target_amount: Option, pay_out: WisePayOutOption, } #[cfg(feature = "payouts")] #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum WisePayOutOption { Balance, #[default] BankTransfer, Swift, SwiftOur, Interac, } #[allow(dead_code)] #[cfg(feature = "payouts")] #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WisePayoutQuoteResponse { source_amount: f64, client_id: String, id: String, status: WiseStatus, profile: i64, rate: Option, source_currency: Option, target_currency: Option, user: Option, rate_type: Option, pay_out: Option, } #[cfg(feature = "payouts")] #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum WiseRateType { #[default] Fixed, Floating, } #[cfg(feature = "payouts")] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct WisePayoutCreateRequest { target_account: i64, quote_uuid: String, customer_transaction_id: String, details: WiseTransferDetails, } #[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WiseTransferDetails { transfer_purpose: Option, source_of_funds: Option, transfer_purpose_sub_transfer_purpose: Option, } #[allow(dead_code)] #[cfg(feature = "payouts")] #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WisePayoutResponse { id: i64, user: i64, target_account: i64, source_account: Option, quote_uuid: String, status: WiseStatus, reference: Option, rate: Option, business: Option, details: Option, has_active_issues: Option, source_currency: Option, source_value: Option, target_currency: Option, target_value: Option, customer_transaction_id: Option, } #[cfg(feature = "payouts")] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct WisePayoutFulfillRequest { #[serde(rename = "type")] fund_type: FundType, } // NOTE - Only balance is allowed as time of incorporating this field - https://api-docs.transferwise.com/api-reference/transfer#fund #[cfg(feature = "payouts")] #[derive(Debug, Default, Serialize)] #[serde(rename_all = "UPPERCASE")] pub enum FundType { #[default] Balance, } #[allow(dead_code)] #[cfg(feature = "payouts")] #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WiseFulfillResponse { status: WiseStatus, error_code: Option, error_message: Option, balance_transaction_id: Option, } #[cfg(feature = "payouts")] #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum WiseStatus { Completed, Pending, Rejected, #[serde(rename = "cancelled")] Cancelled, #[serde(rename = "processing")] #[default] Processing, #[serde(rename = "incoming_payment_waiting")] IncomingPaymentWaiting, } #[cfg(feature = "payouts")] fn get_payout_address_details( address: Option<&api_models::payments::Address>, ) -> Option { address.and_then(|add| { add.address.as_ref().map(|a| WiseAddressDetails { country: a.country, country_code: a.country, first_line: a.line1.clone(), post_code: a.zip.clone(), city: a.city.clone(), state: a.state.clone(), }) }) } #[cfg(feature = "payouts")] fn get_payout_bank_details( payout_method_data: PayoutMethodData, address: Option<&api_models::payments::Address>, entity_type: PayoutEntityType, ) -> Result { let wise_address_details = match get_payout_address_details(address) { Some(a) => Ok(a), None => Err(errors::ConnectorError::MissingRequiredField { field_name: "address", }), }?; match payout_method_data { PayoutMethodData::Bank(payouts::BankPayout::Ach(b)) => Ok(WiseBankDetails { legal_type: LegalType::from(entity_type), address: Some(wise_address_details), account_number: Some(b.bank_account_number.to_owned()), abartn: Some(b.bank_routing_number), account_type: Some(AccountType::Checking), ..WiseBankDetails::default() }), PayoutMethodData::Bank(payouts::BankPayout::Bacs(b)) => Ok(WiseBankDetails { legal_type: LegalType::from(entity_type), address: Some(wise_address_details), account_number: Some(b.bank_account_number.to_owned()), sort_code: Some(b.bank_sort_code), ..WiseBankDetails::default() }), PayoutMethodData::Bank(payouts::BankPayout::Sepa(b)) => Ok(WiseBankDetails { legal_type: LegalType::from(entity_type), address: Some(wise_address_details), iban: Some(b.iban.to_owned()), bic: b.bic, ..WiseBankDetails::default() }), _ => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Wise"), ))?, } } // Payouts recipient create request transform #[cfg(feature = "payouts")] impl TryFrom<&types::PayoutsRouterData> for WiseRecipientCreateRequest { type Error = Error; fn try_from(item: &types::PayoutsRouterData) -> Result { let request = item.request.to_owned(); let customer_details = request.customer_details.to_owned(); let payout_method_data = item.get_payout_method_data()?; let bank_details = get_payout_bank_details( payout_method_data.to_owned(), item.get_optional_billing(), item.request.entity_type, )?; let source_id = match item.connector_auth_type.to_owned() { types::ConnectorAuthType::BodyKey { api_key: _, key1 } => Ok(key1), _ => Err(errors::ConnectorError::MissingRequiredField { field_name: "source_id for PayoutRecipient creation", }), }?; let payout_type = request.get_payout_type()?; match payout_type { storage_enums::PayoutType::Card | storage_enums::PayoutType::Wallet => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Wise"), ))? } storage_enums::PayoutType::Bank => { let account_holder_name = customer_details .ok_or(errors::ConnectorError::MissingRequiredField { field_name: "customer_details for PayoutRecipient creation", })? .name .ok_or(errors::ConnectorError::MissingRequiredField { field_name: "customer_details.name for PayoutRecipient creation", })?; Ok(Self { profile: source_id, currency: request.destination_currency.to_string(), recipient_type: RecipientType::try_from(payout_method_data)?, account_holder_name, details: bank_details, }) } } } } // Payouts recipient fulfill response transform #[cfg(feature = "payouts")] impl TryFrom> for types::PayoutsRouterData { type Error = Error; fn try_from( item: types::PayoutsResponseRouterData, ) -> Result { let response: WiseRecipientCreateResponse = item.response; Ok(Self { response: Ok(types::PayoutsResponseData { status: Some(storage_enums::PayoutStatus::RequiresCreation), connector_payout_id: Some(response.id.to_string()), payout_eligible: None, should_add_next_step_to_process_tracker: false, }), ..item.data }) } } // Payouts quote request transform #[cfg(feature = "payouts")] impl TryFrom<&types::PayoutsRouterData> for WisePayoutQuoteRequest { type Error = Error; fn try_from(item: &types::PayoutsRouterData) -> Result { let request = item.request.to_owned(); let payout_type = request.get_payout_type()?; match payout_type { storage_enums::PayoutType::Bank => Ok(Self { source_amount: Some(request.amount), source_currency: request.source_currency.to_string(), target_amount: None, target_currency: request.destination_currency.to_string(), pay_out: WisePayOutOption::default(), }), storage_enums::PayoutType::Card | storage_enums::PayoutType::Wallet => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Wise"), ))? } } } } // Payouts quote response transform #[cfg(feature = "payouts")] impl TryFrom> for types::PayoutsRouterData { type Error = Error; fn try_from( item: types::PayoutsResponseRouterData, ) -> Result { let response: WisePayoutQuoteResponse = item.response; Ok(Self { response: Ok(types::PayoutsResponseData { status: Some(storage_enums::PayoutStatus::RequiresCreation), connector_payout_id: Some(response.id), payout_eligible: None, should_add_next_step_to_process_tracker: false, }), ..item.data }) } } // Payouts transfer creation request #[cfg(feature = "payouts")] impl TryFrom<&types::PayoutsRouterData> for WisePayoutCreateRequest { type Error = Error; fn try_from(item: &types::PayoutsRouterData) -> Result { let request = item.request.to_owned(); let payout_type = request.get_payout_type()?; match payout_type { storage_enums::PayoutType::Bank => { let connector_customer_id = item.get_connector_customer_id()?; let quote_uuid = item.get_quote_id()?; let wise_transfer_details = WiseTransferDetails { transfer_purpose: None, source_of_funds: None, transfer_purpose_sub_transfer_purpose: None, }; let target_account: i64 = connector_customer_id.trim().parse().map_err(|_| { errors::ConnectorError::MissingRequiredField { field_name: "profile", } })?; Ok(Self { target_account, quote_uuid, customer_transaction_id: request.payout_id, details: wise_transfer_details, }) } storage_enums::PayoutType::Card | storage_enums::PayoutType::Wallet => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Wise"), ))? } } } } // Payouts transfer creation response #[cfg(feature = "payouts")] impl TryFrom> for types::PayoutsRouterData { type Error = Error; fn try_from( item: types::PayoutsResponseRouterData, ) -> Result { let response: WisePayoutResponse = item.response; let status = match storage_enums::PayoutStatus::from(response.status) { storage_enums::PayoutStatus::Cancelled => storage_enums::PayoutStatus::Cancelled, _ => storage_enums::PayoutStatus::RequiresFulfillment, }; Ok(Self { response: Ok(types::PayoutsResponseData { status: Some(status), connector_payout_id: Some(response.id.to_string()), payout_eligible: None, should_add_next_step_to_process_tracker: false, }), ..item.data }) } } // Payouts fulfill request transform #[cfg(feature = "payouts")] impl TryFrom<&types::PayoutsRouterData> for WisePayoutFulfillRequest { type Error = Error; fn try_from(item: &types::PayoutsRouterData) -> Result { let payout_type = item.request.get_payout_type()?; match payout_type { storage_enums::PayoutType::Bank => Ok(Self { fund_type: FundType::default(), }), storage_enums::PayoutType::Card | storage_enums::PayoutType::Wallet => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Wise"), ))? } } } } // Payouts fulfill response transform #[cfg(feature = "payouts")] impl TryFrom> for types::PayoutsRouterData { type Error = Error; fn try_from( item: types::PayoutsResponseRouterData, ) -> Result { let response: WiseFulfillResponse = item.response; Ok(Self { response: Ok(types::PayoutsResponseData { status: Some(storage_enums::PayoutStatus::from(response.status)), connector_payout_id: Some( item.data .request .connector_payout_id .clone() .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?, ), payout_eligible: None, should_add_next_step_to_process_tracker: false, }), ..item.data }) } } #[cfg(feature = "payouts")] impl From for storage_enums::PayoutStatus { fn from(wise_status: WiseStatus) -> Self { match wise_status { WiseStatus::Completed => Self::Success, WiseStatus::Rejected => Self::Failed, WiseStatus::Cancelled => Self::Cancelled, WiseStatus::Pending | WiseStatus::Processing | WiseStatus::IncomingPaymentWaiting => { Self::Pending } } } } #[cfg(feature = "payouts")] impl From for LegalType { fn from(entity_type: PayoutEntityType) -> Self { match entity_type { PayoutEntityType::Individual | PayoutEntityType::Personal | PayoutEntityType::NonProfit | PayoutEntityType::NaturalPerson => Self::Private, PayoutEntityType::Company | PayoutEntityType::PublicSector | PayoutEntityType::Business => Self::Business, } } } #[cfg(feature = "payouts")] impl TryFrom for RecipientType { type Error = error_stack::Report; fn try_from(payout_method_type: PayoutMethodData) -> Result { match payout_method_type { PayoutMethodData::Bank(api_models::payouts::Bank::Ach(_)) => Ok(Self::Aba), PayoutMethodData::Bank(api_models::payouts::Bank::Bacs(_)) => Ok(Self::SortCode), PayoutMethodData::Bank(api_models::payouts::Bank::Sepa(_)) => Ok(Self::Iban), _ => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Wise"), ) .into()), } } }