diff --git a/Cargo.lock b/Cargo.lock index b8290efee027..b2b8d3ffd014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,7 @@ dependencies = [ "router_derive", "serde", "serde_json", + "serde_with", "strum 0.24.1", "thiserror", "time 0.3.22", diff --git a/config/config.example.toml b/config/config.example.toml index bee61f2f6156..2e3e83a05c6e 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -415,7 +415,7 @@ cashapp = {country = "US", currency = "USD"} [connector_customer] connector_list = "gocardless,stax,stripe" -payout_connector_list = "wise" +payout_connector_list = "stripe,wise" [bank_config.online_banking_fpx] adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" diff --git a/config/development.toml b/config/development.toml index 64bdd45f1c31..da04edb9d2a4 100644 --- a/config/development.toml +++ b/config/development.toml @@ -387,7 +387,7 @@ bluesnap = {payment_method = "card"} [connector_customer] connector_list = "gocardless,stax,stripe" -payout_connector_list = "wise" +payout_connector_list = "stripe,wise" [dummy_connector] enabled = true diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 8504ec130e62..e8ac9cf32fe7 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -306,7 +306,7 @@ bank_debit.sepa = { connector_list = "gocardless"} [connector_customer] connector_list = "gocardless,stax,stripe" -payout_connector_list = "wise" +payout_connector_list = "stripe,wise" [multiple_api_version_supported_connectors] supported_connectors = "braintree" diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index fd6b92a91ceb..91211169b08c 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -258,6 +258,7 @@ pub enum RoutableConnectors { #[strum(serialize_all = "snake_case")] pub enum PayoutConnectors { Adyen, + Stripe, Wise, } @@ -266,6 +267,7 @@ impl From for RoutableConnectors { fn from(value: PayoutConnectors) -> Self { match value { PayoutConnectors::Adyen => Self::Adyen, + PayoutConnectors::Stripe => Self::Stripe, PayoutConnectors::Wise => Self::Wise, } } diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 5cc5e5118166..c835714c9bb9 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -4,6 +4,7 @@ use common_utils::{ pii::{self, Email}, }; use masking::Secret; +use router_derive::DebugAsDisplay; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -388,6 +389,7 @@ pub struct PayoutCreateResponse { #[derive(Default, Debug, Clone, Deserialize, ToSchema)] pub struct PayoutRetrieveBody { pub force_sync: Option, + pub merchant_id: Option, } #[derive(Default, Debug, Serialize, ToSchema, Clone, Deserialize)] @@ -406,6 +408,9 @@ pub struct PayoutRetrieveRequest { /// (defaults to false) #[schema(value_type = Option, default = false, example = true)] pub force_sync: Option, + + /// The identifier for the Merchant Account. + pub merchant_id: Option, } #[derive(Default, Debug, Serialize, ToSchema, Clone, Deserialize)] @@ -420,3 +425,40 @@ pub struct PayoutActionRequest { )] pub payout_id: String, } + +#[derive(Default, DebugAsDisplay, Debug, ToSchema, Clone, Deserialize)] +pub struct PayoutVendorAccountDetails { + pub vendor_details: PayoutVendorDetails, + pub individual_details: PayoutIndividualDetails, +} + +#[derive(Default, DebugAsDisplay, Debug, Serialize, ToSchema, Clone, Deserialize)] +pub struct PayoutVendorDetails { + pub account_type: String, + pub business_type: String, + pub business_profile_mcc: Option, + pub business_profile_url: Option, + pub business_profile_name: Option>, + pub company_address_line1: Option>, + pub company_address_line2: Option>, + pub company_address_postal_code: Option>, + pub company_address_city: Option>, + pub company_address_state: Option>, + pub company_phone: Option>, + pub company_tax_id: Option>, + pub company_owners_provided: Option, + pub capabilities_card_payments: Option, + pub capabilities_transfers: Option, +} + +#[derive(Default, DebugAsDisplay, Debug, Serialize, ToSchema, Clone, Deserialize)] +pub struct PayoutIndividualDetails { + pub tos_acceptance_date: Option, + pub tos_acceptance_ip: Option>, + pub individual_dob_day: Option>, + pub individual_dob_month: Option>, + pub individual_dob_year: Option>, + pub individual_id_number: Option>, + pub individual_ssn_last_4: Option>, + pub external_account_account_holder_type: Option, +} diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 39e18483963e..8d7467d91c2e 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1668,15 +1668,19 @@ pub enum CanadaStatesAbbreviation { #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PayoutStatus { + RequiresCustomerAction, + RequiresPayoutMethodData, + #[default] + RequiresCreation, + RequiresFulfillment, + Processing, + OutgoingPaymentSent, Success, Failed, Cancelled, - Pending, Ineligible, - #[default] - RequiresCreation, - RequiresPayoutMethodData, - RequiresFulfillment, + Expired, + FundsRefunded, } #[derive( diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 2f517295ae48..ab0fbcd38873 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -24,6 +24,8 @@ pub const PAYMENTS_LIST_MAX_LIMIT_V1: u32 = 100; /// Maximum limit for payments list post api with filters pub const PAYMENTS_LIST_MAX_LIMIT_V2: u32 = 20; +/// Average delay (in ms) between account onboarding's API response and the changes to actually reflect at Stripe's end +pub const STRIPE_ACCOUNT_ONBOARDING_DELAY_IN_SECONDS: i64 = 15; /// surcharge percentage maximum precision length pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index 64ba10551153..5dc92cee7453 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -27,6 +27,7 @@ pub struct PayoutAttempt { #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: PrimitiveDateTime, pub profile_id: Option, + pub confirm: bool, } impl Default for PayoutAttempt { @@ -51,6 +52,7 @@ impl Default for PayoutAttempt { created_at: now, last_modified_at: now, profile_id: None, + confirm: false, } } } @@ -88,6 +90,7 @@ pub struct PayoutAttemptNew { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub last_modified_at: Option, pub profile_id: Option, + pub confirm: bool, } #[derive(Debug)] diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 0da819f2a70c..306253aa57f4 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -689,6 +689,7 @@ diesel::table! { last_modified_at -> Timestamp, #[max_length = 64] profile_id -> Nullable, + confirm -> Bool, } } diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 09f23bc3b2f3..9187ea577a78 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -76,6 +76,8 @@ pub enum PTRunner { PaymentsSyncWorkflow, RefundWorkflowRouter, DeleteTokenizeDataWorkflow, + #[cfg(feature = "payouts")] + StripeAttachAccount, } #[derive(Debug, Copy, Clone)] @@ -100,6 +102,10 @@ impl ProcessTrackerWorkflows for WorkflowRunner { Some(PTRunner::DeleteTokenizeDataWorkflow) => { Box::new(workflows::tokenized_data::DeleteTokenizeDataWorkflow) } + #[cfg(feature = "payouts")] + Some(PTRunner::StripeAttachAccount) => Box::new( + workflows::stripe_attach_external_account::StripeAttachExternalAccountWorkflow, + ), _ => Err(ProcessTrackerError::UnexpectedFlow)?, }; let app_state = &state.clone(); diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 94c7f0c58778..a6709e6bc538 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -4097,10 +4097,8 @@ impl TryFrom> let status = payout_eligible.map_or( { response.result_code.map_or( - response - .response - .map(storage_enums::PayoutStatus::foreign_from), - |rc| Some(storage_enums::PayoutStatus::foreign_from(rc)), + response.response.map(storage_enums::PayoutStatus::from), + |rc| Some(storage_enums::PayoutStatus::from(rc)), ) }, |pe| { @@ -4117,6 +4115,7 @@ impl TryFrom> status, connector_payout_id: response.psp_reference, payout_eligible, + should_add_next_step_to_process_tracker: false, }), ..item.data }) @@ -4124,13 +4123,15 @@ impl TryFrom> } #[cfg(feature = "payouts")] -impl ForeignFrom for storage_enums::PayoutStatus { - fn foreign_from(adyen_status: AdyenStatus) -> Self { +impl From for storage_enums::PayoutStatus { + fn from(adyen_status: AdyenStatus) -> Self { match adyen_status { - AdyenStatus::Authorised | AdyenStatus::PayoutConfirmReceived => Self::Success, + AdyenStatus::Authorised | AdyenStatus::PayoutConfirmReceived => { + Self::OutgoingPaymentSent + } AdyenStatus::Cancelled | AdyenStatus::PayoutDeclineReceived => Self::Cancelled, AdyenStatus::Error => Self::Failed, - AdyenStatus::Pending => Self::Pending, + AdyenStatus::Pending => Self::Processing, AdyenStatus::PayoutSubmitReceived => Self::RequiresFulfillment, _ => Self::Ineligible, } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 734dca3016d8..c732c4ff1ba5 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -9,6 +9,8 @@ use router_env::{instrument, tracing}; use self::transformers as stripe; use super::utils::{self as connector_utils, RefundsRequestData}; +#[cfg(feature = "payouts")] +use super::utils::{PayoutsData, RouterData}; use crate::{ configs::settings, consts, @@ -24,7 +26,7 @@ use crate::{ }, types::{ self, - api::{self, ConnectorCommon}, + api::{self, ConnectorCommon, ConnectorCommonExt}, }, utils::{self, crypto, ByteSliceExt, BytesExt, OptionExt}, }; @@ -32,6 +34,25 @@ use crate::{ #[derive(Debug, Clone)] pub struct Stripe; +impl ConnectorCommonExt for Stripe +where + Self: services::ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + Self::common_get_content_type(self).to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + impl ConnectorCommon for Stripe { fn id(&self) -> &'static str { "stripe" @@ -58,6 +79,31 @@ impl ConnectorCommon for Stripe { format!("Bearer {}", auth.api_key.peek()).into_masked(), )]) } + + #[cfg(feature = "payouts")] + fn build_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: stripe::StripeConnectErrorResponse = res + .response + .parse_struct("StripeConnectErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(types::ErrorResponse { + status_code: res.status_code, + code: response + .error + .code + .clone() + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .error + .code + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.error.message, + }) + } } impl ConnectorValidation for Stripe { @@ -1992,3 +2038,437 @@ impl services::ConnectorRedirectResponse for Stripe { )) } } + +impl api::Payouts for Stripe {} +#[cfg(feature = "payouts")] +impl api::PayoutCancel for Stripe {} +#[cfg(feature = "payouts")] +impl api::PayoutCreate for Stripe {} +#[cfg(feature = "payouts")] +impl api::PayoutFulfill for Stripe {} +#[cfg(feature = "payouts")] +impl api::PayoutRecipient for Stripe {} +#[cfg(feature = "payouts")] +impl api::PayoutRecipientAccount for Stripe {} + +#[cfg(feature = "payouts")] +impl services::ConnectorIntegration + for Stripe +{ + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let transfer_id = req.request.get_transfer_id()?; + Ok(format!( + "{}v1/transfers/{}/reversals", + connectors.stripe.base_url, transfer_id + )) + } + + fn get_headers( + &self, + req: &types::PayoutsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, _connectors) + } + + fn get_request_body( + &self, + req: &types::RouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = stripe::StripeConnectReversalRequest::try_from(req)?; + let stripe_connect_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::url_encode, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(stripe_connect_req)) + } + + fn build_request( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PayoutCancelType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PayoutCancelType::get_headers(self, req, connectors)?) + .body(types::PayoutCancelType::get_request_body(self, req)?) + .build(); + + Ok(Some(request)) + } + + #[instrument(skip_all)] + fn handle_response( + &self, + data: &types::PayoutsRouterData, + res: types::Response, + ) -> CustomResult, errors::ConnectorError> { + let response: stripe::StripeConnectReversalResponse = res + .response + .parse_struct("StripeConnectReversalResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "payouts")] +impl services::ConnectorIntegration + for Stripe +{ + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}v1/transfers", connectors.stripe.base_url)) + } + + fn get_headers( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_request_body( + &self, + req: &types::PayoutsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = stripe::StripeConnectPayoutCreateRequest::try_from(req)?; + let stripe_connect_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::url_encode, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(stripe_connect_req)) + } + + fn build_request( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PayoutCreateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PayoutCreateType::get_headers(self, req, connectors)?) + .body(types::PayoutCreateType::get_request_body(self, req)?) + .build(); + + Ok(Some(request)) + } + + #[instrument(skip_all)] + fn handle_response( + &self, + data: &types::PayoutsRouterData, + res: types::Response, + ) -> CustomResult, errors::ConnectorError> { + let response: stripe::StripeConnectPayoutCreateResponse = res + .response + .parse_struct("StripeConnectPayoutCreateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "payouts")] +impl services::ConnectorIntegration + for Stripe +{ + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}v1/payouts", connectors.stripe.base_url,)) + } + + fn get_headers( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut headers = self.build_headers(req, connectors)?; + let customer_account = req.get_connector_customer_id()?; + let mut customer_account_header = vec![( + headers::STRIPE_COMPATIBLE_CONNECT_ACCOUNT.to_string(), + customer_account.into_masked(), + )]; + headers.append(&mut customer_account_header); + Ok(headers) + } + + fn get_request_body( + &self, + req: &types::PayoutsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = stripe::StripeConnectPayoutFulfillRequest::try_from(req)?; + let stripe_connect_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::url_encode, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(stripe_connect_req)) + } + + fn build_request( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PayoutFulfillType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PayoutFulfillType::get_headers( + self, req, connectors, + )?) + .body(types::PayoutFulfillType::get_request_body(self, req)?) + .build(); + + Ok(Some(request)) + } + + #[instrument(skip_all)] + fn handle_response( + &self, + data: &types::PayoutsRouterData, + res: types::Response, + ) -> CustomResult, errors::ConnectorError> { + let response: stripe::StripeConnectPayoutFulfillResponse = res + .response + .parse_struct("StripeConnectPayoutFulfillResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +#[cfg(feature = "payouts")] +impl + services::ConnectorIntegration + for Stripe +{ + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}v1/accounts", connectors.stripe.base_url)) + } + + fn get_headers( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_request_body( + &self, + req: &types::PayoutsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = stripe::StripeConnectRecipientCreateRequest::try_from(req)?; + let wise_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::url_encode, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(wise_req)) + } + + fn build_request( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PayoutRecipientType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PayoutRecipientType::get_headers( + self, req, connectors, + )?) + .body(types::PayoutRecipientType::get_request_body(self, req)?) + .build(); + + Ok(Some(request)) + } + + #[instrument(skip_all)] + fn handle_response( + &self, + data: &types::PayoutsRouterData, + res: types::Response, + ) -> CustomResult, errors::ConnectorError> { + let response: stripe::StripeConnectRecipientCreateResponse = res + .response + .parse_struct("StripeConnectRecipientCreateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "payouts")] +impl + services::ConnectorIntegration< + api::PoRecipientAccount, + types::PayoutsData, + types::PayoutsResponseData, + > for Stripe +{ + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_customer_id = req.get_connector_customer_id()?; + Ok(format!( + "{}v1/accounts/{}/external_accounts", + connectors.stripe.base_url, connector_customer_id + )) + } + + fn get_headers( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_request_body( + &self, + req: &types::PayoutsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = stripe::StripeConnectRecipientAccountCreateRequest::try_from(req)?; + let wise_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::url_encode, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(wise_req)) + } + + fn build_request( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PayoutRecipientAccountType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PayoutRecipientAccountType::get_headers( + self, req, connectors, + )?) + .body(types::PayoutRecipientAccountType::get_request_body( + self, req, + )?) + .build(); + + Ok(Some(request)) + } + + #[instrument(skip_all)] + fn handle_response( + &self, + data: &types::PayoutsRouterData, + res: types::Response, + ) -> CustomResult, errors::ConnectorError> + { + let response: stripe::StripeConnectRecipientAccountCreateResponse = res + .response + .parse_struct("StripeConnectRecipientAccountCreateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index a3711a5497c1..f80c4b79fe52 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::ops::Deref; use api_models::{self, enums as api_enums, payments}; @@ -26,13 +28,20 @@ use crate::{ }, utils::{self, OptionExt}, }; +#[cfg(feature = "payouts")] +use crate::{ + connector::utils::PayoutsData, core::payments::CustomerDetailsExt, + types::PayoutIndividualDetailsExt, +}; pub struct StripeAuthType { pub(super) api_key: Secret, } +type Error = error_stack::Report; + impl TryFrom<&types::ConnectorAuthType> for StripeAuthType { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::ConnectorAuthType) -> Result { if let types::ConnectorAuthType::HeaderKey { api_key } = item { Ok(Self { @@ -583,7 +592,6 @@ pub enum StripePaymentMethodType { #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] -#[allow(dead_code)] pub enum StripeCreditTransferTypes { AchCreditTransfer, Multibanco, @@ -591,7 +599,7 @@ pub enum StripeCreditTransferTypes { } impl TryFrom for StripePaymentMethodType { - type Error = error_stack::Report; + type Error = Error; fn try_from(value: enums::PaymentMethodType) -> Result { match value { enums::PaymentMethodType::Credit => Ok(Self::Card), @@ -1670,7 +1678,7 @@ impl TryFrom<&payments::BankRedirectData> for StripePaymentMethodData { } impl TryFrom<&payments::GooglePayWalletData> for StripePaymentMethodData { - type Error = error_stack::Report; + type Error = Error; fn try_from(gpay_data: &payments::GooglePayWalletData) -> Result { Ok(Self::Wallet(StripeWallet::GooglepayToken(GooglePayToken { token: Secret::new( @@ -1688,7 +1696,7 @@ impl TryFrom<&payments::GooglePayWalletData> for StripePaymentMethodData { } impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { let order_id = item.connector_request_reference_id.clone(); @@ -1960,7 +1968,7 @@ impl TryFrom<&types::SetupMandateRouterData> for SetupIntentRequest { } impl TryFrom<&types::TokenizationRouterData> for TokenRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::TokenizationRouterData) -> Result { let payment_data = create_stripe_payment_method( &item.request.payment_method_data, @@ -1974,7 +1982,7 @@ impl TryFrom<&types::TokenizationRouterData> for TokenRequest { } impl TryFrom<&types::ConnectorCustomerRouterData> for CustomerRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::ConnectorCustomerRouterData) -> Result { Ok(Self { description: item.request.description.to_owned(), @@ -2294,7 +2302,7 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData, ) -> Result { @@ -2401,7 +2409,7 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData< F, @@ -2495,7 +2503,7 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData, ) -> Result { @@ -2682,7 +2690,7 @@ pub struct RefundRequest { } impl TryFrom<&types::RefundsRouterData> for RefundRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::RefundsRouterData) -> Result { let amount = item.request.refund_amount; let payment_intent = item.request.connector_transaction_id.clone(); @@ -2734,7 +2742,7 @@ pub struct RefundResponse { impl TryFrom> for types::RefundsRouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::RefundsResponseRouterData, ) -> Result { @@ -2751,7 +2759,7 @@ impl TryFrom> impl TryFrom> for types::RefundsRouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::RefundsResponseRouterData, ) -> Result { @@ -2832,7 +2840,7 @@ pub struct CancelRequest { } impl TryFrom<&types::PaymentsCancelRouterData> for CancelRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::PaymentsCancelRouterData) -> Result { Ok(Self { cancellation_reason: item.request.cancellation_reason.clone(), @@ -2904,7 +2912,7 @@ pub struct CaptureRequest { } impl TryFrom<&types::PaymentsCaptureRouterData> for CaptureRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { Ok(Self { amount_to_capture: Some(item.request.amount_to_capture), @@ -2913,7 +2921,7 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for CaptureRequest { } impl TryFrom<&types::PaymentsPreProcessingRouterData> for StripeCreditTransferSourceRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { let currency = item.request.get_currency()?; @@ -2984,7 +2992,7 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData, ) -> Result { @@ -3018,7 +3026,7 @@ impl } impl TryFrom<&types::PaymentsAuthorizeRouterData> for ChargesRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(value: &types::PaymentsAuthorizeRouterData) -> Result { Ok(Self { @@ -3033,7 +3041,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for ChargesRequest { impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData, ) -> Result { @@ -3062,7 +3070,7 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData, ) -> Result { @@ -3079,7 +3087,7 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData, ) -> Result { @@ -3302,7 +3310,7 @@ impl StripePaymentMethodType, )> for StripePaymentMethodData { - type Error = error_stack::Report; + type Error = Error; fn try_from( (pm_data, auth_type, pm_type): ( api::PaymentMethodData, @@ -3541,7 +3549,7 @@ pub struct Evidence { } impl TryFrom<&types::SubmitEvidenceRouterData> for Evidence { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &types::SubmitEvidenceRouterData) -> Result { let submit_evidence_request_data = item.request.clone(); Ok(Self { @@ -3773,3 +3781,570 @@ mod test_validate_shipping_address_against_payment_method { } } } + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum StripeConnectPayoutStatus { + Canceled, + Failed, + InTransit, + Paid, + Pending, +} + +#[cfg(feature = "payouts")] +#[derive(Debug, Deserialize, Serialize)] +pub struct StripeConnectErrorResponse { + pub error: ErrorDetails, +} + +// Payouts +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Serialize)] +pub struct StripeConnectPayoutCreateRequest { + amount: i64, + currency: enums::Currency, + destination: String, + transfer_group: String, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Deserialize)] +pub struct StripeConnectPayoutCreateResponse { + id: String, + object: String, + amount: i64, + amount_reversed: i64, + balance_transaction: String, + created: i32, + currency: String, + description: Option, + destination: String, + destination_payment: String, + livemode: bool, + reversals: TransferReversals, + reversed: bool, + source_transaction: Option, + source_type: String, + transfer_group: String, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Deserialize)] +pub struct TransferReversals { + object: String, + has_more: bool, + total_count: i32, + url: String, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Serialize)] +pub struct StripeConnectPayoutFulfillRequest { + amount: i64, + currency: enums::Currency, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Deserialize)] +pub struct StripeConnectPayoutFulfillResponse { + id: String, + object: String, + amount: i64, + arrival_date: i32, + automatic: bool, + balance_transaction: String, + created: i32, + currency: String, + description: Option, + destination: String, + failure_balance_transaction: Option, + failure_code: Option, + failure_message: Option, + livemode: bool, + method: String, + original_payout: Option, + reconciliation_status: String, + reversed_by: Option, + source_type: String, + statement_descriptor: Option, + status: StripeConnectPayoutStatus, + #[serde(rename = "type")] + account_type: String, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Serialize)] +pub struct StripeConnectReversalRequest { + amount: i64, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Deserialize)] +pub struct StripeConnectReversalResponse { + id: String, + object: String, + amount: i64, + balance_transaction: String, + created: i32, + currency: String, + destination_payment_refund: String, + source_refund: Option, + transfer: String, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Serialize)] +pub struct StripeConnectRecipientCreateRequest { + #[serde(rename = "type")] + account_type: String, + country: Option, + email: Option, + #[serde(rename = "capabilities[card_payments][requested]")] + capabilities_card_payments: Option, + #[serde(rename = "capabilities[transfers][requested]")] + capabilities_transfers: Option, + #[serde(rename = "tos_acceptance[date]")] + tos_acceptance_date: Option, + #[serde(rename = "tos_acceptance[ip]")] + tos_acceptance_ip: Option>, + business_type: String, + #[serde(rename = "business_profile[mcc]")] + business_profile_mcc: Option, + #[serde(rename = "business_profile[url]")] + business_profile_url: Option, + #[serde(rename = "business_profile[name]")] + business_profile_name: Option>, + #[serde(rename = "company[address][line1]")] + company_address_line1: Option>, + #[serde(rename = "company[address][line2]")] + company_address_line2: Option>, + #[serde(rename = "company[address][postal_code]")] + company_address_postal_code: Option>, + #[serde(rename = "company[address][city]")] + company_address_city: Option>, + #[serde(rename = "company[address][state]")] + company_address_state: Option>, + #[serde(rename = "company[phone]")] + company_phone: Option>, + #[serde(rename = "company[tax_id]")] + company_tax_id: Option>, + #[serde(rename = "company[owners_provided]")] + company_owners_provided: Option, + #[serde(rename = "individual[first_name]")] + individual_first_name: Option>, + #[serde(rename = "individual[last_name]")] + individual_last_name: Option>, + #[serde(rename = "individual[dob][day]")] + individual_dob_day: Option>, + #[serde(rename = "individual[dob][month]")] + individual_dob_month: Option>, + #[serde(rename = "individual[dob][year]")] + individual_dob_year: Option>, + #[serde(rename = "individual[address][line1]")] + individual_address_line1: Option>, + #[serde(rename = "individual[address][line2]")] + individual_address_line2: Option>, + #[serde(rename = "individual[address][postal_code]")] + individual_address_postal_code: Option>, + #[serde(rename = "individual[address][city]")] + individual_address_city: Option, + #[serde(rename = "individual[address][state]")] + individual_address_state: Option>, + #[serde(rename = "individual[email]")] + individual_email: Option, + #[serde(rename = "individual[phone]")] + individual_phone: Option>, + #[serde(rename = "individual[id_number]")] + individual_id_number: Option>, + #[serde(rename = "individual[ssn_last_4]")] + individual_ssn_last_4: Option>, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Default, Deserialize)] +pub struct StripeConnectRecipientCreateResponse { + id: String, + object: String, + business_type: String, + charges_enabled: bool, + country: enums::CountryAlpha2, + created: i32, + default_currency: String, + email: Email, + payouts_enabled: bool, + #[serde(rename = "type")] + account_type: String, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +pub enum StripeConnectRecipientAccountCreateRequest { + Bank(RecipientBankAccountRequest), + Card(RecipientCardAccountRequest), + Token(RecipientTokenRequest), +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Serialize)] +pub struct RecipientTokenRequest { + external_account: String, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Serialize)] +pub struct RecipientCardAccountRequest { + #[serde(rename = "external_account[object]")] + external_account_object: String, + #[serde(rename = "external_account[number]")] + external_account_number: Secret, + #[serde(rename = "external_account[exp_month]")] + external_account_exp_month: Secret, + #[serde(rename = "external_account[exp_year]")] + external_account_exp_year: Secret, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Serialize)] +pub struct RecipientBankAccountRequest { + #[serde(rename = "external_account[object]")] + external_account_object: String, + #[serde(rename = "external_account[country]")] + external_account_country: enums::CountryAlpha2, + #[serde(rename = "external_account[currency]")] + external_account_currency: enums::Currency, + #[serde(rename = "external_account[account_holder_name]")] + external_account_account_holder_name: Secret, + #[serde(rename = "external_account[account_number]")] + external_account_account_number: Secret, + #[serde(rename = "external_account[account_holder_type]")] + external_account_account_holder_type: String, + #[serde(rename = "external_account[routing_number]")] + external_account_routing_number: Secret, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum StripeConnectRecipientAccountCreateResponse { + Bank(RecipientBankAccountResponse), + Card(RecipientCardAccountResponse), +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Default, Deserialize)] +pub struct RecipientBankAccountResponse { + id: String, + object: String, + account: String, + account_holder_name: String, + account_holder_type: String, + account_type: Option, + bank_name: String, + country: enums::CountryAlpha2, + currency: String, + default_for_currency: bool, + fingerprint: String, + last4: String, + routing_number: String, + status: String, +} + +#[cfg(feature = "payouts")] +#[derive(Clone, Debug, Default, Deserialize)] +pub struct RecipientCardAccountResponse { + id: String, + object: String, + account: String, + brand: String, + country: enums::CountryAlpha2, + currency: String, + default_for_currency: bool, + dynamic_last4: Option, + exp_month: i8, + exp_year: i8, + fingerprint: String, + funding: String, + last4: String, + name: String, + status: String, +} + +// Payouts create/transfer request transform +#[cfg(feature = "payouts")] +impl TryFrom<&types::PayoutsRouterData> for StripeConnectPayoutCreateRequest { + type Error = Error; + fn try_from(item: &types::PayoutsRouterData) -> Result { + let request = item.request.to_owned(); + let connector_customer_id = item.get_connector_customer_id()?; + Ok(Self { + amount: request.amount, + currency: request.destination_currency, + destination: connector_customer_id, + transfer_group: request.payout_id, + }) + } +} + +// Payouts create response transform +#[cfg(feature = "payouts")] +impl TryFrom> + for types::PayoutsRouterData +{ + type Error = Error; + fn try_from( + item: types::PayoutsResponseRouterData, + ) -> Result { + let response: StripeConnectPayoutCreateResponse = item.response; + + Ok(Self { + response: Ok(types::PayoutsResponseData { + status: Some(enums::PayoutStatus::RequiresFulfillment), + connector_payout_id: response.id, + 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 StripeConnectPayoutFulfillRequest { + type Error = Error; + fn try_from(item: &types::PayoutsRouterData) -> Result { + let request = item.request.to_owned(); + Ok(Self { + amount: request.amount, + currency: request.destination_currency, + }) + } +} + +// Payouts fulfill response transform +#[cfg(feature = "payouts")] +impl TryFrom> + for types::PayoutsRouterData +{ + type Error = Error; + fn try_from( + item: types::PayoutsResponseRouterData, + ) -> Result { + let response: StripeConnectPayoutFulfillResponse = item.response; + + Ok(Self { + response: Ok(types::PayoutsResponseData { + status: Some(enums::PayoutStatus::from(response.status)), + connector_payout_id: response.id, + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + }), + ..item.data + }) + } +} + +// Payouts reversal request transform +#[cfg(feature = "payouts")] +impl TryFrom<&types::PayoutsRouterData> for StripeConnectReversalRequest { + type Error = Error; + fn try_from(item: &types::PayoutsRouterData) -> Result { + Ok(Self { + amount: item.request.amount, + }) + } +} + +// Payouts reversal response transform +#[cfg(feature = "payouts")] +impl TryFrom> + for types::PayoutsRouterData +{ + type Error = Error; + fn try_from( + item: types::PayoutsResponseRouterData, + ) -> Result { + let response: StripeConnectReversalResponse = item.response; + + Ok(Self { + response: Ok(types::PayoutsResponseData { + status: Some(enums::PayoutStatus::Cancelled), + connector_payout_id: response.id, + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + }), + ..item.data + }) + } +} + +// Recipient creation request transform +#[cfg(feature = "payouts")] +impl TryFrom<&types::PayoutsRouterData> for StripeConnectRecipientCreateRequest { + type Error = Error; + fn try_from(item: &types::PayoutsRouterData) -> Result { + let request = item.request.to_owned(); + let customer_details = request.get_customer_details()?; + let customer_email = customer_details.get_email()?; + let address = item.get_billing_address()?.clone(); + let payout_vendor_details = request.get_vendor_details()?; + let (vendor_details, individual_details) = ( + payout_vendor_details.vendor_details, + payout_vendor_details.individual_details, + ); + Ok(Self { + account_type: vendor_details.account_type, + country: address.country, + email: Some(customer_email.clone()), + capabilities_card_payments: vendor_details.capabilities_card_payments, + capabilities_transfers: vendor_details.capabilities_transfers, + tos_acceptance_date: individual_details.tos_acceptance_date, + tos_acceptance_ip: individual_details.tos_acceptance_ip, + business_type: vendor_details.business_type, + business_profile_mcc: vendor_details.business_profile_mcc, + business_profile_url: vendor_details.business_profile_url, + business_profile_name: vendor_details.business_profile_name, + company_address_line1: vendor_details.company_address_line1, + company_address_line2: vendor_details.company_address_line2, + company_address_postal_code: vendor_details.company_address_postal_code, + company_address_city: vendor_details.company_address_city, + company_address_state: vendor_details.company_address_state, + company_phone: vendor_details.company_phone, + company_tax_id: vendor_details.company_tax_id, + company_owners_provided: vendor_details.company_owners_provided, + individual_first_name: address.first_name, + individual_last_name: address.last_name, + individual_dob_day: individual_details.individual_dob_day, + individual_dob_month: individual_details.individual_dob_month, + individual_dob_year: individual_details.individual_dob_year, + individual_address_line1: address.line1, + individual_address_line2: address.line2, + individual_address_postal_code: address.zip, + individual_address_city: address.city, + individual_address_state: address.state, + individual_email: Some(customer_email), + individual_phone: customer_details.phone, + individual_id_number: individual_details.individual_id_number, + individual_ssn_last_4: individual_details.individual_ssn_last_4, + }) + } +} + +// Recipient creation response transform +#[cfg(feature = "payouts")] +impl TryFrom> + for types::PayoutsRouterData +{ + type Error = Error; + fn try_from( + item: types::PayoutsResponseRouterData, + ) -> Result { + let response: StripeConnectRecipientCreateResponse = item.response; + + Ok(Self { + response: Ok(types::PayoutsResponseData { + status: Some(enums::PayoutStatus::RequiresPayoutMethodData), + connector_payout_id: response.id, + payout_eligible: None, + should_add_next_step_to_process_tracker: true, + }), + ..item.data + }) + } +} + +// Recipient account's creation request +#[cfg(feature = "payouts")] +impl TryFrom<&types::PayoutsRouterData> for StripeConnectRecipientAccountCreateRequest { + type Error = Error; + fn try_from(item: &types::PayoutsRouterData) -> Result { + let request = item.request.to_owned(); + let payout_method_data = item.get_payout_method_data()?; + let customer_details = request.get_customer_details()?; + let customer_name = customer_details.get_name()?; + let payout_vendor_details = request.get_vendor_details()?; + match payout_method_data { + api_models::payouts::PayoutMethodData::Card(_c) => { + Ok(Self::Token(RecipientTokenRequest { + external_account: "tok_visa_debit".to_string(), + })) + } + api_models::payouts::PayoutMethodData::Bank(bank) => match bank { + api_models::payouts::Bank::Ach(bank_details) => { + Ok(Self::Bank(RecipientBankAccountRequest { + external_account_object: "bank_account".to_string(), + external_account_country: bank_details.bank_country_code, + external_account_currency: request.destination_currency.to_owned(), + external_account_account_holder_name: customer_name, + external_account_account_holder_type: payout_vendor_details + .individual_details + .get_external_account_account_holder_type()?, + external_account_account_number: bank_details.bank_account_number, + external_account_routing_number: bank_details.bank_routing_number, + })) + } + api_models::payouts::Bank::Bacs(_) => Err(errors::ConnectorError::NotSupported { + message: "BACS payouts are not supported".to_string(), + connector: "stripe", + } + .into()), + api_models::payouts::Bank::Sepa(_) => Err(errors::ConnectorError::NotSupported { + message: "SEPA payouts are not supported".to_string(), + connector: "stripe", + } + .into()), + }, + } + } +} + +// Recipient account's creation response +#[cfg(feature = "payouts")] +impl TryFrom> + for types::PayoutsRouterData +{ + type Error = Error; + fn try_from( + item: types::PayoutsResponseRouterData, + ) -> Result { + let response: StripeConnectRecipientAccountCreateResponse = item.response; + + match response { + StripeConnectRecipientAccountCreateResponse::Bank(bank_response) => Ok(Self { + response: Ok(types::PayoutsResponseData { + status: Some(enums::PayoutStatus::RequiresCreation), + connector_payout_id: bank_response.id, + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + }), + ..item.data + }), + StripeConnectRecipientAccountCreateResponse::Card(card_response) => Ok(Self { + response: Ok(types::PayoutsResponseData { + status: Some(enums::PayoutStatus::RequiresCreation), + connector_payout_id: card_response.id, + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + }), + ..item.data + }), + } + } +} + +#[cfg(feature = "payouts")] +impl From for enums::PayoutStatus { + fn from(stripe_connect_status: StripeConnectPayoutStatus) -> Self { + match stripe_connect_status { + StripeConnectPayoutStatus::Paid => Self::Success, + StripeConnectPayoutStatus::Failed => Self::Failed, + StripeConnectPayoutStatus::Canceled => Self::Cancelled, + StripeConnectPayoutStatus::Pending | StripeConnectPayoutStatus::InTransit => { + Self::Processing + } + } + } +} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 4f59c38ea7c9..baee03042c01 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +#[cfg(feature = "payouts")] +use api_models::payouts::PayoutVendorAccountDetails; use api_models::{ enums::{CanadaStatesAbbreviation, UsStatesAbbreviation}, payments::{self, BankDebitBilling, OrderDetailsWithAmount}, @@ -16,6 +18,8 @@ use masking::{ExposeInterface, Secret}; use once_cell::sync::Lazy; use regex::Regex; use serde::Serializer; +#[cfg(feature = "payouts")] +use types::CustomerDetails; use crate::{ consts, @@ -539,6 +543,32 @@ impl RefundsRequestData for types::RefundsData { } } +#[cfg(feature = "payouts")] +pub trait PayoutsData { + fn get_transfer_id(&self) -> Result; + fn get_customer_details(&self) -> Result; + fn get_vendor_details(&self) -> Result; +} + +#[cfg(feature = "payouts")] +impl PayoutsData for types::PayoutsData { + fn get_transfer_id(&self) -> Result { + self.connector_payout_id + .clone() + .ok_or_else(missing_field_err("transfer_id")) + } + fn get_customer_details(&self) -> Result { + self.customer_details + .clone() + .ok_or_else(missing_field_err("customer_details")) + } + fn get_vendor_details(&self) -> Result { + self.vendor_details + .clone() + .ok_or_else(missing_field_err("vendor_details")) + } +} + #[derive(Clone, Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GooglePayWalletData { diff --git a/crates/router/src/connector/wise/transformers.rs b/crates/router/src/connector/wise/transformers.rs index c481f0c73433..92e4a6bd7260 100644 --- a/crates/router/src/connector/wise/transformers.rs +++ b/crates/router/src/connector/wise/transformers.rs @@ -15,7 +15,6 @@ use crate::{ types::{ api::payouts, storage::enums::{self as storage_enums, PayoutEntityType}, - transformers::ForeignFrom, }, }; use crate::{core::errors, types}; @@ -323,7 +322,7 @@ fn get_payout_bank_details( }?; match payout_method_data { PayoutMethodData::Bank(payouts::BankPayout::Ach(b)) => Ok(WiseBankDetails { - legal_type: LegalType::foreign_from(entity_type), + legal_type: LegalType::from(entity_type), address: wise_address_details, account_number: Some(b.bank_account_number.to_owned()), abartn: Some(b.bank_routing_number), @@ -331,14 +330,14 @@ fn get_payout_bank_details( ..WiseBankDetails::default() }), PayoutMethodData::Bank(payouts::BankPayout::Bacs(b)) => Ok(WiseBankDetails { - legal_type: LegalType::foreign_from(entity_type), + legal_type: LegalType::from(entity_type), address: 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::foreign_from(entity_type), + legal_type: LegalType::from(entity_type), address: wise_address_details, iban: Some(b.iban.to_owned()), bic: b.bic, @@ -412,6 +411,7 @@ impl TryFrom status: Some(storage_enums::PayoutStatus::RequiresCreation), connector_payout_id: response.id.to_string(), payout_eligible: None, + should_add_next_step_to_process_tracker: false, }), ..item.data }) @@ -456,6 +456,7 @@ impl TryFrom> status: Some(storage_enums::PayoutStatus::RequiresCreation), connector_payout_id: response.id, payout_eligible: None, + should_add_next_step_to_process_tracker: false, }), ..item.data }) @@ -507,7 +508,7 @@ impl TryFrom> item: types::PayoutsResponseRouterData, ) -> Result { let response: WisePayoutResponse = item.response; - let status = match storage_enums::PayoutStatus::foreign_from(response.status) { + let status = match storage_enums::PayoutStatus::from(response.status) { storage_enums::PayoutStatus::Cancelled => storage_enums::PayoutStatus::Cancelled, _ => storage_enums::PayoutStatus::RequiresFulfillment, }; @@ -517,6 +518,7 @@ impl TryFrom> status: Some(status), connector_payout_id: response.id.to_string(), payout_eligible: None, + should_add_next_step_to_process_tracker: false, }), ..item.data }) @@ -554,9 +556,10 @@ impl TryFrom> Ok(Self { response: Ok(types::PayoutsResponseData { - status: Some(storage_enums::PayoutStatus::foreign_from(response.status)), + status: Some(storage_enums::PayoutStatus::from(response.status)), connector_payout_id: "".to_string(), payout_eligible: None, + should_add_next_step_to_process_tracker: false, }), ..item.data }) @@ -564,22 +567,22 @@ impl TryFrom> } #[cfg(feature = "payouts")] -impl ForeignFrom for storage_enums::PayoutStatus { - fn foreign_from(wise_status: WiseStatus) -> Self { +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 - } + WiseStatus::Completed + | WiseStatus::Pending + | WiseStatus::Processing + | WiseStatus::IncomingPaymentWaiting => Self::OutgoingPaymentSent, } } } #[cfg(feature = "payouts")] -impl ForeignFrom for LegalType { - fn foreign_from(entity_type: PayoutEntityType) -> Self { +impl From for LegalType { + fn from(entity_type: PayoutEntityType) -> Self { match entity_type { PayoutEntityType::Individual | PayoutEntityType::Personal diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 14b97fec42e4..777fc978e5ef 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -39,6 +39,7 @@ use self::{ use super::errors::StorageErrorExt; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, + connector::utils::missing_field_err, core::{ errors::{self, CustomResult, RouterResponse, RouterResult}, payment_methods::PaymentMethodRetrieve, @@ -1502,6 +1503,22 @@ pub struct CustomerDetails { pub phone_country_code: Option, } +pub trait CustomerDetailsExt { + type Error; + fn get_name(&self) -> Result, Self::Error>; + fn get_email(&self) -> Result; +} + +impl CustomerDetailsExt for CustomerDetails { + type Error = error_stack::Report; + fn get_name(&self) -> Result, Self::Error> { + self.name.clone().ok_or_else(missing_field_err("name")) + } + fn get_email(&self) -> Result { + self.email.clone().ok_or_else(missing_field_err("email")) + } +} + pub fn if_not_create_change_operation<'a, Op, F, Ctx>( status: storage_enums::IntentStatus, confirm: Option, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index b54f1c6cd527..9f0ae104f02c 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -911,7 +911,6 @@ default_imp_for_payouts!( connector::Rapyd, connector::Square, connector::Stax, - connector::Stripe, connector::Shift4, connector::Trustpay, connector::Tsys, @@ -988,7 +987,6 @@ default_imp_for_payouts_create!( connector::Rapyd, connector::Square, connector::Stax, - connector::Stripe, connector::Shift4, connector::Trustpay, connector::Tsys, @@ -1145,7 +1143,6 @@ default_imp_for_payouts_fulfill!( connector::Rapyd, connector::Square, connector::Stax, - connector::Stripe, connector::Shift4, connector::Trustpay, connector::Tsys, @@ -1222,7 +1219,6 @@ default_imp_for_payouts_cancel!( connector::Rapyd, connector::Square, connector::Stax, - connector::Stripe, connector::Shift4, connector::Trustpay, connector::Tsys, @@ -1378,7 +1374,6 @@ default_imp_for_payouts_recipient!( connector::Rapyd, connector::Square, connector::Stax, - connector::Stripe, connector::Shift4, connector::Trustpay, connector::Tsys, @@ -1397,12 +1392,11 @@ macro_rules! default_imp_for_approve { api::Approve, types::PaymentsApproveData, types::PaymentsResponseData, - > for $path::$connector - {} - )* - }; -} - + > for $path::$connector + {} + )* + }; + } #[cfg(feature = "dummy_connector")] impl api::PaymentApprove for connector::DummyConnector {} #[cfg(feature = "dummy_connector")] @@ -1414,7 +1408,6 @@ impl > for connector::DummyConnector { } - default_imp_for_approve!( connector::Aci, connector::Adyen, @@ -1467,33 +1460,115 @@ default_imp_for_approve!( ); macro_rules! default_imp_for_reject { + ($($path:ident::$connector:ident),*) => { + $( + impl api::PaymentReject for $path::$connector {} + impl + services::ConnectorIntegration< + api::Reject, + types::PaymentsRejectData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; + } + +#[cfg(feature = "dummy_connector")] +impl api::PaymentReject for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::Reject, + types::PaymentsRejectData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_reject!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "payouts")] +macro_rules! default_imp_for_payouts_recipient_account { ($($path:ident::$connector:ident),*) => { $( - impl api::PaymentReject for $path::$connector {} + impl api::PayoutRecipientAccount for $path::$connector {} impl services::ConnectorIntegration< - api::Reject, - types::PaymentsRejectData, - types::PaymentsResponseData, + api::PoRecipientAccount, + types::PayoutsData, + types::PayoutsResponseData, > for $path::$connector {} )* }; } +#[cfg(feature = "payouts")] #[cfg(feature = "dummy_connector")] -impl api::PaymentReject for connector::DummyConnector {} +impl api::PayoutRecipientAccount for connector::DummyConnector {} +#[cfg(feature = "payouts")] #[cfg(feature = "dummy_connector")] impl services::ConnectorIntegration< - api::Reject, - types::PaymentsRejectData, - types::PaymentsResponseData, + api::PoRecipientAccount, + types::PayoutsData, + types::PayoutsResponseData, > for connector::DummyConnector { } -default_imp_for_reject!( +#[cfg(feature = "payouts")] +default_imp_for_payouts_recipient_account!( connector::Aci, connector::Adyen, connector::Airwallex, @@ -1533,7 +1608,6 @@ default_imp_for_reject!( connector::Rapyd, connector::Square, connector::Stax, - connector::Stripe, connector::Shift4, connector::Trustpay, connector::Tsys, diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index ddb2a017e35a..02d7fa3c2349 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -2,19 +2,26 @@ pub mod helpers; pub mod validator; use api_models::enums as api_enums; -use common_utils::{crypto::Encryptable, ext_traits::ValueExt}; +use common_utils::{consts, crypto::Encryptable, ext_traits::ValueExt}; use diesel_models::enums as storage_enums; -use error_stack::{report, ResultExt}; +use error_stack::{report, IntoReport, ResultExt}; use router_env::{instrument, tracing}; +use scheduler::{ + db::process_tracker::ProcessTrackerExt, errors::ProcessTrackerError, utils as pt_utils, +}; use serde_json; -use super::errors::{ConnectorErrorExt, StorageErrorExt}; +use super::{ + errors::{ConnectorErrorExt, StorageErrorExt}, + payments::customers, +}; use crate::{ core::{ errors::{self, RouterResponse, RouterResult}, payments::{self, helpers as payment_helpers}, utils as core_utils, }, + db::StorageInterface, routes::AppState, services, types::{ @@ -35,6 +42,7 @@ pub struct PayoutData { pub payout_attempt: storage::PayoutAttempt, pub payout_method_data: Option, pub merchant_connector_account: Option, + pub should_bubble_out: bool, } // ********************************************** CORE FLOWS ********************************************** @@ -262,13 +270,7 @@ pub async fn payouts_retrieve_core( ) .await?; - response_handler( - &state, - &merchant_account, - &payouts::PayoutRequest::PayoutRetrieveRequest(req.to_owned()), - &payout_data, - ) - .await + response_handler(&merchant_account, &payout_data).await } #[cfg(feature = "payouts")] @@ -336,7 +338,6 @@ pub async fn payouts_cancel_core( &state, &merchant_account, &key_store, - &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), &connector_data, &mut payout_data, ) @@ -344,13 +345,7 @@ pub async fn payouts_cancel_core( .attach_printable("Payout cancellation failed for given Payout request")?; } - response_handler( - &state, - &merchant_account, - &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), - &payout_data, - ) - .await + response_handler(&merchant_account, &payout_data).await } #[cfg(feature = "payouts")] @@ -411,7 +406,6 @@ pub async fn payouts_fulfill_core( &state, &merchant_account, &key_store, - &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), &connector_data, &mut payout_data, ) @@ -426,13 +420,7 @@ pub async fn payouts_fulfill_core( })); } - response_handler( - &state, - &merchant_account, - &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), - &payout_data, - ) - .await + response_handler(&merchant_account, &payout_data).await } // ********************************************** HELPERS ********************************************** @@ -472,7 +460,6 @@ pub async fn call_connector_payout( state, merchant_account, key_store, - req, &connector_data, payout_data, ) @@ -502,7 +489,6 @@ pub async fn call_connector_payout( state, merchant_account, key_store, - req, &connector_data, payout_data, ) @@ -514,7 +500,6 @@ pub async fn call_connector_payout( state, merchant_account, key_store, - req, &connector_data, payout_data, ) @@ -524,13 +509,13 @@ pub async fn call_connector_payout( }; // Auto fulfillment flow - let status = payout_data.payout_attempt.status; - if payouts.auto_fulfill && status == storage_enums::PayoutStatus::RequiresFulfillment { + if payouts.auto_fulfill + && payout_data.payout_attempt.status == storage_enums::PayoutStatus::RequiresFulfillment + { *payout_data = fulfill_payout( state, merchant_account, key_store, - &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), &connector_data, payout_data, ) @@ -538,13 +523,7 @@ pub async fn call_connector_payout( .attach_printable("Payout fulfillment failed for given Payout request")?; } - response_handler( - state, - merchant_account, - &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), - payout_data, - ) - .await + response_handler(merchant_account, payout_data).await } #[cfg(feature = "payouts")] @@ -552,10 +531,12 @@ pub async fn create_recipient( state: &AppState, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - req: &payouts::PayoutCreateRequest, connector_data: &api::PayoutConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { + if payout_data.should_bubble_out { + return Ok(payout_data.to_owned()); + } let customer_details = payout_data.customer_details.to_owned(); let connector_name = connector_data.connector_name.to_string(); @@ -581,12 +562,11 @@ pub async fn create_recipient( ); if should_call_connector { // 1. Form router data - let customer_router_data = core_utils::construct_payout_router_data( + let mut router_data = core_utils::construct_payout_router_data( state, - &connector_name, + &connector_data.connector_name, merchant_account, key_store, - &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), payout_data, ) .await?; @@ -602,8 +582,8 @@ pub async fn create_recipient( // 3. Call connector service let router_resp = services::execute_connector_processing_step( state, - connector_integration, - &customer_router_data, + connector_integration.to_owned(), + &router_data, payments::CallConnectorAction::Trigger, None, ) @@ -612,26 +592,47 @@ pub async fn create_recipient( match router_resp.response { Ok(recipient_create_data) => { + // Execute post tasks + router_data.connector_customer = + Some(recipient_create_data.connector_payout_id.to_owned()); if let Some(customer) = customer_details { let db = &*state.store; let customer_id = customer.customer_id.to_owned(); let merchant_id = merchant_account.merchant_id.to_owned(); - let updated_customer = storage::CustomerUpdate::ConnectorCustomer { - connector_customer: Some( - serde_json::json!({connector_label: recipient_create_data.connector_payout_id}), - ), - }; - payout_data.customer_details = Some( - db.update_customer_by_customer_id_merchant_id( - customer_id, - merchant_id, - updated_customer, - key_store, + if let Some(updated_customer) = + customers::update_connector_customer_in_customers( + &connector_label, + Some(&customer), + &Some(recipient_create_data.connector_payout_id), ) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error updating customers in db")?, + { + payout_data.customer_details = Some( + db.update_customer_by_customer_id_merchant_id( + customer_id, + merchant_id, + updated_customer, + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating customers in db")?, + ) + } + } + // Add next step to ProcessTracker + if recipient_create_data.should_add_next_step_to_process_tracker { + add_external_account_addition_task( + &*state.store, + payout_data, + common_utils::date_time::now().saturating_add(time::Duration::seconds(consts::STRIPE_ACCOUNT_ONBOARDING_DELAY_IN_SECONDS)), ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while adding stripe_attach_external_account workflow to process tracker")?; + // Helps callee functions skip the execution + payout_data.should_bubble_out = true; } } Err(err) => Err(errors::ApiErrorResponse::PayoutFailed { @@ -647,17 +648,18 @@ pub async fn check_payout_eligibility( state: &AppState, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - req: &payouts::PayoutCreateRequest, connector_data: &api::PayoutConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { + if payout_data.should_bubble_out { + return Ok(payout_data.to_owned()); + } // 1. Form Router data let router_data = core_utils::construct_payout_router_data( state, - &connector_data.connector_name.to_string(), + &connector_data.connector_name, merchant_account, key_store, - &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), payout_data, ) .await?; @@ -747,17 +749,18 @@ pub async fn create_payout( state: &AppState, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - req: &payouts::PayoutCreateRequest, connector_data: &api::PayoutConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { + if payout_data.should_bubble_out { + return Ok(payout_data.to_owned()); + } // 1. Form Router data let mut router_data = core_utils::construct_payout_router_data( state, - &connector_data.connector_name.to_string(), + &connector_data.connector_name, merchant_account, key_store, - &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), payout_data, ) .await?; @@ -848,22 +851,116 @@ pub async fn create_payout( Ok(payout_data.clone()) } +#[cfg(feature = "payouts")] +pub async fn create_recipient_account( + state: &AppState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + connector_data: &api::PayoutConnectorData, + payout_data: &mut PayoutData, +) -> RouterResult { + if payout_data.should_bubble_out { + return Ok(payout_data.to_owned()); + } + // 1. Form Router data + let router_data = core_utils::construct_payout_router_data( + state, + &connector_data.connector_name, + merchant_account, + key_store, + payout_data, + ) + .await?; + + // 2. Fetch connector integration details + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::PoRecipientAccount, + types::PayoutsData, + types::PayoutsResponseData, + > = connector_data.connector.get_connector_integration(); + + // 3. Call connector service + let router_data_resp = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payout_failed_response()?; + + // 4. Process data returned by the connector + let db = &*state.store; + let merchant_id = &merchant_account.merchant_id; + let payout_id = &payout_data.payout_attempt.payout_id; + match router_data_resp.response { + Ok(payout_response_data) => { + let status = payout_response_data + .status + .unwrap_or(payout_data.payout_attempt.status.to_owned()); + let updated_payout_attempt = + storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: payout_response_data.connector_payout_id, + status, + error_code: None, + error_message: None, + is_eligible: payout_response_data.payout_eligible, + last_modified_at: Some(common_utils::date_time::now()), + }; + payout_data.payout_attempt = db + .update_payout_attempt_by_merchant_id_payout_id( + merchant_id, + payout_id, + updated_payout_attempt, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating payout_attempt in db")? + } + Err(err) => { + let updated_payout_attempt = + storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: String::default(), + status: storage_enums::PayoutStatus::Failed, + error_code: Some(err.code), + error_message: Some(err.message), + is_eligible: None, + last_modified_at: Some(common_utils::date_time::now()), + }; + payout_data.payout_attempt = db + .update_payout_attempt_by_merchant_id_payout_id( + merchant_id, + payout_id, + updated_payout_attempt, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating payout_attempt in db")? + } + }; + + Ok(payout_data.clone()) +} + #[cfg(feature = "payouts")] pub async fn cancel_payout( state: &AppState, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - req: &payouts::PayoutRequest, connector_data: &api::PayoutConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { + if payout_data.should_bubble_out { + return Ok(payout_data.to_owned()); + } // 1. Form Router data let router_data = core_utils::construct_payout_router_data( state, - &connector_data.connector_name.to_string(), + &connector_data.connector_name, merchant_account, key_store, - req, payout_data, ) .await?; @@ -916,7 +1013,7 @@ pub async fn cancel_payout( .attach_printable("Error updating payout_attempt in db")? } Err(err) => { - let updated_payouts_create = + let updated_payout_attempt = storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: String::default(), status: storage_enums::PayoutStatus::Failed, @@ -929,7 +1026,7 @@ pub async fn cancel_payout( .update_payout_attempt_by_merchant_id_payout_id( merchant_id, payout_id, - updated_payouts_create, + updated_payout_attempt, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -945,17 +1042,18 @@ pub async fn fulfill_payout( state: &AppState, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - req: &payouts::PayoutRequest, connector_data: &api::PayoutConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { + if payout_data.should_bubble_out { + return Ok(payout_data.to_owned()); + } // 1. Form Router data let router_data = core_utils::construct_payout_router_data( state, - &connector_data.connector_name.to_string(), + &connector_data.connector_name, merchant_account, key_store, - req, payout_data, ) .await?; @@ -1053,9 +1151,7 @@ pub async fn fulfill_payout( #[cfg(feature = "payouts")] pub async fn response_handler( - _state: &AppState, merchant_account: &domain::MerchantAccount, - _req: &payouts::PayoutRequest, payout_data: &PayoutData, ) -> RouterResponse { let payout_attempt = payout_data.payout_attempt.to_owned(); @@ -1246,6 +1342,7 @@ pub async fn payout_create_db_entries( .set_created_at(Some(common_utils::date_time::now())) .set_last_modified_at(Some(common_utils::date_time::now())) .set_profile_id(req.profile_id.to_owned()) + .set_confirm(req.confirm.unwrap_or(false)) .to_owned(); let payout_attempt = db .insert_payout_attempt(payout_attempt_req) @@ -1267,6 +1364,7 @@ pub async fn payout_create_db_entries( .cloned() .or(stored_payout_method_data.cloned()), merchant_connector_account: None, + should_bubble_out: false, }) } @@ -1323,5 +1421,35 @@ pub async fn make_payout_data( payout_attempt, payout_method_data: None, merchant_connector_account: None, + should_bubble_out: false, }) } + +pub async fn add_external_account_addition_task( + db: &dyn StorageInterface, + payout_data: &PayoutData, + schedule_time: time::PrimitiveDateTime, +) -> Result<(), ProcessTrackerError> { + let runner = "STRIPE_ATTACH_ACCOUNT"; + let task = "ATTACH_ACCOUNT_WORKFLOW"; + let process_tracker_id = pt_utils::get_process_tracker_id( + runner, + task, + &payout_data.payout_attempt.payout_attempt_id, + &payout_data.payout_attempt.merchant_id, + ); + let tracking_data = api::PayoutRetrieveRequest { + payout_id: payout_data.payouts.payout_id.to_owned(), + force_sync: None, + merchant_id: Some(payout_data.payouts.merchant_id.to_owned()), + }; + let process_tracker_entry = storage::ProcessTracker::make_process_tracker_new( + process_tracker_id, + task, + runner, + tracking_data, + schedule_time, + )?; + db.insert_process(process_tracker_entry).await?; + Ok(()) +} diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index f750b645d6c6..d9527f877445 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -427,17 +427,20 @@ pub fn should_call_payout_connector_create_customer<'a>( pub fn is_payout_initiated(status: api_enums::PayoutStatus) -> bool { matches!( status, - api_enums::PayoutStatus::Pending | api_enums::PayoutStatus::RequiresFulfillment + api_enums::PayoutStatus::OutgoingPaymentSent | api_enums::PayoutStatus::RequiresFulfillment ) } pub fn is_payout_terminal_state(status: api_enums::PayoutStatus) -> bool { - !matches!( + matches!( status, - api_enums::PayoutStatus::Pending - | api_enums::PayoutStatus::RequiresCreation - | api_enums::PayoutStatus::RequiresFulfillment - | api_enums::PayoutStatus::RequiresPayoutMethodData + api_enums::PayoutStatus::OutgoingPaymentSent + | api_enums::PayoutStatus::Success + | api_enums::PayoutStatus::Failed + | api_enums::PayoutStatus::Cancelled + | api_enums::PayoutStatus::Ineligible + | api_enums::PayoutStatus::Expired + | api_enums::PayoutStatus::FundsRefunded ) } @@ -447,13 +450,15 @@ pub fn is_payout_err_state(status: api_enums::PayoutStatus) -> bool { api_enums::PayoutStatus::Cancelled | api_enums::PayoutStatus::Failed | api_enums::PayoutStatus::Ineligible + | api_enums::PayoutStatus::Expired ) } pub fn is_eligible_for_local_payout_cancellation(status: api_enums::PayoutStatus) -> bool { matches!( status, - api_enums::PayoutStatus::RequiresCreation - | api_enums::PayoutStatus::RequiresPayoutMethodData, + api_enums::PayoutStatus::RequiresCustomerAction + | api_enums::PayoutStatus::RequiresPayoutMethodData + | api_enums::PayoutStatus::RequiresCreation ) } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 7790e2ac5b78..8e87f9594b41 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -2,9 +2,13 @@ use std::{marker::PhantomData, str::FromStr}; use api_models::enums::{DisputeStage, DisputeStatus}; #[cfg(feature = "payouts")] +use api_models::payouts::PayoutVendorAccountDetails; +#[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "payouts")] +use masking::PeekInterface; use router_env::{instrument, tracing}; use uuid::Uuid; @@ -29,6 +33,9 @@ use crate::{ pub const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW: &str = "irrelevant_connector_request_reference_id_in_dispute_flow"; +#[cfg(feature = "payouts")] +pub const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_PAYOUTS_FLOW: &str = + "irrelevant_connector_request_reference_id_in_payouts_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"; @@ -71,18 +78,16 @@ pub async fn get_mca_for_payout<'a>( } #[cfg(feature = "payouts")] -#[instrument(skip_all)] pub async fn construct_payout_router_data<'a, F>( state: &'a AppState, - connector_id: &str, + connector_name: &api_models::enums::PayoutConnectors, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - _request: &api_models::payouts::PayoutRequest, payout_data: &mut PayoutData, ) -> RouterResult> { let (merchant_connector_account, profile_id) = get_mca_for_payout( state, - connector_id, + &connector_name.to_string(), merchant_account, key_store, payout_data, @@ -132,12 +137,26 @@ pub async fn construct_payout_router_data<'a, F>( .and_then(|c| c.connector_customer.as_ref()) .and_then(|cc| cc.get(connector_label)) .and_then(|id| serde_json::from_value::(id.to_owned()).ok()); + let vendor_details: Option = match connector_name { + api_models::enums::PayoutConnectors::Adyen => None, + api_models::enums::PayoutConnectors::Stripe => { + payout_data.payouts.metadata.to_owned().and_then(|meta| { + let val = meta + .peek() + .to_owned() + .parse_value("PayoutVendorAccountDetails") + .ok(); + val + }) + } + api_models::enums::PayoutConnectors::Wise => None, + }; 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(), + connector: connector_name.to_string(), payment_id: "".to_string(), attempt_id: "".to_string(), status: enums::AttemptStatus::Failure, @@ -158,6 +177,7 @@ pub async fn construct_payout_router_data<'a, F>( source_currency: payouts.source_currency, entity_type: payouts.entity_type.to_owned(), payout_type: payouts.payout_type, + vendor_details, customer_details: customer_details .to_owned() .map(|c| payments::CustomerDetails { @@ -175,7 +195,7 @@ pub async fn construct_payout_router_data<'a, F>( payment_method_token: None, recurring_mandate_payment_data: None, preprocessing_id: None, - connector_request_reference_id: IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW + connector_request_reference_id: IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_PAYOUTS_FLOW .to_string(), payout_method_data: payout_data.payout_method_data.to_owned(), quote_id: None, diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 738646b2964b..3f0ae3f203be 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -66,6 +66,7 @@ pub mod headers { pub const X_WEBHOOK_SIGNATURE: &str = "X-Webhook-Signature-512"; pub const X_REQUEST_ID: &str = "X-Request-Id"; pub const STRIPE_COMPATIBLE_WEBHOOK_SIGNATURE: &str = "Stripe-Signature"; + pub const STRIPE_COMPATIBLE_CONNECT_ACCOUNT: &str = "Stripe-Account"; } pub mod pii { diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 15cf59aaf32d..452a7175ff7a 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -69,7 +69,8 @@ pub async fn payouts_retrieve( ) -> HttpResponse { let payout_retrieve_request = payout_types::PayoutRetrieveRequest { payout_id: path.into_inner(), - force_sync: query_params.force_sync, + force_sync: query_params.force_sync.to_owned(), + merchant_id: query_params.merchant_id.to_owned(), }; let flow = Flow::PayoutsRetrieve; api::server_wrap( diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 48e3697c6ba9..8f72b440ce9b 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -20,13 +20,16 @@ pub use api_models::{ pub use common_utils::request::RequestBody; use common_utils::{pii, pii::Email}; use data_models::mandates::MandateData; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{self, IntoReport, ResultExt}; use masking::Secret; use self::{api::payments, storage::enums as storage_enums}; pub use crate::core::payments::{CustomerDetails, PaymentAddress}; #[cfg(feature = "payouts")] -use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW; +use crate::{ + connector::utils::missing_field_err, + core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_PAYOUTS_FLOW, +}; use crate::{ core::{ errors::{self, RouterResult}, @@ -169,6 +172,9 @@ pub type PayoutFulfillType = pub type PayoutRecipientType = dyn services::ConnectorIntegration; #[cfg(feature = "payouts")] +pub type PayoutRecipientAccountType = + dyn services::ConnectorIntegration; +#[cfg(feature = "payouts")] pub type PayoutQuoteType = dyn services::ConnectorIntegration; @@ -284,7 +290,7 @@ pub struct RouterData { pub payout_method_data: Option, #[cfg(feature = "payouts")] - /// Contains payout method data + /// Contains payout's quote ID pub quote_id: Option, pub test_mode: Option, @@ -336,6 +342,23 @@ pub struct PayoutsData { pub payout_type: storage_enums::PayoutType, pub entity_type: storage_enums::PayoutEntityType, pub customer_details: Option, + pub vendor_details: Option, +} + +#[cfg(feature = "payouts")] +pub trait PayoutIndividualDetailsExt { + type Error; + fn get_external_account_account_holder_type(&self) -> Result; +} + +#[cfg(feature = "payouts")] +impl PayoutIndividualDetailsExt for api_models::payouts::PayoutIndividualDetails { + type Error = error_stack::Report; + fn get_external_account_account_holder_type(&self) -> Result { + self.external_account_account_holder_type + .clone() + .ok_or_else(missing_field_err("external_account_account_holder_type")) + } } #[cfg(feature = "payouts")] @@ -344,12 +367,7 @@ pub struct PayoutsResponseData { pub status: Option, pub connector_payout_id: String, pub payout_eligible: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct PayoutsFulfillResponseData { - pub status: Option, - pub reference_id: Option, + pub should_add_next_step_to_process_tracker: bool, } #[derive(Debug, Clone)] @@ -1167,7 +1185,7 @@ impl preprocessing_id: None, connector_customer: data.connector_customer.clone(), connector_request_reference_id: - IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW.to_string(), + IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_PAYOUTS_FLOW.to_string(), payout_method_data: data.payout_method_data.clone(), quote_id: data.quote_id.clone(), test_mode: data.test_mode, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 6c1790d84841..3bf819cfd215 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -262,6 +262,7 @@ impl PayoutConnectorData { match enums::PayoutConnectors::from_str(connector_name) { Ok(name) => match name { enums::PayoutConnectors::Adyen => Ok(Box::new(&connector::Adyen)), + enums::PayoutConnectors::Stripe => Ok(Box::new(&connector::Stripe)), enums::PayoutConnectors::Wise => Ok(Box::new(&connector::Wise)), }, Err(_) => Err(report!(errors::ConnectorError::InvalidConnectorName) diff --git a/crates/router/src/types/api/payouts.rs b/crates/router/src/types/api/payouts.rs index dd118b091599..0fcd1720b9d6 100644 --- a/crates/router/src/types/api/payouts.rs +++ b/crates/router/src/types/api/payouts.rs @@ -33,6 +33,10 @@ pub struct PoQuote; #[derive(Debug, Clone)] pub struct PoRecipient; +#[cfg(feature = "payouts")] +#[derive(Debug, Clone)] +pub struct PoRecipientAccount; + #[cfg(feature = "payouts")] pub trait PayoutCancel: api::ConnectorIntegration @@ -69,6 +73,12 @@ pub trait PayoutRecipient: { } +#[cfg(feature = "payouts")] +pub trait PayoutRecipientAccount: + api::ConnectorIntegration +{ +} + #[cfg(feature = "payouts")] pub trait Payouts: ConnectorCommon @@ -78,6 +88,7 @@ pub trait Payouts: + PayoutFulfill + PayoutQuote + PayoutRecipient + + PayoutRecipientAccount { } #[cfg(not(feature = "payouts"))] diff --git a/crates/router/src/workflows.rs b/crates/router/src/workflows.rs index b036193bb270..d62490fe5d9f 100644 --- a/crates/router/src/workflows.rs +++ b/crates/router/src/workflows.rs @@ -1,3 +1,5 @@ pub mod payment_sync; pub mod refund_router; +#[cfg(feature = "payouts")] +pub mod stripe_attach_external_account; pub mod tokenized_data; diff --git a/crates/router/src/workflows/stripe_attach_external_account.rs b/crates/router/src/workflows/stripe_attach_external_account.rs new file mode 100644 index 000000000000..99baea73e6ae --- /dev/null +++ b/crates/router/src/workflows/stripe_attach_external_account.rs @@ -0,0 +1,126 @@ +use common_utils::ext_traits::{OptionExt, ValueExt}; +use diesel_models::enums; +use scheduler::{ + consumer::{self, workflows::ProcessTrackerWorkflow}, + errors, +}; + +use crate::{ + core::payouts as payouts_core, + errors as core_errors, + routes::AppState, + types::{api, storage}, +}; + +pub struct StripeAttachExternalAccountWorkflow; + +#[async_trait::async_trait] +impl ProcessTrackerWorkflow for StripeAttachExternalAccountWorkflow { + async fn execute_workflow<'a>( + &'a self, + state: &'a AppState, + process: storage::ProcessTracker, + ) -> Result<(), errors::ProcessTrackerError> { + // Gather context + let db = &*state.store; + let tracking_data: api::PayoutRetrieveRequest = process + .tracking_data + .clone() + .parse_value("PayoutRetrieveRequest")?; + + let merchant_id = tracking_data + .merchant_id + .clone() + .get_required_value("merchant_id")?; + + let key_store = db + .get_merchant_key_store_by_merchant_id( + merchant_id.as_ref(), + &db.get_master_key().to_vec().into(), + ) + .await?; + + let merchant_account = db + .find_merchant_account_by_merchant_id(&merchant_id, &key_store) + .await?; + + let request = api::payouts::PayoutRequest::PayoutRetrieveRequest(tracking_data); + + let mut payout_data = + payouts_core::make_payout_data(state, &merchant_account, &key_store, &request).await?; + let (payout_attempt, payouts) = ( + payout_data.payout_attempt.clone(), + payout_data.payouts.clone(), + ); + + // Fetch payout method data + payout_data.payout_method_data = Some( + payouts_core::helpers::make_payout_method_data( + state, + None, + payout_attempt.payout_token.as_deref(), + &payout_attempt.customer_id, + &payout_attempt.merchant_id, + &payout_attempt.payout_id, + Some(&payouts.payout_type), + ) + .await? + .get_required_value("payout_method_data")?, + ); + + // Form connector data + let routed_through = Some(payout_attempt.connector.to_owned()); + let connector_data = + payouts_core::get_connector_data(state, &merchant_account, routed_through, None) + .await?; + + // 1. Attach recipient's external accounts + payout_data = payouts_core::create_recipient_account( + state, + &merchant_account, + &key_store, + &connector_data, + &mut payout_data, + ) + .await?; + + if payout_attempt.confirm + && payout_data.payout_attempt.status == enums::PayoutStatus::RequiresCreation + { + // 2. Create payout + payout_data = payouts_core::create_payout( + state, + &merchant_account, + &key_store, + &connector_data, + &mut payout_data, + ) + .await?; + } + + if payouts.auto_fulfill + && payout_data.payout_attempt.status == enums::PayoutStatus::RequiresFulfillment + { + // 3. Fulfill payout + payouts_core::fulfill_payout( + state, + &merchant_account, + &key_store, + &connector_data, + &mut payout_data, + ) + .await?; + } + + Ok(()) + } + + async fn error_handler<'a>( + &'a self, + state: &'a AppState, + process: storage::ProcessTracker, + error: errors::ProcessTrackerError, + ) -> core_errors::CustomResult<(), errors::ProcessTrackerError> { + consumer::consumer_error_handler(state.store.as_scheduler(), process, error).await + } +} diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 7d600d98d3e4..5c8191ec1804 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -449,6 +449,7 @@ pub trait ConnectorActions: Connector { phone: Some(Secret::new("620874518".to_string())), phone_country_code: Some("+31".to_string()), }), + vendor_details: None, }, payment_info, ) diff --git a/migrations/2023-08-31-084553_alter_payout_status/down.sql b/migrations/2023-08-31-084553_alter_payout_status/down.sql new file mode 100644 index 000000000000..6f266be9fa02 --- /dev/null +++ b/migrations/2023-08-31-084553_alter_payout_status/down.sql @@ -0,0 +1,41 @@ +DELETE FROM pg_enum +WHERE + enumlabel = 'requires_customer_action' + AND enumtypid = ( + SELECT oid + FROM pg_type + WHERE + typname = 'PayoutStatus' + ); + +DELETE FROM pg_enum +WHERE + enumlabel = 'outgoing_payment_sent' + AND enumtypid = ( + SELECT oid + FROM pg_type + WHERE + typname = 'PayoutStatus' + ); + +DELETE FROM pg_enum +WHERE + enumlabel = 'funds_refunded' + AND enumtypid = ( + SELECT oid + FROM pg_type + WHERE + typname = 'PayoutStatus' + ); + +DELETE FROM pg_enum +WHERE + enumlabel = 'expired' + AND enumtypid = ( + SELECT oid + FROM pg_type + WHERE + typname = 'PayoutStatus' + ); + +ALTER TYPE "PayoutStatus" RENAME VALUE 'processing' TO 'pending'; \ No newline at end of file diff --git a/migrations/2023-08-31-084553_alter_payout_status/up.sql b/migrations/2023-08-31-084553_alter_payout_status/up.sql new file mode 100644 index 000000000000..3a5b1eec030b --- /dev/null +++ b/migrations/2023-08-31-084553_alter_payout_status/up.sql @@ -0,0 +1,13 @@ +ALTER TYPE "PayoutStatus" +ADD + VALUE IF NOT EXISTS 'requires_customer_action'; + +ALTER TYPE "PayoutStatus" +ADD + VALUE IF NOT EXISTS 'outgoing_payment_sent'; + +ALTER TYPE "PayoutStatus" ADD VALUE IF NOT EXISTS 'funds_refunded'; + +ALTER TYPE "PayoutStatus" ADD VALUE IF NOT EXISTS 'expired'; + +ALTER TYPE "PayoutStatus" RENAME VALUE 'pending' TO 'processing'; \ No newline at end of file diff --git a/migrations/2023-10-12-070914_add_confirm_to_payout_attempt/down.sql b/migrations/2023-10-12-070914_add_confirm_to_payout_attempt/down.sql new file mode 100644 index 000000000000..2a7a9980b9c5 --- /dev/null +++ b/migrations/2023-10-12-070914_add_confirm_to_payout_attempt/down.sql @@ -0,0 +1 @@ +ALTER TABLE PAYOUT_ATTEMPT DROP COLUMN IF EXISTS confirm; \ No newline at end of file diff --git a/migrations/2023-10-12-070914_add_confirm_to_payout_attempt/up.sql b/migrations/2023-10-12-070914_add_confirm_to_payout_attempt/up.sql new file mode 100644 index 000000000000..fc97e1e3611d --- /dev/null +++ b/migrations/2023-10-12-070914_add_confirm_to_payout_attempt/up.sql @@ -0,0 +1 @@ +ALTER TABLE PAYOUT_ATTEMPT ADD COLUMN confirm BOOLEAN NOT NULL; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 27b73f1c097d..89fe94fbffc2 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9680,6 +9680,7 @@ "type": "string", "enum": [ "adyen", + "stripe", "wise" ] }, @@ -10089,6 +10090,10 @@ "force_sync": { "type": "boolean", "nullable": true + }, + "merchant_id": { + "type": "string", + "nullable": true } } }, @@ -10111,20 +10116,29 @@ "default": false, "example": true, "nullable": true + }, + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account.", + "nullable": true } } }, "PayoutStatus": { "type": "string", "enum": [ + "requires_customer_action", + "requires_payout_method_data", + "requires_creation", + "requires_fulfillment", + "processing", + "outgoing_payment_sent", "success", "failed", "cancelled", - "pending", "ineligible", - "requires_creation", - "requires_payout_method_data", - "requires_fulfillment" + "expired", + "funds_refunded" ] }, "PayoutType": {