Feat(connector): [BRAINTREE] Implement Card Mandates (#5204)

This commit is contained in:
awasthi21
2024-07-05 13:37:46 +05:30
committed by GitHub
parent 00f9ed4cae
commit 1904ffad88
13 changed files with 246 additions and 49 deletions

View File

@ -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 {}

View File

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

View File

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

View File

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

View File

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

View File

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