feat(connector): [Braintree] Add Authorize, Capture, Void, PSync, Refund, Rsync for Braintree GraphQL API (#1962)

Co-authored-by: AkshayaFoiger <akshaya.shankar@juspay.in>
This commit is contained in:
Prasunna Soppa
2023-08-24 19:12:43 +05:30
committed by GitHub
parent 8c066d3ea7
commit 820f6153af
15 changed files with 1901 additions and 268 deletions

View File

@ -93,6 +93,13 @@ pub struct Settings {
pub connector_request_reference_id_config: ConnectorRequestReferenceIdConfig,
#[cfg(feature = "payouts")]
pub payouts: Payouts,
pub multiple_api_version_supported_connectors: MultipleApiVersionSupportedConnectors,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct MultipleApiVersionSupportedConnectors {
#[serde(deserialize_with = "connector_deser")]
pub supported_connectors: HashSet<api_models::enums::Connector>,
}
#[derive(Debug, Deserialize, Clone, Default)]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,992 @@
use error_stack::ResultExt;
use masking::Secret;
use serde::{Deserialize, Serialize};
use crate::{
connector::utils::{self, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData},
consts,
core::errors,
types::{self, api, storage::enums},
};
pub const TOKENIZE_CREDIT_CARD: &str = "mutation tokenizeCreditCard($input: TokenizeCreditCardInput!) { tokenizeCreditCard(input: $input) { clientMutationId paymentMethod { id } } }";
pub const CHARGE_CREDIT_CARD_MUTATION: &str = "mutation ChargeCreditCard($input: ChargeCreditCardInput!) { chargeCreditCard(input: $input) { transaction { id legacyId createdAt amount { value currencyCode } status } } }";
pub const AUTHORIZE_CREDIT_CARD_MUTATION: &str = "mutation authorizeCreditCard($input: AuthorizeCreditCardInput!) { authorizeCreditCard(input: $input) { transaction { id legacyId amount { value currencyCode } status } } }";
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 } } }";
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentInput {
payment_method_id: String,
transaction: TransactionBody,
}
#[derive(Debug, Serialize)]
pub struct VariablePaymentInput {
input: PaymentInput,
}
#[derive(Debug, Serialize)]
pub struct BraintreePaymentsRequest {
query: String,
variables: VariablePaymentInput,
}
#[derive(Debug, Deserialize)]
pub struct BraintreeMeta {
merchant_account_id: Option<Secret<String>>,
merchant_config_currency: Option<types::storage::enums::Currency>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionBody {
amount: String,
merchant_account_id: Secret<String>,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?;
match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(_) => {
let query = match item.request.is_auto_capture()? {
true => CHARGE_CREDIT_CARD_MUTATION.to_string(),
false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(),
};
Ok(Self {
query,
variables: VariablePaymentInput {
input: PaymentInput {
payment_method_id: item.get_payment_method_token()?,
transaction: TransactionBody {
amount: utils::to_currency_base_unit(
item.request.amount,
item.request.currency,
)?,
merchant_account_id: metadata.merchant_account_id.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
},
},
},
})
}
api_models::payments::PaymentMethodData::CardRedirect(_)
| api_models::payments::PaymentMethodData::Wallet(_)
| api_models::payments::PaymentMethodData::PayLater(_)
| api_models::payments::PaymentMethodData::BankRedirect(_)
| api_models::payments::PaymentMethodData::BankDebit(_)
| api_models::payments::PaymentMethodData::BankTransfer(_)
| api_models::payments::PaymentMethodData::Crypto(_)
| api_models::payments::PaymentMethodData::MandatePayment
| api_models::payments::PaymentMethodData::Reward(_)
| api_models::payments::PaymentMethodData::Upi(_)
| api_models::payments::PaymentMethodData::Voucher(_)
| api_models::payments::PaymentMethodData::GiftCard(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("braintree"),
)
.into())
}
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthResponse {
data: DataAuthResponse,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BraintreeAuthResponse {
AuthResponse(Box<AuthResponse>),
ErrorResponse(Box<ErrorResponse>),
}
#[derive(Debug, Clone, Deserialize)]
pub struct TransactionAuthChargeResponseBody {
id: String,
status: BraintreePaymentStatus,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DataAuthResponse {
authorize_credit_card: AuthChargeCreditCard,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthChargeCreditCard {
transaction: TransactionAuthChargeResponseBody,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, BraintreeAuthResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, BraintreeAuthResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
match item.response {
BraintreeAuthResponse::ErrorResponse(error_response) => Ok(Self {
response: build_error_response(&error_response.errors, item.http_code),
..item.data
}),
BraintreeAuthResponse::AuthResponse(auth_response) => {
let transaction_data = auth_response.data.authorize_credit_card.transaction;
Ok(Self {
status: enums::AttemptStatus::from(transaction_data.status.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
transaction_data.id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
}),
..item.data
})
}
}
}
}
fn build_error_response<T>(
response: &[ErrorDetails],
http_code: u16,
) -> Result<T, types::ErrorResponse> {
let error_messages = response
.iter()
.map(|error| error.message.to_string())
.collect::<Vec<String>>();
let reason = match !error_messages.is_empty() {
true => Some(error_messages.join(" ")),
false => None,
};
get_error_response(
response
.get(0)
.and_then(|err_details| err_details.extensions.as_ref())
.and_then(|extensions| extensions.legacy_code.clone()),
response
.get(0)
.map(|err_details| err_details.message.clone()),
reason,
http_code,
)
}
fn get_error_response<T>(
error_code: Option<String>,
error_msg: Option<String>,
error_reason: Option<String>,
http_code: u16,
) -> Result<T, types::ErrorResponse> {
Err(types::ErrorResponse {
code: error_code.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()),
message: error_msg.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
reason: error_reason,
status_code: http_code,
})
}
// Using Auth type from braintree/transformer.rs, need this in later time when we use graphql version
// pub struct BraintreeAuthType {
// pub(super) auth_header: String,
// pub(super) merchant_id: Secret<String>,
// }
// impl TryFrom<&types::ConnectorAuthType> for BraintreeAuthType {
// type Error = error_stack::Report<errors::ConnectorError>;
// fn try_from(item: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
// if let types::ConnectorAuthType::SignatureKey {
// api_key: public_key,
// key1: merchant_id,
// api_secret: private_key,
// } = item
// {
// let auth_key = format!("{}:{}", public_key.peek(), private_key.peek());
// let auth_header = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_key));
// Ok(Self {
// auth_header,
// merchant_id: merchant_id.to_owned(),
// })
// } else {
// Err(errors::ConnectorError::FailedToObtainAuthType)?
// }
// }
// }
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BraintreePaymentStatus {
Authorized,
Authorizing,
AuthorizedExpired,
Failed,
ProcessorDeclined,
GatewayRejected,
Voided,
Settling,
Settled,
SettlementPending,
SettlementDeclined,
SettlementConfirmed,
SubmittedForSettlement,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ErrorDetails {
pub message: String,
pub extensions: Option<AdditionalErrorDetails>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdditionalErrorDetails {
pub legacy_code: Option<String>,
}
impl From<BraintreePaymentStatus> for enums::AttemptStatus {
fn from(item: BraintreePaymentStatus) -> Self {
match item {
BraintreePaymentStatus::Settling
| BraintreePaymentStatus::Settled
| BraintreePaymentStatus::SettlementConfirmed => Self::Charged,
BraintreePaymentStatus::Authorizing => Self::Authorizing,
BraintreePaymentStatus::AuthorizedExpired => Self::AuthorizationFailed,
BraintreePaymentStatus::Failed
| BraintreePaymentStatus::GatewayRejected
| BraintreePaymentStatus::ProcessorDeclined
| BraintreePaymentStatus::SettlementDeclined => Self::Failure,
BraintreePaymentStatus::Authorized => Self::Authorized,
BraintreePaymentStatus::Voided => Self::Voided,
BraintreePaymentStatus::SubmittedForSettlement
| BraintreePaymentStatus::SettlementPending => Self::Pending,
}
}
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, BraintreePaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
BraintreePaymentsResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
match item.response {
BraintreePaymentsResponse::ErrorResponse(error_response) => Ok(Self {
response: build_error_response(&error_response.errors.clone(), item.http_code),
..item.data
}),
BraintreePaymentsResponse::PaymentsResponse(payment_response) => {
let transaction_data = payment_response.data.charge_credit_card.transaction;
Ok(Self {
status: enums::AttemptStatus::from(transaction_data.status.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
transaction_data.id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
}),
..item.data
})
}
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct PaymentsResponse {
data: DataResponse,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BraintreePaymentsResponse {
PaymentsResponse(Box<PaymentsResponse>),
ErrorResponse(Box<ErrorResponse>),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DataResponse {
charge_credit_card: AuthChargeCreditCard,
}
#[derive(Default, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RefundInputData {
amount: String,
merchant_account_id: Secret<String>,
}
#[derive(Default, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BraintreeRefundInput {
transaction_id: String,
refund: RefundInputData,
}
#[derive(Default, Debug, Clone, Serialize)]
pub struct BraintreeRefundVariables {
input: BraintreeRefundInput,
}
#[derive(Default, Debug, Clone, Serialize)]
pub struct BraintreeRefundRequest {
query: String,
variables: BraintreeRefundVariables,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for BraintreeRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?;
let query = REFUND_TRANSACTION_MUTATION.to_string();
let variables = BraintreeRefundVariables {
input: BraintreeRefundInput {
transaction_id: item.request.connector_transaction_id.clone(),
refund: RefundInputData {
amount: utils::to_currency_base_unit(
item.request.refund_amount,
item.request.currency,
)?,
merchant_account_id: metadata.merchant_account_id.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "merchant_account_id",
},
)?,
},
},
};
Ok(Self { query, variables })
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BraintreeRefundStatus {
SettlementPending,
Settling,
Settled,
SubmittedForSettlement,
Failed,
}
impl From<BraintreeRefundStatus> for enums::RefundStatus {
fn from(item: BraintreeRefundStatus) -> Self {
match item {
BraintreeRefundStatus::Settled | BraintreeRefundStatus::Settling => Self::Success,
BraintreeRefundStatus::SubmittedForSettlement
| BraintreeRefundStatus::SettlementPending => Self::Pending,
BraintreeRefundStatus::Failed => Self::Failure,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct BraintreeRefundTransactionBody {
pub id: String,
pub status: BraintreeRefundStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BraintreeRefundTransaction {
pub refund: BraintreeRefundTransactionBody,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BraintreeRefundResponseData {
pub refund_transaction: BraintreeRefundTransaction,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RefundResponse {
pub data: BraintreeRefundResponseData,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BraintreeRefundResponse {
RefundResponse(Box<RefundResponse>),
ErrorResponse(Box<ErrorResponse>),
}
impl TryFrom<types::RefundsResponseRouterData<api::Execute, BraintreeRefundResponse>>
for types::RefundsRouterData<api::Execute>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::RefundsResponseRouterData<api::Execute, BraintreeRefundResponse>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: match item.response {
BraintreeRefundResponse::ErrorResponse(error_response) => {
build_error_response(&error_response.errors, item.http_code)
}
BraintreeRefundResponse::RefundResponse(refund_data) => {
let refund_data = refund_data.data.refund_transaction.refund;
Ok(types::RefundsResponseData {
connector_refund_id: refund_data.id.clone(),
refund_status: enums::RefundStatus::from(refund_data.status.clone()),
})
}
},
..item.data
})
}
}
#[derive(Debug, Serialize)]
pub struct BraintreeRSyncRequest {
query: String,
}
impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundSyncRouterData) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?;
let refund_id = item.request.get_connector_refund_id()?;
let query = format!("query {{ search {{ refunds(input: {{ id: {{is: \"{}\"}} }}, first: 1) {{ edges {{ node {{ id status createdAt amount {{ value currencyCode }} orderId }} }} }} }} }}",refund_id);
Ok(Self { query })
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct RSyncNodeData {
id: String,
status: BraintreeRefundStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RSyncEdgeData {
node: RSyncNodeData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RefundData {
edges: Vec<RSyncEdgeData>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RSyncSearchData {
refunds: RefundData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RSyncResponseData {
search: RSyncSearchData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RSyncResponse {
data: RSyncResponseData,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BraintreeRSyncResponse {
RSyncResponse(Box<RSyncResponse>),
ErrorResponse(Box<ErrorResponse>),
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, BraintreeRSyncResponse>>
for types::RefundsRouterData<api::RSync>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, BraintreeRSyncResponse>,
) -> Result<Self, Self::Error> {
match item.response {
BraintreeRSyncResponse::ErrorResponse(error_response) => Ok(Self {
response: build_error_response(&error_response.errors, item.http_code),
..item.data
}),
BraintreeRSyncResponse::RSyncResponse(rsync_response) => {
let edge_data = rsync_response
.data
.search
.refunds
.edges
.first()
.ok_or(errors::ConnectorError::MissingConnectorRefundID)?;
let connector_refund_id = &edge_data.node.id;
let response = Ok(types::RefundsResponseData {
connector_refund_id: connector_refund_id.to_string(),
refund_status: enums::RefundStatus::from(edge_data.node.status.clone()),
});
Ok(Self {
response,
..item.data
})
}
}
}
}
#[derive(Default, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreditCardData {
number: cards::CardNumber,
expiration_year: Secret<String>,
expiration_month: Secret<String>,
cvv: Secret<String>,
cardholder_name: Secret<String>,
}
#[derive(Default, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InputData {
credit_card: CreditCardData,
}
#[derive(Default, Debug, Clone, Serialize)]
pub struct VariableInput {
input: InputData,
}
#[derive(Default, Debug, Clone, Serialize)]
pub struct BraintreeTokenRequest {
query: String,
variables: VariableInput,
}
impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::TokenizationRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(card_data) => {
let query = TOKENIZE_CREDIT_CARD.to_string();
let input = InputData {
credit_card: CreditCardData {
number: card_data.card_number,
expiration_year: card_data.card_exp_year,
expiration_month: card_data.card_exp_month,
cvv: card_data.card_cvc,
cardholder_name: card_data.card_holder_name,
},
};
Ok(Self {
query,
variables: VariableInput { input },
})
}
api_models::payments::PaymentMethodData::CardRedirect(_)
| api_models::payments::PaymentMethodData::Wallet(_)
| api_models::payments::PaymentMethodData::PayLater(_)
| api_models::payments::PaymentMethodData::BankRedirect(_)
| api_models::payments::PaymentMethodData::BankDebit(_)
| api_models::payments::PaymentMethodData::BankTransfer(_)
| api_models::payments::PaymentMethodData::Crypto(_)
| api_models::payments::PaymentMethodData::MandatePayment
| api_models::payments::PaymentMethodData::Reward(_)
| api_models::payments::PaymentMethodData::Upi(_)
| api_models::payments::PaymentMethodData::Voucher(_)
| api_models::payments::PaymentMethodData::GiftCard(_) => {
Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("braintree"),
)
.into())
}
}
}
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct TokenizePaymentMethodData {
id: String,
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenizeCreditCardData {
payment_method: TokenizePaymentMethodData,
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenizeCreditCard {
tokenize_credit_card: TokenizeCreditCardData,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct TokenResponse {
data: TokenizeCreditCard,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct ErrorResponse {
errors: Vec<ErrorDetails>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BraintreeTokenResponse {
TokenResponse(Box<TokenResponse>),
ErrorResponse(Box<ErrorResponse>),
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, BraintreeTokenResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, BraintreeTokenResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: match item.response {
BraintreeTokenResponse::ErrorResponse(error_response) => {
build_error_response(error_response.errors.as_ref(), item.http_code)
}
BraintreeTokenResponse::TokenResponse(token_response) => {
Ok(types::PaymentsResponseData::TokenizationResponse {
token: token_response
.data
.tokenize_credit_card
.payment_method
.id
.clone(),
})
}
},
..item.data
})
}
}
#[derive(Default, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureTransactionBody {
amount: String,
}
#[derive(Default, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureInputData {
transaction_id: String,
transaction: CaptureTransactionBody,
}
#[derive(Default, Debug, Clone, Serialize)]
pub struct VariableCaptureInput {
input: CaptureInputData,
}
#[derive(Default, Debug, Clone, Serialize)]
pub struct BraintreeCaptureRequest {
query: String,
variables: VariableCaptureInput,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for BraintreeCaptureRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
let query = CAPTURE_TRANSACTION_MUTATION.to_string();
let variables = VariableCaptureInput {
input: CaptureInputData {
transaction_id: item.request.connector_transaction_id.clone(),
transaction: CaptureTransactionBody {
amount: utils::to_currency_base_unit(
item.request.amount_to_capture,
item.request.currency,
)?,
},
},
};
Ok(Self { query, variables })
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CaptureResponseTransactionBody {
id: String,
status: BraintreePaymentStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CaptureTransactionData {
transaction: CaptureResponseTransactionBody,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureResponseData {
capture_transaction: CaptureTransactionData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CaptureResponse {
data: CaptureResponseData,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BraintreeCaptureResponse {
CaptureResponse(Box<CaptureResponse>),
ErrorResponse(Box<ErrorResponse>),
}
impl TryFrom<types::PaymentsCaptureResponseRouterData<BraintreeCaptureResponse>>
for types::PaymentsCaptureRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::PaymentsCaptureResponseRouterData<BraintreeCaptureResponse>,
) -> Result<Self, Self::Error> {
match item.response {
BraintreeCaptureResponse::CaptureResponse(capture_data) => {
let transaction_data = capture_data.data.capture_transaction.transaction;
Ok(Self {
status: enums::AttemptStatus::from(transaction_data.status.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
transaction_data.id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
}),
..item.data
})
}
BraintreeCaptureResponse::ErrorResponse(error_data) => Ok(Self {
response: build_error_response(&error_data.errors, item.http_code),
..item.data
}),
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelInputData {
transaction_id: String,
}
#[derive(Debug, Serialize)]
pub struct VariableCancelInput {
input: CancelInputData,
}
#[derive(Debug, Serialize)]
pub struct BraintreeCancelRequest {
query: String,
variables: VariableCancelInput,
}
impl TryFrom<&types::PaymentsCancelRouterData> for BraintreeCancelRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
let query = VOID_TRANSACTION_MUTATION.to_string();
let variables = VariableCancelInput {
input: CancelInputData {
transaction_id: item.request.connector_transaction_id.clone(),
},
};
Ok(Self { query, variables })
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CancelResponseTransactionBody {
id: String,
status: BraintreePaymentStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CancelTransactionData {
reversal: CancelResponseTransactionBody,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelResponseData {
reverse_transaction: CancelTransactionData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CancelResponse {
data: CancelResponseData,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BraintreeCancelResponse {
CancelResponse(Box<CancelResponse>),
ErrorResponse(Box<ErrorResponse>),
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, BraintreeCancelResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, BraintreeCancelResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
match item.response {
BraintreeCancelResponse::ErrorResponse(error_response) => Ok(Self {
response: build_error_response(&error_response.errors, item.http_code),
..item.data
}),
BraintreeCancelResponse::CancelResponse(void_response) => {
let void_data = void_response.data.reverse_transaction.reversal;
let transaction_id = void_data.id.clone();
Ok(Self {
status: enums::AttemptStatus::from(void_data.status.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
transaction_id.to_string(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
}),
..item.data
})
}
}
}
}
#[derive(Debug, Serialize)]
pub struct BraintreePSyncRequest {
query: String,
}
impl TryFrom<&types::PaymentsSyncRouterData> for BraintreePSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsSyncRouterData) -> Result<Self, Self::Error> {
let transaction_id = item
.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
let query = format!("query {{ search {{ transactions(input: {{ id: {{is: \"{}\"}} }}, first: 1) {{ edges {{ node {{ id status createdAt amount {{ value currencyCode }} orderId }} }} }} }} }}", transaction_id);
Ok(Self { query })
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct NodeData {
id: String,
status: BraintreePaymentStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct EdgeData {
node: NodeData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TransactionData {
edges: Vec<EdgeData>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SearchData {
transactions: TransactionData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PSyncResponseData {
search: SearchData,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BraintreePSyncResponse {
PSyncResponse(Box<PSyncResponse>),
ErrorResponse(Box<ErrorResponse>),
}
#[derive(Debug, Clone, Deserialize)]
pub struct PSyncResponse {
data: PSyncResponseData,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, BraintreePSyncResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, BraintreePSyncResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
match item.response {
BraintreePSyncResponse::ErrorResponse(error_response) => Ok(Self {
response: build_error_response(&error_response.errors, item.http_code),
..item.data
}),
BraintreePSyncResponse::PSyncResponse(psync_response) => {
let edge_data = psync_response
.data
.search
.transactions
.edges
.first()
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?;
let transaction_id = &edge_data.node.id;
Ok(Self {
status: enums::AttemptStatus::from(edge_data.node.status.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
transaction_id.to_string(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
}),
..item.data
})
}
}
}
}

View File

@ -4,7 +4,7 @@ use masking::{PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use crate::{
connector::utils,
connector::utils::{self},
consts,
core::errors,
types::{self, api, storage::enums},
@ -21,6 +21,7 @@ pub struct PaymentOptions {
#[derive(Debug, Deserialize)]
pub struct BraintreeMeta {
merchant_account_id: Option<Secret<String>>,
merchant_config_currency: Option<types::storage::enums::Currency>,
}
#[derive(Debug, Serialize, Eq, PartialEq)]
@ -41,6 +42,10 @@ pub struct BraintreeSessionRequest {
impl TryFrom<&types::PaymentsSessionRouterData> for BraintreeSessionRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(_item: &types::PaymentsSessionRouterData) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(_item.connector_meta_data.clone())?;
utils::validate_currency(_item.request.currency, metadata.merchant_config_currency)?;
Ok(Self {
client_token: BraintreeApiVersion {
version: "2".to_string(),
@ -93,12 +98,14 @@ pub struct CardDetails {
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?;
let submit_for_settlement = matches!(
item.request.capture_method,
Some(enums::CaptureMethod::Automatic) | None
);
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
let merchant_account_id = metadata.merchant_account_id;
let amount = utils::to_currency_base_unit(item.request.amount, item.request.currency)?;
let device_data = DeviceData {};
@ -354,9 +361,18 @@ pub struct Amount {
impl<F> TryFrom<&types::RefundsRouterData<F>> for BraintreeRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(_item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
let metadata: BraintreeMeta =
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?;
let refund_amount =
utils::to_currency_base_unit(item.request.refund_amount, item.request.currency)?;
Ok(Self {
transaction: Amount { amount: None },
transaction: Amount {
amount: Some(refund_amount),
},
})
}
}

View File

@ -1293,3 +1293,21 @@ mod error_code_error_message_tests {
assert_eq!(error_code_error_message_none, None);
}
}
pub fn validate_currency(
request_currency: types::storage::enums::Currency,
merchant_config_currency: Option<types::storage::enums::Currency>,
) -> Result<(), errors::ConnectorError> {
let merchant_config_currency =
merchant_config_currency.ok_or(errors::ConnectorError::NoConnectorMetaData)?;
if request_currency != merchant_config_currency {
Err(errors::ConnectorError::NotSupported {
message: format!(
"currency {} is not supported for this merchant account",
request_currency
),
connector: "Braintree",
})?
}
Ok(())
}

View File

@ -2483,6 +2483,7 @@ pub fn router_data_type_conversion<F1, F2, Req1, Req2, Res1, Res2>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode: router_data.test_mode,
connector_api_version: router_data.connector_api_version,
connector_http_status_code: router_data.connector_http_status_code,
}
}

View File

@ -1,4 +1,4 @@
use std::{fmt::Debug, marker::PhantomData};
use std::{fmt::Debug, marker::PhantomData, str::FromStr};
use common_utils::fp_utils;
use diesel_models::{ephemeral_key, payment_attempt::PaymentListFilters};
@ -99,6 +99,29 @@ where
let customer_id = customer.to_owned().map(|customer| customer.customer_id);
let supported_connector = &state
.conf
.multiple_api_version_supported_connectors
.supported_connectors;
let connector_enum = api_models::enums::Connector::from_str(connector_id)
.into_report()
.change_context(errors::ConnectorError::InvalidConnectorName)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "connector",
})
.attach_printable_lazy(|| format!("unable to parse connector name {connector_id:?}"))?;
let connector_api_version = if supported_connector.contains(&connector_enum) {
state
.store
.find_config_by_key_cached(&format!("connector_api_version_{connector_id}"))
.await
.map(|value| value.config)
.ok()
} else {
None
};
router_data = types::RouterData {
flow: PhantomData,
merchant_id: merchant_account.merchant_id.clone(),
@ -139,6 +162,7 @@ where
quote_id: None,
test_mode,
payment_method_balance: None,
connector_api_version,
connector_http_status_code: None,
};

View File

@ -1,4 +1,4 @@
use std::marker::PhantomData;
use std::{marker::PhantomData, str::FromStr};
use api_models::enums::{DisputeStage, DisputeStatus};
#[cfg(feature = "payouts")]
@ -183,6 +183,7 @@ pub async fn construct_payout_router_data<'a, F>(
quote_id: None,
test_mode,
payment_method_balance: None,
connector_api_version: None,
connector_http_status_code: None,
};
@ -239,6 +240,29 @@ pub async fn construct_refund_router_data<'a, F>(
));
let test_mode: Option<bool> = merchant_connector_account.is_test_mode_on();
let supported_connector = &state
.conf
.multiple_api_version_supported_connectors
.supported_connectors;
let connector_enum = api_models::enums::Connector::from_str(connector_id)
.into_report()
.change_context(errors::ConnectorError::InvalidConnectorName)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "connector",
})
.attach_printable_lazy(|| format!("unable to parse connector name {connector_id:?}"))?;
let connector_api_version = if supported_connector.contains(&connector_enum) {
state
.store
.find_config_by_key_cached(&format!("connector_api_version_{connector_id}"))
.await
.map(|value| value.config)
.ok()
} else {
None
};
let router_data = types::RouterData {
flow: PhantomData,
merchant_id: merchant_account.merchant_id.clone(),
@ -291,6 +315,7 @@ pub async fn construct_refund_router_data<'a, F>(
quote_id: None,
test_mode,
payment_method_balance: None,
connector_api_version,
connector_http_status_code: None,
};
@ -509,6 +534,7 @@ pub async fn construct_accept_dispute_router_data<'a>(
quote_id: None,
test_mode,
payment_method_balance: None,
connector_api_version: None,
connector_http_status_code: None,
};
Ok(router_data)
@ -584,6 +610,7 @@ pub async fn construct_submit_evidence_router_data<'a>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode,
connector_api_version: None,
connector_http_status_code: None,
};
Ok(router_data)
@ -660,6 +687,7 @@ pub async fn construct_upload_file_router_data<'a>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode,
connector_api_version: None,
connector_http_status_code: None,
};
Ok(router_data)
@ -738,6 +766,7 @@ pub async fn construct_defend_dispute_router_data<'a>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode,
connector_api_version: None,
connector_http_status_code: None,
};
Ok(router_data)
@ -811,6 +840,7 @@ pub async fn construct_retrieve_file_router_data<'a>(
#[cfg(feature = "payouts")]
quote_id: None,
test_mode,
connector_api_version: None,
connector_http_status_code: None,
};
Ok(router_data)

View File

@ -238,6 +238,10 @@ pub struct RouterData<Flow, Request, Response> {
pub preprocessing_id: Option<String>,
/// This is the balance amount for gift cards or voucher
pub payment_method_balance: Option<PaymentMethodBalance>,
///for switching between two different versions of the same connector
pub connector_api_version: Option<String>,
/// Contains flow-specific data required to construct a request and send it to the connector.
pub request: Request,
@ -954,6 +958,7 @@ impl<F1, F2, T1, T2> From<(&RouterData<F1, T1, PaymentsResponseData>, T2)>
quote_id: data.quote_id.clone(),
test_mode: data.test_mode,
payment_method_balance: data.payment_method_balance.clone(),
connector_api_version: data.connector_api_version.clone(),
connector_http_status_code: data.connector_http_status_code,
}
}
@ -1026,6 +1031,7 @@ impl<F1, F2>
quote_id: data.quote_id.clone(),
test_mode: data.test_mode,
payment_method_balance: None,
connector_api_version: None,
connector_http_status_code: data.connector_http_status_code,
}
}