mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 11:06:50 +08:00
Feat(connector): [BRAINTREE] Implement Card Mandates (#5204)
This commit is contained in:
@ -15,6 +15,7 @@ use self::transformers as braintree;
|
||||
use super::utils::{self as connector_utils, PaymentsAuthorizeRequestData};
|
||||
use crate::{
|
||||
configs::settings,
|
||||
connector::utils::PaymentMethodDataType,
|
||||
consts,
|
||||
core::{
|
||||
errors::{self, CustomResult},
|
||||
@ -175,6 +176,15 @@ impl ConnectorValidation for Braintree {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mandate_payment(
|
||||
&self,
|
||||
pm_type: Option<types::storage::enums::PaymentMethodType>,
|
||||
pm_data: domain::payments::PaymentMethodData,
|
||||
) -> CustomResult<(), errors::ConnectorError> {
|
||||
let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]);
|
||||
connector_utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl api::Payment for Braintree {}
|
||||
|
||||
@ -5,11 +5,14 @@ use serde::{Deserialize, Serialize};
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
use crate::{
|
||||
connector::utils::{self, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData},
|
||||
connector::utils::{
|
||||
self, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData,
|
||||
RefundsRequestData, RouterData,
|
||||
},
|
||||
consts,
|
||||
core::errors,
|
||||
services,
|
||||
types::{self, api, domain, storage::enums},
|
||||
types::{self, api, domain, storage::enums, MandateReference},
|
||||
unimplemented_payment_method,
|
||||
};
|
||||
|
||||
@ -20,6 +23,8 @@ pub const AUTHORIZE_CREDIT_CARD_MUTATION: &str = "mutation authorizeCreditCard($
|
||||
pub const CAPTURE_TRANSACTION_MUTATION: &str = "mutation captureTransaction($input: CaptureTransactionInput!) { captureTransaction(input: $input) { clientMutationId transaction { id legacyId amount { value currencyCode } status } } }";
|
||||
pub const VOID_TRANSACTION_MUTATION: &str = "mutation voidTransaction($input: ReverseTransactionInput!) { reverseTransaction(input: $input) { clientMutationId reversal { ... on Transaction { id legacyId amount { value currencyCode } status } } } }";
|
||||
pub const REFUND_TRANSACTION_MUTATION: &str = "mutation refundTransaction($input: RefundTransactionInput!) { refundTransaction(input: $input) {clientMutationId refund { id legacyId amount { value currencyCode } status } } }";
|
||||
pub const AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION: &str="mutation authorizeCreditCard($input: AuthorizeCreditCardInput!) { authorizeCreditCard(input: $input) { transaction { id status createdAt paymentMethod { id } } } }";
|
||||
pub const CHARGE_AND_VAULT_TRANSACTION_MUTATION: &str ="mutation ChargeCreditCard($input: ChargeCreditCardInput!) { chargeCreditCard(input: $input) { transaction { id status createdAt paymentMethod { id } } } }";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BraintreeRouterData<T> {
|
||||
@ -58,11 +63,18 @@ pub struct CardPaymentRequest {
|
||||
variables: VariablePaymentInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MandatePaymentRequest {
|
||||
query: String,
|
||||
variables: VariablePaymentInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum BraintreePaymentsRequest {
|
||||
Card(CardPaymentRequest),
|
||||
CardThreeDs(BraintreeClientTokenRequest),
|
||||
Mandate(MandatePaymentRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -84,11 +96,69 @@ impl TryFrom<&Option<pii::SecretSerdeValue>> for BraintreeMeta {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransactionBody {
|
||||
pub struct RegularTransactionBody {
|
||||
amount: String,
|
||||
merchant_account_id: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VaultTransactionBody {
|
||||
amount: String,
|
||||
merchant_account_id: Secret<String>,
|
||||
vault_payment_method_after_transacting: TransactionTiming,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum TransactionBody {
|
||||
Regular(RegularTransactionBody),
|
||||
Vault(VaultTransactionBody),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransactionTiming {
|
||||
when: String,
|
||||
}
|
||||
|
||||
impl
|
||||
TryFrom<(
|
||||
&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>,
|
||||
String,
|
||||
BraintreeMeta,
|
||||
)> for MandatePaymentRequest
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
(item, connector_mandate_id, metadata): (
|
||||
&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>,
|
||||
String,
|
||||
BraintreeMeta,
|
||||
),
|
||||
) -> Result<Self, Self::Error> {
|
||||
let (query, transaction_body) = (
|
||||
match item.router_data.request.is_auto_capture()? {
|
||||
true => CHARGE_CREDIT_CARD_MUTATION.to_string(),
|
||||
false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(),
|
||||
},
|
||||
TransactionBody::Regular(RegularTransactionBody {
|
||||
amount: item.amount.to_owned(),
|
||||
merchant_account_id: metadata.merchant_account_id,
|
||||
}),
|
||||
);
|
||||
Ok(Self {
|
||||
query,
|
||||
variables: VariablePaymentInput {
|
||||
input: PaymentInput {
|
||||
payment_method_id: connector_mandate_id.into(),
|
||||
transaction: transaction_body,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>>
|
||||
for BraintreePaymentsRequest
|
||||
{
|
||||
@ -105,7 +175,6 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>>
|
||||
item.router_data.request.currency,
|
||||
Some(metadata.merchant_config_currency),
|
||||
)?;
|
||||
|
||||
match item.router_data.request.payment_method_data.clone() {
|
||||
domain::PaymentMethodData::Card(_) => {
|
||||
if item.router_data.is_three_ds() {
|
||||
@ -116,6 +185,18 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>>
|
||||
Ok(Self::Card(CardPaymentRequest::try_from((item, metadata))?))
|
||||
}
|
||||
}
|
||||
domain::PaymentMethodData::MandatePayment => {
|
||||
let connector_mandate_id = item.router_data.request.connector_mandate_id().ok_or(
|
||||
errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "connector_mandate_id",
|
||||
},
|
||||
)?;
|
||||
Ok(Self::Mandate(MandatePaymentRequest::try_from((
|
||||
item,
|
||||
connector_mandate_id,
|
||||
metadata,
|
||||
))?))
|
||||
}
|
||||
domain::PaymentMethodData::CardRedirect(_)
|
||||
| domain::PaymentMethodData::Wallet(_)
|
||||
| domain::PaymentMethodData::PayLater(_)
|
||||
@ -123,7 +204,6 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>>
|
||||
| domain::PaymentMethodData::BankDebit(_)
|
||||
| domain::PaymentMethodData::BankTransfer(_)
|
||||
| domain::PaymentMethodData::Crypto(_)
|
||||
| domain::PaymentMethodData::MandatePayment
|
||||
| domain::PaymentMethodData::Reward
|
||||
| domain::PaymentMethodData::RealTimePayment(_)
|
||||
| domain::PaymentMethodData::Upi(_)
|
||||
@ -194,9 +274,16 @@ pub enum BraintreeCompleteAuthResponse {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct PaymentMethodInfo {
|
||||
id: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransactionAuthChargeResponseBody {
|
||||
id: String,
|
||||
status: BraintreePaymentStatus,
|
||||
payment_method: Option<PaymentMethodInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
@ -242,7 +329,12 @@ impl<F>
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
mandate_reference: transaction_data.payment_method.as_ref().map(|pm| {
|
||||
MandateReference {
|
||||
connector_mandate_id: Some(pm.id.clone().expose()),
|
||||
payment_method_id: None,
|
||||
}
|
||||
}),
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: None,
|
||||
@ -426,7 +518,12 @@ impl<F>
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
mandate_reference: transaction_data.payment_method.as_ref().map(|pm| {
|
||||
MandateReference {
|
||||
connector_mandate_id: Some(pm.id.clone().expose()),
|
||||
payment_method_id: None,
|
||||
}
|
||||
}),
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: None,
|
||||
@ -490,7 +587,12 @@ impl<F>
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
mandate_reference: transaction_data.payment_method.as_ref().map(|pm| {
|
||||
MandateReference {
|
||||
connector_mandate_id: Some(pm.id.clone().expose()),
|
||||
payment_method_id: None,
|
||||
}
|
||||
}),
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: None,
|
||||
@ -536,7 +638,12 @@ impl<F>
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
mandate_reference: transaction_data.payment_method.as_ref().map(|pm| {
|
||||
MandateReference {
|
||||
connector_mandate_id: Some(pm.id.clone().expose()),
|
||||
payment_method_id: None,
|
||||
}
|
||||
}),
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: None,
|
||||
@ -1327,9 +1434,31 @@ impl
|
||||
BraintreeMeta,
|
||||
),
|
||||
) -> Result<Self, Self::Error> {
|
||||
let query = match item.router_data.request.is_auto_capture()? {
|
||||
true => CHARGE_CREDIT_CARD_MUTATION.to_string(),
|
||||
false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(),
|
||||
let (query, transaction_body) = if item.router_data.request.is_mandate_payment() {
|
||||
(
|
||||
match item.router_data.request.is_auto_capture()? {
|
||||
true => CHARGE_AND_VAULT_TRANSACTION_MUTATION.to_string(),
|
||||
false => AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION.to_string(),
|
||||
},
|
||||
TransactionBody::Vault(VaultTransactionBody {
|
||||
amount: item.amount.to_owned(),
|
||||
merchant_account_id: metadata.merchant_account_id,
|
||||
vault_payment_method_after_transacting: TransactionTiming {
|
||||
when: "ALWAYS".to_string(),
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
match item.router_data.request.is_auto_capture()? {
|
||||
true => CHARGE_CREDIT_CARD_MUTATION.to_string(),
|
||||
false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(),
|
||||
},
|
||||
TransactionBody::Regular(RegularTransactionBody {
|
||||
amount: item.amount.to_owned(),
|
||||
merchant_account_id: metadata.merchant_account_id,
|
||||
}),
|
||||
)
|
||||
};
|
||||
Ok(Self {
|
||||
query,
|
||||
@ -1341,10 +1470,7 @@ impl
|
||||
unimplemented_payment_method!("Apple Pay", "Simplified", "Braintree"),
|
||||
)?,
|
||||
},
|
||||
transaction: TransactionBody {
|
||||
amount: item.amount.to_owned(),
|
||||
merchant_account_id: metadata.merchant_account_id,
|
||||
},
|
||||
transaction: transaction_body,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -1367,11 +1493,10 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
|
||||
item.router_data.request.currency,
|
||||
Some(metadata.merchant_config_currency),
|
||||
)?;
|
||||
let payload_data =
|
||||
utils::PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload(
|
||||
&item.router_data.request,
|
||||
)?
|
||||
.expose();
|
||||
let payload_data = PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload(
|
||||
&item.router_data.request,
|
||||
)?
|
||||
.expose();
|
||||
let redirection_response: BraintreeRedirectionResponse = serde_json::from_value(
|
||||
payload_data,
|
||||
)
|
||||
@ -1384,21 +1509,39 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>>
|
||||
.change_context(errors::ConnectorError::MissingConnectorRedirectionPayload {
|
||||
field_name: "three_ds_data",
|
||||
})?;
|
||||
let query = match utils::PaymentsCompleteAuthorizeRequestData::is_auto_capture(
|
||||
&item.router_data.request,
|
||||
)? {
|
||||
true => CHARGE_CREDIT_CARD_MUTATION.to_string(),
|
||||
false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(),
|
||||
|
||||
let (query, transaction_body) = if item.router_data.request.is_mandate_payment() {
|
||||
(
|
||||
match item.router_data.request.is_auto_capture()? {
|
||||
true => CHARGE_AND_VAULT_TRANSACTION_MUTATION.to_string(),
|
||||
false => AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION.to_string(),
|
||||
},
|
||||
TransactionBody::Vault(VaultTransactionBody {
|
||||
amount: item.amount.to_owned(),
|
||||
merchant_account_id: metadata.merchant_account_id,
|
||||
vault_payment_method_after_transacting: TransactionTiming {
|
||||
when: "ALWAYS".to_string(),
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
match item.router_data.request.is_auto_capture()? {
|
||||
true => CHARGE_CREDIT_CARD_MUTATION.to_string(),
|
||||
false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(),
|
||||
},
|
||||
TransactionBody::Regular(RegularTransactionBody {
|
||||
amount: item.amount.to_owned(),
|
||||
merchant_account_id: metadata.merchant_account_id,
|
||||
}),
|
||||
)
|
||||
};
|
||||
Ok(Self {
|
||||
query,
|
||||
variables: VariablePaymentInput {
|
||||
input: PaymentInput {
|
||||
payment_method_id: three_ds_data.nonce,
|
||||
transaction: TransactionBody {
|
||||
amount: item.amount.to_owned(),
|
||||
merchant_account_id: metadata.merchant_account_id,
|
||||
},
|
||||
transaction: transaction_body,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -1017,6 +1017,7 @@ pub trait PaymentsCompleteAuthorizeRequestData {
|
||||
fn get_email(&self) -> Result<Email, Error>;
|
||||
fn get_redirect_response_payload(&self) -> Result<pii::SecretSerdeValue, Error>;
|
||||
fn get_complete_authorize_url(&self) -> Result<String, Error>;
|
||||
fn is_mandate_payment(&self) -> bool;
|
||||
}
|
||||
|
||||
impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData {
|
||||
@ -1046,6 +1047,17 @@ impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData {
|
||||
.clone()
|
||||
.ok_or_else(missing_field_err("complete_authorize_url"))
|
||||
}
|
||||
fn is_mandate_payment(&self) -> bool {
|
||||
((self.customer_acceptance.is_some() || self.setup_mandate_details.is_some())
|
||||
&& self.setup_future_usage.map_or(false, |setup_future_usage| {
|
||||
setup_future_usage == storage_enums::FutureUsage::OffSession
|
||||
}))
|
||||
|| self
|
||||
.mandate_id
|
||||
.as_ref()
|
||||
.and_then(|mandate_ids| mandate_ids.mandate_reference_id.as_ref())
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PaymentsSyncRequestData {
|
||||
|
||||
@ -610,6 +610,21 @@ pub async fn get_token_pm_type_mandate_details(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let payment_method_info = payment_method_id
|
||||
.async_map(|payment_method_id| async move {
|
||||
state
|
||||
.store
|
||||
.find_payment_method(
|
||||
&payment_method_id,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(
|
||||
errors::ApiErrorResponse::PaymentMethodNotFound,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.transpose()?;
|
||||
(
|
||||
request.payment_token.to_owned(),
|
||||
request.payment_method,
|
||||
@ -617,7 +632,7 @@ pub async fn get_token_pm_type_mandate_details(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
payment_method_info,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,10 @@ use crate::{
|
||||
core::{
|
||||
errors::{self, CustomResult, RouterResult, StorageErrorExt},
|
||||
mandate::helpers as m_helpers,
|
||||
payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData},
|
||||
payments::{
|
||||
self, helpers, operations, CustomerAcceptance, CustomerDetails, PaymentAddress,
|
||||
PaymentData,
|
||||
},
|
||||
utils as core_utils,
|
||||
},
|
||||
db::StorageInterface,
|
||||
@ -86,7 +89,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
|
||||
})?;
|
||||
|
||||
let recurring_details = request.recurring_details.clone();
|
||||
let customer_acceptance = request.customer_acceptance.clone().map(From::from);
|
||||
|
||||
payment_attempt = db
|
||||
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
|
||||
@ -127,6 +129,19 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
|
||||
&payment_intent.customer_id,
|
||||
)
|
||||
.await?;
|
||||
let customer_acceptance: Option<CustomerAcceptance> = request
|
||||
.customer_acceptance
|
||||
.clone()
|
||||
.map(From::from)
|
||||
.or(payment_method_info
|
||||
.clone()
|
||||
.map(|pm| {
|
||||
pm.customer_acceptance
|
||||
.parse_value::<CustomerAcceptance>("CustomerAcceptance")
|
||||
})
|
||||
.transpose()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to deserialize to CustomerAcceptance")?);
|
||||
let token = token.or_else(|| payment_attempt.payment_token.clone());
|
||||
|
||||
if let Some(payment_method) = payment_method {
|
||||
|
||||
@ -1790,6 +1790,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
|
||||
connector_meta: payment_data.payment_attempt.connector_metadata,
|
||||
complete_authorize_url,
|
||||
metadata: payment_data.payment_intent.metadata,
|
||||
customer_acceptance: payment_data.customer_acceptance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user