Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/api_models/src/errors/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ pub struct Extra {
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connector_transaction_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}

#[derive(Serialize, Debug, Clone)]
Expand Down
5 changes: 2 additions & 3 deletions crates/common_types/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::domain::{AdyenSplitData, XenditSplitSubMerchantData};

#[derive(
Serialize, Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, ToSchema,
)]
Expand Down Expand Up @@ -44,7 +43,7 @@ pub struct StripeSplitPaymentRequest {

/// Platform fees to be collected on the payment
#[schema(value_type = i64, example = 6540)]
pub application_fees: MinorUnit,
pub application_fees: Option<MinorUnit>,

/// Identifier for the reseller's account where the funds were transferred
pub transfer_account_id: String,
Expand Down Expand Up @@ -139,7 +138,7 @@ pub struct StripeChargeResponseData {

/// Platform fees collected on the payment
#[schema(value_type = i64, example = 6540)]
pub application_fees: MinorUnit,
pub application_fees: Option<MinorUnit>,

/// Identifier for the reseller's account where the funds were transferred
pub transfer_account_id: String,
Expand Down
14 changes: 14 additions & 0 deletions crates/hyperswitch_connectors/src/connectors/stripe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,9 +870,13 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
.to_string()
.into(),
)];

let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);

let stripe_split_payment_metadata = stripe::StripeSplitPaymentRequest::try_from(req)?;

// if the request has split payment object, then append the transfer account id in headers in charge_type is Direct
if let Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment(
stripe_split_payment,
)) = &req.request.split_payments
Expand All @@ -890,6 +894,16 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
header.append(&mut customer_account_header);
}
}
// if request doesn't have transfer_account_id, but stripe_split_payment_metadata has it, append it
else if let Some(transfer_account_id) =
stripe_split_payment_metadata.transfer_account_id.clone()
{
let mut customer_account_header = vec![(
STRIPE_COMPATIBLE_CONNECT_ACCOUNT.to_string(),
transfer_account_id.into_masked(),
)];
header.append(&mut customer_account_header);
}
Ok(header)
}

Expand Down
193 changes: 167 additions & 26 deletions crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{collections::HashMap, ops::Deref};

use api_models::{self, enums as api_enums, payments};
use common_enums::{enums, AttemptStatus, PaymentChargeType, StripeChargeType};
use common_types::payments::SplitPaymentsRequest;
use common_utils::{
collect_missing_value_keys,
errors::CustomResult,
Expand Down Expand Up @@ -198,7 +199,7 @@ pub struct PaymentIntentRequest {

#[derive(Debug, Eq, PartialEq, Serialize)]
pub struct IntentCharges {
pub application_fee_amount: MinorUnit,
pub application_fee_amount: Option<MinorUnit>,
#[serde(
rename = "transfer_data[destination]",
skip_serializing_if = "Option::is_none"
Expand Down Expand Up @@ -1668,8 +1669,39 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest
fn try_from(data: (&PaymentsAuthorizeRouterData, MinorUnit)) -> Result<Self, Self::Error> {
let item = data.0;

let mandate_metadata = item
.request
.mandate_id
.as_ref()
.and_then(|mandate_id| mandate_id.mandate_reference_id.as_ref())
.and_then(|reference_id| match reference_id {
payments::MandateReferenceId::ConnectorMandateId(mandate_data) => {
Some(mandate_data.get_mandate_metadata())
}
_ => None,
});

let (transfer_account_id, charge_type, application_fees) = if let Some(secret_value) =
mandate_metadata.as_ref().and_then(|s| s.as_ref())
{
let json_value = secret_value.clone().expose();

let parsed: Result<StripeSplitPaymentRequest, _> = serde_json::from_value(json_value);

match parsed {
Ok(data) => (
data.transfer_account_id,
data.charge_type,
data.application_fees,
),
Err(_) => (None, None, None),
}
} else {
(None, None, None)
};

let payment_method_token = match &item.request.split_payments {
Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment(_)) => {
Some(SplitPaymentsRequest::StripeSplitPayment(_)) => {
match item.payment_method_token.clone() {
Some(PaymentMethodToken::Token(secret)) => Some(secret),
_ => None,
Expand Down Expand Up @@ -1948,27 +1980,45 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest
};

let charges = match &item.request.split_payments {
Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment(
stripe_split_payment,
)) => match &stripe_split_payment.charge_type {
PaymentChargeType::Stripe(charge_type) => match charge_type {
StripeChargeType::Direct => Some(IntentCharges {
application_fee_amount: stripe_split_payment.application_fees,
destination_account_id: None,
}),
StripeChargeType::Destination => Some(IntentCharges {
application_fee_amount: stripe_split_payment.application_fees,
destination_account_id: Some(
stripe_split_payment.transfer_account_id.clone(),
),
}),
},
},
Some(common_types::payments::SplitPaymentsRequest::AdyenSplitPayment(_))
| Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(_))
Some(SplitPaymentsRequest::StripeSplitPayment(stripe_split_payment)) => {
match &stripe_split_payment.charge_type {
PaymentChargeType::Stripe(charge_type) => match charge_type {
StripeChargeType::Direct => Some(IntentCharges {
application_fee_amount: stripe_split_payment.application_fees,
destination_account_id: None,
}),
StripeChargeType::Destination => Some(IntentCharges {
application_fee_amount: stripe_split_payment.application_fees,
destination_account_id: Some(
stripe_split_payment.transfer_account_id.clone(),
),
}),
},
}
}
Some(SplitPaymentsRequest::AdyenSplitPayment(_))
| Some(SplitPaymentsRequest::XenditSplitPayment(_))
| None => None,
};

let charges_in = if charges.is_none() {
match charge_type {
Some(PaymentChargeType::Stripe(StripeChargeType::Direct)) => Some(IntentCharges {
application_fee_amount: application_fees, // default to 0 if None
destination_account_id: None,
}),
Some(PaymentChargeType::Stripe(StripeChargeType::Destination)) => {
Some(IntentCharges {
application_fee_amount: application_fees,
destination_account_id: transfer_account_id,
})
}
_ => None,
}
} else {
charges
};

let pm = match (payment_method, payment_method_token.clone()) {
(Some(method), _) => Some(Secret::new(method)),
(None, Some(token)) => Some(token),
Expand Down Expand Up @@ -2009,7 +2059,7 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest
payment_method_types,
expand: Some(ExpandableObjects::LatestCharge),
browser_info,
charges,
charges: charges_in,
})
}
}
Expand Down Expand Up @@ -2146,6 +2196,87 @@ impl TryFrom<&ConnectorCustomerRouterData> for CustomerRequest {
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StripeSplitPaymentRequest {
pub charge_type: Option<PaymentChargeType>,
pub application_fees: Option<MinorUnit>,
pub transfer_account_id: Option<String>,
}

impl TryFrom<&PaymentsAuthorizeRouterData> for StripeSplitPaymentRequest {
type Error = error_stack::Report<ConnectorError>;

fn try_from(item: &PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
//extracting mandate metadata from CIT call if CIT call was a Split Payment
let from_metadata = item
.request
.mandate_id
.as_ref()
.and_then(|mandate_id| mandate_id.mandate_reference_id.as_ref())
.and_then(|reference_id| match reference_id {
payments::MandateReferenceId::ConnectorMandateId(mandate_data) => {
mandate_data.get_mandate_metadata()
}
_ => None,
})
.and_then(|secret_value| {
let json_value = secret_value.clone().expose();
serde_json::from_value::<Self>(json_value).ok()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we not ignore error here, instead log the error if not populate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright

});

// If the Split Payment Request in MIT mismatches with the metadata from CIT, throw an error
if from_metadata.is_some() && item.request.split_payments.is_some() {
let mut mit_charge_type = None;
let mut mit_application_fees = None;
let mut mit_transfer_account_id = None;
if let Some(SplitPaymentsRequest::StripeSplitPayment(stripe_split_payment)) =
item.request.split_payments.as_ref()
{
mit_charge_type = Some(stripe_split_payment.charge_type.clone());
mit_application_fees = stripe_split_payment.application_fees;
mit_transfer_account_id = Some(stripe_split_payment.transfer_account_id.clone());
}

if mit_charge_type != from_metadata.as_ref().and_then(|m| m.charge_type.clone())
|| mit_application_fees != from_metadata.as_ref().and_then(|m| m.application_fees)
|| mit_transfer_account_id
!= from_metadata
.as_ref()
.and_then(|m| m.transfer_account_id.clone())
{
let mismatched_fields = ["transfer_account_id", "application_fees", "charge_type"];

let field_str = mismatched_fields.join(", ");
return Err(error_stack::Report::from(
ConnectorError::MandatePaymentDataMismatch { fields: field_str },
));
}
}

// If Mandate Metadata from CIT call has something, populate it
let (charge_type, mut transfer_account_id, application_fees) =
if let Some(ref metadata) = from_metadata {
(
metadata.charge_type.clone(),
metadata.transfer_account_id.clone(),
metadata.application_fees,
)
} else {
(None, None, None)
};

// If Charge Type is Destination, transfer_account_id need not be appended in headers
if charge_type == Some(PaymentChargeType::Stripe(StripeChargeType::Destination)) {
transfer_account_id = None;
}
Ok(Self {
charge_type,
transfer_account_id,
application_fees,
})
}
}

#[derive(Clone, Default, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StripePaymentStatus {
Expand Down Expand Up @@ -2524,10 +2655,23 @@ where
// For backward compatibility payment_method_id & connector_mandate_id is being populated with the same value
let connector_mandate_id = Some(payment_method_id.clone().expose());
let payment_method_id = Some(payment_method_id.expose());

let mandate_metadata: Option<Secret<Value>> =
match item.data.request.get_split_payment_data() {
Some(SplitPaymentsRequest::StripeSplitPayment(stripe_split_data)) => {
Some(Secret::new(serde_json::json!({
"transfer_account_id": stripe_split_data.transfer_account_id,
"charge_type": stripe_split_data.charge_type,
"application_fees": stripe_split_data.application_fees,
})))
}
_ => None,
};

MandateReference {
connector_mandate_id,
payment_method_id,
mandate_metadata: None,
mandate_metadata,
connector_mandate_request_reference_id: None,
}
});
Expand Down Expand Up @@ -4252,10 +4396,7 @@ where
T: SplitPaymentData,
{
let charge_request = request.get_split_payment_data();
if let Some(common_types::payments::SplitPaymentsRequest::StripeSplitPayment(
stripe_split_payment,
)) = charge_request
{
if let Some(SplitPaymentsRequest::StripeSplitPayment(stripe_split_payment)) = charge_request {
let stripe_charge_response = common_types::payments::StripeChargeResponseData {
charge_id: Some(charge_id),
charge_type: stripe_split_payment.charge_type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@ pub enum ApiErrorResponse {
InvalidPlatformOperation,
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_45", message = "External vault failed during processing with connector")]
ExternalVaultFailed,
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_46", message = "Fields like {fields} doesn't match with the ones used during mandate creation")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_46", message = "Fields like {fields} doesn't match with the ones used during mandate creation")]
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_46", message = "Field {fields} doesn't match with the ones used during mandate creation")]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

MandatePaymentDataMismatch { fields: String },
#[error(error_type = ErrorType::InvalidRequestError, code = "WE_01", message = "Failed to authenticate the webhook")]
WebhookAuthenticationFailed,
#[error(error_type = ErrorType::InvalidRequestError, code = "WE_02", message = "Bad request received in webhook")]
Expand Down Expand Up @@ -651,6 +653,9 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> for ApiErrorRespon
Self::ExternalVaultFailed => {
AER::BadRequest(ApiError::new("IR", 45, "External Vault failed while processing with connector.", None))
},
Self::MandatePaymentDataMismatch { fields} => {
AER::BadRequest(ApiError::new("IR", 46, format!("Fields like {fields} doesn't match with the ones used during mandate creation"), Some(Extra {fields: Some(fields.to_owned()), ..Default::default()}))) //FIXME: error message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
AER::BadRequest(ApiError::new("IR", 46, format!("Fields like {fields} doesn't match with the ones used during mandate creation"), Some(Extra {fields: Some(fields.to_owned()), ..Default::default()}))) //FIXME: error message
AER::BadRequest(ApiError::new("IR", 46, format!("Field {fields} doesn't match with the ones used during mandate creation"), Some(Extra {fields: Some(fields.to_owned()), ..Default::default()}))) //FIXME: error message

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

}

Self::WebhookAuthenticationFailed => {
AER::Unauthorized(ApiError::new("WE", 1, "Webhook authentication failed", None))
Expand Down
2 changes: 2 additions & 0 deletions crates/hyperswitch_interfaces/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ pub enum ConnectorError {
error_message: String,
error_object: serde_json::Value,
},
#[error("Fields like {fields} doesn't match with the ones used during mandate creation")]
MandatePaymentDataMismatch { fields: String },
}

impl ConnectorError {
Expand Down
3 changes: 3 additions & 0 deletions crates/router/src/compatibility/stripe/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,9 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
errors::ApiErrorResponse::AddressNotFound => Self::AddressNotFound,
errors::ApiErrorResponse::NotImplemented { .. } => Self::Unauthorized,
errors::ApiErrorResponse::FlowNotSupported { .. } => Self::InternalServerError,
errors::ApiErrorResponse::MandatePaymentDataMismatch { .. } => {
Self::InternalServerError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be 4xx if we are checking some request data to our internal saved data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright

}
errors::ApiErrorResponse::PaymentUnexpectedState {
current_flow,
field_name,
Expand Down
7 changes: 7 additions & 0 deletions crates/router/src/core/errors/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ impl<T> ConnectorErrorExt<T> for error_stack::Result<T, errors::ConnectorError>
| errors::ConnectorError::DateFormattingFailed
| errors::ConnectorError::InvalidDataFormat { .. }
| errors::ConnectorError::MismatchedPaymentData
| errors::ConnectorError::MandatePaymentDataMismatch { .. }
| errors::ConnectorError::InvalidWalletToken { .. }
| errors::ConnectorError::MissingConnectorRelatedTransactionID { .. }
| errors::ConnectorError::FileValidationFailed { .. }
Expand Down Expand Up @@ -230,6 +231,11 @@ impl<T> ConnectorErrorExt<T> for error_stack::Result<T, errors::ConnectorError>
"payment_method_data, payment_method_type and payment_experience does not match",
}
},
errors::ConnectorError::MandatePaymentDataMismatch {fields}=> {
errors::ApiErrorResponse::MandatePaymentDataMismatch {
fields: fields.to_owned(),
}
},
errors::ConnectorError::NotSupported { message, connector } => {
errors::ApiErrorResponse::NotSupported { message: format!("{message} is not supported by {connector}") }
},
Expand Down Expand Up @@ -376,6 +382,7 @@ impl<T> ConnectorErrorExt<T> for error_stack::Result<T, errors::ConnectorError>
| errors::ConnectorError::DateFormattingFailed
| errors::ConnectorError::InvalidDataFormat { .. }
| errors::ConnectorError::MismatchedPaymentData
| errors::ConnectorError::MandatePaymentDataMismatch { .. }
| errors::ConnectorError::MissingConnectorRelatedTransactionID { .. }
| errors::ConnectorError::FileValidationFailed { .. }
| errors::ConnectorError::MissingConnectorRedirectionPayload { .. }
Expand Down
Loading
Loading