mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
feat(connector): [Coinbase] [Opennode] Add support for crypto payments via PG redirection (#834)
Co-authored-by: arvindpatel24 <arvind.patel@juspay.in> Co-authored-by: Jagan Elavarasan <jaganelavarasan@gmail.com>
This commit is contained in:
@ -60,6 +60,7 @@ cards = [
|
||||
"bluesnap",
|
||||
"braintree",
|
||||
"checkout",
|
||||
"coinbase",
|
||||
"cybersource",
|
||||
"dlocal",
|
||||
"fiserv",
|
||||
@ -67,6 +68,7 @@ cards = [
|
||||
"mollie",
|
||||
"multisafepay",
|
||||
"nuvei",
|
||||
"opennode",
|
||||
"paypal",
|
||||
"payu",
|
||||
"shift4",
|
||||
@ -99,6 +101,7 @@ bambora.base_url = "https://api.na.bambora.com"
|
||||
bluesnap.base_url = "https://sandbox.bluesnap.com/"
|
||||
braintree.base_url = "https://api.sandbox.braintreegateway.com/"
|
||||
checkout.base_url = "https://api.sandbox.checkout.com/"
|
||||
coinbase.base_url = "https://api.commerce.coinbase.com"
|
||||
cybersource.base_url = "https://apitest.cybersource.com/"
|
||||
dlocal.base_url = "https://sandbox.dlocal.com/"
|
||||
fiserv.base_url = "https://cert.api.fiservapps.com/"
|
||||
@ -107,6 +110,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/"
|
||||
mollie.base_url = "https://api.mollie.com/v2/"
|
||||
multisafepay.base_url = "https://testapi.multisafepay.com/"
|
||||
nuvei.base_url = "https://ppp-test.nuvei.com/"
|
||||
opennode.base_url = "https://dev-api.opennode.com"
|
||||
paypal.base_url = "https://www.sandbox.paypal.com/"
|
||||
payu.base_url = "https://secure.snd.payu.com/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
|
||||
@ -133,6 +133,7 @@ bambora.base_url = "https://api.na.bambora.com"
|
||||
bluesnap.base_url = "https://sandbox.bluesnap.com/"
|
||||
braintree.base_url = "https://api.sandbox.braintreegateway.com/"
|
||||
checkout.base_url = "https://api.sandbox.checkout.com/"
|
||||
coinbase.base_url = "https://api.commerce.coinbase.com"
|
||||
cybersource.base_url = "https://apitest.cybersource.com/"
|
||||
dlocal.base_url = "https://sandbox.dlocal.com/"
|
||||
fiserv.base_url = "https://cert.api.fiservapps.com/"
|
||||
@ -141,6 +142,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/"
|
||||
mollie.base_url = "https://api.mollie.com/v2/"
|
||||
multisafepay.base_url = "https://testapi.multisafepay.com/"
|
||||
nuvei.base_url = "https://ppp-test.nuvei.com/"
|
||||
opennode.base_url = "https://dev-api.opennode.com"
|
||||
paypal.base_url = "https://www.sandbox.paypal.com/"
|
||||
payu.base_url = "https://secure.snd.payu.com/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
@ -157,6 +159,7 @@ wallets = ["klarna", "braintree", "applepay"]
|
||||
cards = [
|
||||
"adyen",
|
||||
"authorizedotnet",
|
||||
"coinbase",
|
||||
"braintree",
|
||||
"checkout",
|
||||
"cybersource",
|
||||
|
||||
@ -78,6 +78,7 @@ bambora.base_url = "https://api.na.bambora.com"
|
||||
bluesnap.base_url = "https://sandbox.bluesnap.com/"
|
||||
braintree.base_url = "https://api.sandbox.braintreegateway.com/"
|
||||
checkout.base_url = "https://api.sandbox.checkout.com/"
|
||||
coinbase.base_url = "https://api.commerce.coinbase.com"
|
||||
cybersource.base_url = "https://apitest.cybersource.com/"
|
||||
dlocal.base_url = "https://sandbox.dlocal.com/"
|
||||
fiserv.base_url = "https://cert.api.fiservapps.com/"
|
||||
@ -86,6 +87,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/"
|
||||
mollie.base_url = "https://api.mollie.com/v2/"
|
||||
multisafepay.base_url = "https://testapi.multisafepay.com/"
|
||||
nuvei.base_url = "https://ppp-test.nuvei.com/"
|
||||
opennode.base_url = "https://dev-api.opennode.com"
|
||||
paypal.base_url = "https://www.sandbox.paypal.com/"
|
||||
payu.base_url = "https://secure.snd.payu.com/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
@ -108,6 +110,7 @@ cards = [
|
||||
"bluesnap",
|
||||
"braintree",
|
||||
"checkout",
|
||||
"coinbase",
|
||||
"cybersource",
|
||||
"dlocal",
|
||||
"fiserv",
|
||||
@ -115,6 +118,7 @@ cards = [
|
||||
"mollie",
|
||||
"multisafepay",
|
||||
"nuvei",
|
||||
"opennode",
|
||||
"paypal",
|
||||
"payu",
|
||||
"shift4",
|
||||
|
||||
@ -34,6 +34,7 @@ pub enum AttemptStatus {
|
||||
VoidFailed,
|
||||
AutoRefunded,
|
||||
PartialCharged,
|
||||
Unresolved,
|
||||
#[default]
|
||||
Pending,
|
||||
Failure,
|
||||
@ -266,6 +267,8 @@ pub enum Currency {
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum EventType {
|
||||
PaymentSucceeded,
|
||||
PaymentProcessing,
|
||||
ActionRequired,
|
||||
RefundSucceeded,
|
||||
RefundFailed,
|
||||
DisputeOpened,
|
||||
@ -299,6 +302,7 @@ pub enum IntentStatus {
|
||||
Cancelled,
|
||||
Processing,
|
||||
RequiresCustomerAction,
|
||||
RequiresMerchantAction,
|
||||
RequiresPaymentMethod,
|
||||
#[default]
|
||||
RequiresConfirmation,
|
||||
@ -416,6 +420,7 @@ pub enum PaymentMethodType {
|
||||
GooglePay,
|
||||
ApplePay,
|
||||
Paypal,
|
||||
CryptoCurrency,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@ -441,6 +446,7 @@ pub enum PaymentMethod {
|
||||
PayLater,
|
||||
Wallet,
|
||||
BankRedirect,
|
||||
Crypto,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@ -561,9 +567,11 @@ pub enum Connector {
|
||||
Bluesnap,
|
||||
Braintree,
|
||||
Checkout,
|
||||
Coinbase,
|
||||
Cybersource,
|
||||
#[default]
|
||||
Dummy,
|
||||
Opennode,
|
||||
Bambora,
|
||||
Dlocal,
|
||||
Fiserv,
|
||||
@ -618,6 +626,7 @@ pub enum RoutableConnectors {
|
||||
Bluesnap,
|
||||
Braintree,
|
||||
Checkout,
|
||||
Coinbase,
|
||||
Cybersource,
|
||||
Dlocal,
|
||||
Fiserv,
|
||||
@ -626,6 +635,7 @@ pub enum RoutableConnectors {
|
||||
Mollie,
|
||||
Multisafepay,
|
||||
Nuvei,
|
||||
Opennode,
|
||||
Paypal,
|
||||
Payu,
|
||||
Rapyd,
|
||||
@ -758,6 +768,7 @@ impl From<AttemptStatus> for IntentStatus {
|
||||
|
||||
AttemptStatus::Authorized => Self::RequiresCapture,
|
||||
AttemptStatus::AuthenticationPending => Self::RequiresCustomerAction,
|
||||
AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
||||
|
||||
AttemptStatus::PartialCharged
|
||||
| AttemptStatus::Started
|
||||
@ -826,3 +837,10 @@ pub enum DisputeStatus {
|
||||
// dispute has been unsuccessfully challenged
|
||||
DisputeLost,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UnresolvedResponseReason {
|
||||
pub code: String,
|
||||
/// A message to merchant to give hint on next action he/she should do to resolve
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
@ -441,6 +441,7 @@ pub enum PaymentMethodData {
|
||||
Wallet(WalletData),
|
||||
PayLater(PayLaterData),
|
||||
BankRedirect(BankRedirectData),
|
||||
Crypto(CryptoData),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
@ -455,6 +456,7 @@ pub enum AdditionalPaymentData {
|
||||
},
|
||||
Wallet {},
|
||||
PayLater {},
|
||||
Crypto {},
|
||||
}
|
||||
|
||||
impl From<&PaymentMethodData> for AdditionalPaymentData {
|
||||
@ -478,6 +480,7 @@ impl From<&PaymentMethodData> for AdditionalPaymentData {
|
||||
},
|
||||
PaymentMethodData::Wallet(_) => Self::Wallet {},
|
||||
PaymentMethodData::PayLater(_) => Self::PayLater {},
|
||||
PaymentMethodData::Crypto(_) => Self::Crypto {},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -516,6 +519,10 @@ pub enum BankRedirectData {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CryptoData {}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
pub struct SofortBilling {
|
||||
/// The country associated with the billing
|
||||
@ -620,6 +627,7 @@ pub enum PaymentMethodDataResponse {
|
||||
PayLater(PayLaterData),
|
||||
Paypal,
|
||||
BankRedirect(BankRedirectData),
|
||||
Crypto(CryptoData),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||
@ -1125,6 +1133,7 @@ impl From<PaymentMethodData> for PaymentMethodDataResponse {
|
||||
PaymentMethodData::BankRedirect(bank_redirect_data) => {
|
||||
Self::BankRedirect(bank_redirect_data)
|
||||
}
|
||||
PaymentMethodData::Crypto(crpto_data) => Self::Crypto(crpto_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,9 @@ use crate::{disputes, enums as api_enums, payments, refunds};
|
||||
pub enum IncomingWebhookEvent {
|
||||
PaymentIntentFailure,
|
||||
PaymentIntentSuccess,
|
||||
PaymentIntentProcessing,
|
||||
PaymentActionRequired,
|
||||
EventNotSupported,
|
||||
RefundFailure,
|
||||
RefundSuccess,
|
||||
DisputeOpened,
|
||||
@ -36,6 +39,9 @@ impl From<IncomingWebhookEvent> for WebhookFlow {
|
||||
match evt {
|
||||
IncomingWebhookEvent::PaymentIntentFailure => Self::Payment,
|
||||
IncomingWebhookEvent::PaymentIntentSuccess => Self::Payment,
|
||||
IncomingWebhookEvent::PaymentIntentProcessing => Self::Payment,
|
||||
IncomingWebhookEvent::PaymentActionRequired => Self::Payment,
|
||||
IncomingWebhookEvent::EventNotSupported => Self::Payment,
|
||||
IncomingWebhookEvent::RefundSuccess => Self::Refund,
|
||||
IncomingWebhookEvent::RefundFailure => Self::Refund,
|
||||
IncomingWebhookEvent::DisputeOpened => Self::Dispute,
|
||||
|
||||
@ -221,6 +221,7 @@ impl From<api_enums::IntentStatus> for StripePaymentStatus {
|
||||
api_enums::IntentStatus::Failed => Self::Canceled,
|
||||
api_enums::IntentStatus::Processing => Self::Processing,
|
||||
api_enums::IntentStatus::RequiresCustomerAction => Self::RequiresAction,
|
||||
api_enums::IntentStatus::RequiresMerchantAction => Self::RequiresAction,
|
||||
api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod,
|
||||
api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation,
|
||||
api_enums::IntentStatus::RequiresCapture => Self::RequiresCapture,
|
||||
|
||||
@ -187,6 +187,7 @@ impl From<api_enums::IntentStatus> for StripeSetupStatus {
|
||||
api_enums::IntentStatus::Failed => Self::Canceled,
|
||||
api_enums::IntentStatus::Processing => Self::Processing,
|
||||
api_enums::IntentStatus::RequiresCustomerAction => Self::RequiresAction,
|
||||
api_enums::IntentStatus::RequiresMerchantAction => Self::RequiresAction,
|
||||
api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod,
|
||||
api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation,
|
||||
api_enums::IntentStatus::RequiresCapture => {
|
||||
|
||||
@ -255,6 +255,7 @@ pub struct Connectors {
|
||||
pub bluesnap: ConnectorParams,
|
||||
pub braintree: ConnectorParams,
|
||||
pub checkout: ConnectorParams,
|
||||
pub coinbase: ConnectorParams,
|
||||
pub cybersource: ConnectorParams,
|
||||
pub dlocal: ConnectorParams,
|
||||
pub fiserv: ConnectorParams,
|
||||
@ -263,6 +264,7 @@ pub struct Connectors {
|
||||
pub mollie: ConnectorParams,
|
||||
pub multisafepay: ConnectorParams,
|
||||
pub nuvei: ConnectorParams,
|
||||
pub opennode: ConnectorParams,
|
||||
pub paypal: ConnectorParams,
|
||||
pub payu: ConnectorParams,
|
||||
pub rapyd: ConnectorParams,
|
||||
|
||||
@ -7,6 +7,7 @@ pub mod bambora;
|
||||
pub mod bluesnap;
|
||||
pub mod braintree;
|
||||
pub mod checkout;
|
||||
pub mod coinbase;
|
||||
pub mod cybersource;
|
||||
pub mod dlocal;
|
||||
pub mod fiserv;
|
||||
@ -14,6 +15,7 @@ pub mod globalpay;
|
||||
pub mod klarna;
|
||||
pub mod multisafepay;
|
||||
pub mod nuvei;
|
||||
pub mod opennode;
|
||||
pub mod paypal;
|
||||
pub mod payu;
|
||||
pub mod rapyd;
|
||||
@ -29,8 +31,9 @@ pub mod mollie;
|
||||
pub use self::{
|
||||
aci::Aci, adyen::Adyen, airwallex::Airwallex, applepay::Applepay,
|
||||
authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree,
|
||||
checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv,
|
||||
globalpay::Globalpay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nuvei::Nuvei,
|
||||
paypal::Paypal, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay,
|
||||
worldline::Worldline, worldpay::Worldpay,
|
||||
checkout::Checkout, coinbase::Coinbase, cybersource::Cybersource, dlocal::Dlocal,
|
||||
fiserv::Fiserv, globalpay::Globalpay, klarna::Klarna, mollie::Mollie,
|
||||
multisafepay::Multisafepay, nuvei::Nuvei, opennode::Opennode, paypal::Paypal, payu::Payu,
|
||||
rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, worldline::Worldline,
|
||||
worldpay::Worldpay,
|
||||
};
|
||||
|
||||
@ -111,6 +111,11 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest {
|
||||
api::PaymentMethodData::PayLater(_) => PaymentDetails::Klarna,
|
||||
api::PaymentMethodData::Wallet(_) => PaymentDetails::Wallet,
|
||||
api::PaymentMethodData::BankRedirect(_) => PaymentDetails::BankRedirect,
|
||||
api::PaymentMethodData::Crypto(_) => Err(errors::ConnectorError::NotSupported {
|
||||
payment_method: format!("{:?}", item.payment_method),
|
||||
connector: "Aci",
|
||||
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl.to_string(),
|
||||
})?,
|
||||
};
|
||||
|
||||
let auth = AciAuthType::try_from(&item.connector_auth_type)?;
|
||||
|
||||
@ -431,6 +431,14 @@ impl<'a> TryFrom<&types::PaymentsAuthorizeRouterData> for AdyenPaymentRequest<'a
|
||||
storage_models::enums::PaymentMethod::BankRedirect => {
|
||||
get_bank_redirect_specific_payment_data(item)
|
||||
}
|
||||
storage_models::enums::PaymentMethod::Crypto => {
|
||||
Err(errors::ConnectorError::NotSupported {
|
||||
payment_method: format!("{:?}", item.payment_method),
|
||||
connector: "Adyen",
|
||||
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl
|
||||
.to_string(),
|
||||
})?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -645,6 +653,11 @@ fn get_payment_method_data<'a>(
|
||||
}
|
||||
}
|
||||
}
|
||||
api::PaymentMethodData::Crypto(_) => Err(errors::ConnectorError::NotSupported {
|
||||
payment_method: format!("{:?}", item.payment_method),
|
||||
connector: "Adyen",
|
||||
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl.to_string(),
|
||||
})?,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -68,11 +68,12 @@ enum PaymentDetails {
|
||||
BankRedirect,
|
||||
}
|
||||
|
||||
impl From<api_models::payments::PaymentMethodData> for PaymentDetails {
|
||||
fn from(value: api_models::payments::PaymentMethodData) -> Self {
|
||||
impl TryFrom<api_models::payments::PaymentMethodData> for PaymentDetails {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(value: api_models::payments::PaymentMethodData) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
api::PaymentMethodData::Card(ref ccard) => {
|
||||
Self::CreditCard(CreditCardDetails {
|
||||
Ok(Self::CreditCard(CreditCardDetails {
|
||||
card_number: ccard.card_number.clone(),
|
||||
// expiration_date: format!("{expiry_year}-{expiry_month}").into(),
|
||||
expiration_date: ccard
|
||||
@ -81,11 +82,16 @@ impl From<api_models::payments::PaymentMethodData> for PaymentDetails {
|
||||
.zip(ccard.card_exp_year.clone())
|
||||
.map(|(expiry_month, expiry_year)| format!("{expiry_year}-{expiry_month}")),
|
||||
card_code: Some(ccard.card_cvc.clone()),
|
||||
})
|
||||
}))
|
||||
}
|
||||
api::PaymentMethodData::PayLater(_) => Self::Klarna,
|
||||
api::PaymentMethodData::Wallet(_) => Self::Wallet,
|
||||
api::PaymentMethodData::BankRedirect(_) => Self::BankRedirect,
|
||||
api::PaymentMethodData::PayLater(_) => Ok(Self::Klarna),
|
||||
api::PaymentMethodData::Wallet(_) => Ok(Self::Wallet),
|
||||
api::PaymentMethodData::BankRedirect(_) => Ok(Self::BankRedirect),
|
||||
api::PaymentMethodData::Crypto(_) => Err(errors::ConnectorError::NotSupported {
|
||||
payment_method: format!("{value:?}"),
|
||||
connector: "AuthorizeDotNet",
|
||||
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl.to_string(),
|
||||
})?,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,7 +165,7 @@ impl From<enums::CaptureMethod> for AuthorizationType {
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for CreateTransactionRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
let payment_details = item.request.payment_method_data.clone().into();
|
||||
let payment_details = PaymentDetails::try_from(item.request.payment_method_data.clone())?;
|
||||
let authorization_indicator_type =
|
||||
item.request.capture_method.map(|c| AuthorizationIndicator {
|
||||
authorization_indicator: c.into(),
|
||||
|
||||
@ -72,7 +72,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentsRequest {
|
||||
api::PaymentMethodData::Card(ref ccard) => Some(ccard),
|
||||
api::PaymentMethodData::Wallet(_)
|
||||
| api::PaymentMethodData::PayLater(_)
|
||||
| api::PaymentMethodData::BankRedirect(_) => None,
|
||||
| api::PaymentMethodData::BankRedirect(_)
|
||||
| api::PaymentMethodData::Crypto(_) => None,
|
||||
};
|
||||
|
||||
let three_ds = match item.auth_type {
|
||||
|
||||
570
crates/router/src/connector/coinbase.rs
Normal file
570
crates/router/src/connector/coinbase.rs
Normal file
@ -0,0 +1,570 @@
|
||||
mod transformers;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use common_utils::{crypto, ext_traits::ByteSliceExt};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use transformers as coinbase;
|
||||
|
||||
use self::coinbase::CoinbaseWebhookDetails;
|
||||
use super::utils;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::errors::{self, CustomResult},
|
||||
db, headers,
|
||||
services::{self, ConnectorIntegration},
|
||||
types::{
|
||||
self,
|
||||
api::{self, ConnectorCommon, ConnectorCommonExt},
|
||||
ErrorResponse, Response,
|
||||
},
|
||||
utils::{BytesExt, Encode},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Coinbase;
|
||||
|
||||
impl api::Payment for Coinbase {}
|
||||
impl api::PaymentSession for Coinbase {}
|
||||
impl api::ConnectorAccessToken for Coinbase {}
|
||||
impl api::PreVerify for Coinbase {}
|
||||
impl api::PaymentAuthorize for Coinbase {}
|
||||
impl api::PaymentSync for Coinbase {}
|
||||
impl api::PaymentCapture for Coinbase {}
|
||||
impl api::PaymentVoid for Coinbase {}
|
||||
impl api::Refund for Coinbase {}
|
||||
impl api::RefundExecute for Coinbase {}
|
||||
impl api::RefundSync for Coinbase {}
|
||||
|
||||
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Coinbase
|
||||
where
|
||||
Self: ConnectorIntegration<Flow, Request, Response>,
|
||||
{
|
||||
fn build_headers(
|
||||
&self,
|
||||
req: &types::RouterData<Flow, Request, Response>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let mut header = vec![
|
||||
(
|
||||
headers::CONTENT_TYPE.to_string(),
|
||||
self.common_get_content_type().to_string(),
|
||||
),
|
||||
(headers::X_CC_VERSION.to_string(), "2018-03-22".to_string()),
|
||||
];
|
||||
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
|
||||
header.append(&mut api_key);
|
||||
Ok(header)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorCommon for Coinbase {
|
||||
fn id(&self) -> &'static str {
|
||||
"coinbase"
|
||||
}
|
||||
|
||||
fn common_get_content_type(&self) -> &'static str {
|
||||
"application/json"
|
||||
}
|
||||
|
||||
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
|
||||
connectors.coinbase.base_url.as_ref()
|
||||
}
|
||||
|
||||
fn get_auth_header(
|
||||
&self,
|
||||
auth_type: &types::ConnectorAuthType,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let auth: coinbase::CoinbaseAuthType = coinbase::CoinbaseAuthType::try_from(auth_type)
|
||||
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
Ok(vec![(headers::X_CC_API_KEY.to_string(), auth.api_key)])
|
||||
}
|
||||
|
||||
fn build_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
let response: coinbase::CoinbaseErrorResponse = res
|
||||
.response
|
||||
.parse_struct("CoinbaseErrorResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
Ok(ErrorResponse {
|
||||
status_code: res.status_code,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
reason: response.reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Coinbase
|
||||
{
|
||||
//TODO: implement sessions flow
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
|
||||
for Coinbase
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||
for Coinbase
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
for Coinbase
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsAuthorizeRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!("{}/charges", self.base_url(_connectors)))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req_obj = coinbase::CoinbasePaymentsRequest::try_from(req)?;
|
||||
let coinbase_req =
|
||||
Encode::<coinbase::CoinbasePaymentsRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(coinbase_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.headers(types::PaymentsAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsAuthorizeRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: coinbase::CoinbasePaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Coinbase PaymentsAuthorizeResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
for Coinbase
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsSyncRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let connector_id = _req
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
Ok(format!(
|
||||
"{}/charges/{}",
|
||||
self.base_url(_connectors),
|
||||
connector_id
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Get)
|
||||
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
|
||||
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
let response: coinbase::CoinbasePaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("coinbase PaymentsSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
||||
for Coinbase
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCaptureRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsCaptureRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
_req: &types::PaymentsCaptureRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into())
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsCaptureRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
|
||||
.headers(types::PaymentsCaptureType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsCaptureRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
|
||||
let response: coinbase::CoinbasePaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Coinbase PaymentsCaptureResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
|
||||
for Coinbase
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
|
||||
for Coinbase
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::RefundsRouterData<api::Execute>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req_obj = coinbase::CoinbaseRefundRequest::try_from(req)?;
|
||||
let coinbase_req =
|
||||
Encode::<coinbase::CoinbaseRefundRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(coinbase_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
let request = services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
|
||||
.headers(types::RefundExecuteType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::RefundExecuteType::get_request_body(self, req)?)
|
||||
.build();
|
||||
Ok(Some(request))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefundsRouterData<api::Execute>,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
|
||||
let response: coinbase::RefundResponse = res
|
||||
.response
|
||||
.parse_struct("coinbase RefundResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Coinbase {
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::RefundSyncRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Get)
|
||||
.url(&types::RefundSyncType::get_url(self, req, connectors)?)
|
||||
.headers(types::RefundSyncType::get_headers(self, req, connectors)?)
|
||||
.body(types::RefundSyncType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefundSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
|
||||
let response: coinbase::RefundResponse = res
|
||||
.response
|
||||
.parse_struct("coinbase RefundSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl api::IncomingWebhook for Coinbase {
|
||||
fn get_webhook_source_verification_algorithm(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
|
||||
Ok(Box::new(crypto::HmacSha256))
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_signature(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let base64_signature =
|
||||
utils::get_header_key_value("X-CC-Webhook-Signature", request.headers)?;
|
||||
hex::decode(base64_signature)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_message(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
_merchant_id: &str,
|
||||
_secret: &[u8],
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let message = std::str::from_utf8(request.body)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||
Ok(message.to_string().into_bytes())
|
||||
}
|
||||
|
||||
async fn get_webhook_source_verification_merchant_secret(
|
||||
&self,
|
||||
db: &dyn db::StorageInterface,
|
||||
merchant_id: &str,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let key = format!("whsec_verification_{}_{}", self.id(), merchant_id);
|
||||
let secret = db
|
||||
.get_key(&key)
|
||||
.await
|
||||
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?;
|
||||
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
fn get_webhook_object_reference_id(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
||||
let notif: CoinbaseWebhookDetails = request
|
||||
.body
|
||||
.parse_struct("CoinbaseWebhookDetails")
|
||||
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
|
||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(notif.event.data.id),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
let notif: CoinbaseWebhookDetails = request
|
||||
.body
|
||||
.parse_struct("CoinbaseWebhookDetails")
|
||||
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||
match notif.event.event_type {
|
||||
coinbase::WebhookEventType::Confirmed | coinbase::WebhookEventType::Resolved => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentIntentSuccess)
|
||||
}
|
||||
coinbase::WebhookEventType::Failed => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentActionRequired)
|
||||
}
|
||||
coinbase::WebhookEventType::Pending => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentIntentProcessing)
|
||||
}
|
||||
_ => Ok(api::IncomingWebhookEvent::EventNotSupported),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_webhook_resource_object(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||
let notif: CoinbaseWebhookDetails = request
|
||||
.body
|
||||
.parse_struct("CoinbaseWebhookDetails")
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
Encode::<CoinbaseWebhookDetails>::encode_to_value(¬if.event)
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)
|
||||
}
|
||||
}
|
||||
405
crates/router/src/connector/coinbase/transformers.rs
Normal file
405
crates/router/src/connector/coinbase/transformers.rs
Normal file
@ -0,0 +1,405 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
connector::utils::{self, AddressDetailsData, PaymentsAuthorizeRequestData, RouterData},
|
||||
core::errors,
|
||||
pii::Secret,
|
||||
services,
|
||||
types::{self, api, storage::enums},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Eq, PartialEq, Serialize)]
|
||||
pub struct LocalPrice {
|
||||
pub amount: String,
|
||||
pub currency: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
pub customer_id: String,
|
||||
pub customer_name: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
pub struct CoinbasePaymentsRequest {
|
||||
pub name: Secret<String>,
|
||||
pub description: String,
|
||||
pub pricing_type: String,
|
||||
pub local_price: LocalPrice,
|
||||
pub redirect_url: String,
|
||||
pub cancel_url: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for CoinbasePaymentsRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
get_crypto_specific_payment_data(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Auth Struct
|
||||
pub struct CoinbaseAuthType {
|
||||
pub(super) api_key: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::ConnectorAuthType> for CoinbaseAuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(_auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
if let types::ConnectorAuthType::HeaderKey { api_key } = _auth_type {
|
||||
Ok(Self {
|
||||
api_key: api_key.to_string(),
|
||||
})
|
||||
} else {
|
||||
Err(errors::ConnectorError::FailedToObtainAuthType.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
// PaymentsResponse
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum CoinbasePaymentStatus {
|
||||
New,
|
||||
#[default]
|
||||
Pending,
|
||||
Completed,
|
||||
Expired,
|
||||
Unresolved,
|
||||
Resolved,
|
||||
Cancelled,
|
||||
#[serde(rename = "PENDING REFUND")]
|
||||
PendingRefund,
|
||||
Refunded,
|
||||
}
|
||||
|
||||
impl From<CoinbasePaymentStatus> for enums::AttemptStatus {
|
||||
fn from(item: CoinbasePaymentStatus) -> Self {
|
||||
match item {
|
||||
CoinbasePaymentStatus::Completed | CoinbasePaymentStatus::Resolved => Self::Charged,
|
||||
CoinbasePaymentStatus::Expired => Self::Failure,
|
||||
CoinbasePaymentStatus::New => Self::AuthenticationPending,
|
||||
CoinbasePaymentStatus::Unresolved => Self::Unresolved,
|
||||
_ => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, strum::Display)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
pub enum UnResolvedContext {
|
||||
Underpaid,
|
||||
Overpaid,
|
||||
Delayed,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Timeline {
|
||||
status: CoinbasePaymentStatus,
|
||||
context: Option<UnResolvedContext>,
|
||||
time: String,
|
||||
pub payment: Option<TimelinePayment>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub struct CoinbasePaymentsResponse {
|
||||
// status: CoinbasePaymentStatus,
|
||||
// id: String,
|
||||
data: CoinbasePaymentResponseData,
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, CoinbasePaymentsResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
CoinbasePaymentsResponse,
|
||||
T,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let form_fields = HashMap::new();
|
||||
let redirection_data = services::RedirectForm {
|
||||
endpoint: item.response.data.hosted_url.to_string(),
|
||||
method: services::Method::Get,
|
||||
form_fields,
|
||||
};
|
||||
let timeline = item
|
||||
.response
|
||||
.data
|
||||
.timeline
|
||||
.last()
|
||||
.ok_or_else(|| errors::ConnectorError::ResponseHandlingFailed)?
|
||||
.clone();
|
||||
let connector_id = types::ResponseId::ConnectorTransactionId(item.response.data.id);
|
||||
let attempt_status = timeline.status.clone();
|
||||
let response_data = timeline.context.map_or(
|
||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: connector_id.clone(),
|
||||
redirection_data: Some(redirection_data),
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
}),
|
||||
|context| {
|
||||
Ok(types::PaymentsResponseData::TransactionUnresolvedResponse{
|
||||
resource_id: connector_id,
|
||||
reason: Some(api::enums::UnresolvedResponseReason {
|
||||
code: context.to_string(),
|
||||
message: "Please check the transaction in coinbase dashboard and resolve manually"
|
||||
.to_string(),
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::from(attempt_status),
|
||||
response: response_data,
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// REFUND :
|
||||
// Type definition for RefundRequest
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
pub struct CoinbaseRefundRequest {}
|
||||
|
||||
impl<F> TryFrom<&types::RefundsRouterData<F>> for CoinbaseRefundRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(_item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||
Err(errors::ConnectorError::NotImplemented("try_from RefundsRouterData".to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
// Type definition for Refund Response
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
|
||||
pub enum RefundStatus {
|
||||
Succeeded,
|
||||
Failed,
|
||||
#[default]
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl From<RefundStatus> for enums::RefundStatus {
|
||||
fn from(item: RefundStatus) -> Self {
|
||||
match item {
|
||||
RefundStatus::Succeeded => Self::Success,
|
||||
RefundStatus::Failed => Self::Failure,
|
||||
RefundStatus::Processing => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RefundResponse {}
|
||||
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
for types::RefundsRouterData<api::Execute>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
_item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Err(errors::ConnectorError::NotImplemented(
|
||||
"try_from RefundsResponseRouterData".to_string(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
|
||||
for types::RefundsRouterData<api::RSync>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
_item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Err(errors::ConnectorError::NotImplemented(
|
||||
"try_from RefundsResponseRouterData".to_string(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct CoinbaseErrorResponse {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, PartialEq)]
|
||||
pub struct CoinbaseConnectorMeta {
|
||||
pub pricing_type: String,
|
||||
}
|
||||
|
||||
fn get_crypto_specific_payment_data(
|
||||
item: &types::PaymentsAuthorizeRouterData,
|
||||
) -> Result<CoinbasePaymentsRequest, error_stack::Report<errors::ConnectorError>> {
|
||||
let billing_address = item
|
||||
.get_billing()?
|
||||
.address
|
||||
.as_ref()
|
||||
.ok_or_else(utils::missing_field_err("billing.address"))?;
|
||||
let name = billing_address.get_first_name()?.to_owned();
|
||||
let description = item.get_description()?;
|
||||
let connector_meta: CoinbaseConnectorMeta =
|
||||
utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?;
|
||||
let pricing_type = connector_meta.pricing_type;
|
||||
let local_price = get_local_price(item);
|
||||
let redirect_url = item.request.get_return_url()?;
|
||||
let cancel_url = item.request.get_return_url()?;
|
||||
|
||||
Ok(CoinbasePaymentsRequest {
|
||||
name,
|
||||
description,
|
||||
pricing_type,
|
||||
local_price,
|
||||
redirect_url,
|
||||
cancel_url,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_local_price(item: &types::PaymentsAuthorizeRouterData) -> LocalPrice {
|
||||
LocalPrice {
|
||||
amount: format!("{:?}", item.request.amount),
|
||||
currency: item.request.currency.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CoinbaseWebhookDetails {
|
||||
pub attempt_number: i64,
|
||||
pub event: Event,
|
||||
pub id: String,
|
||||
pub scheduled_for: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Event {
|
||||
pub api_version: String,
|
||||
pub created_at: String,
|
||||
pub data: CoinbasePaymentResponseData,
|
||||
pub id: String,
|
||||
pub resource: String,
|
||||
#[serde(rename = "type")]
|
||||
pub event_type: WebhookEventType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum WebhookEventType {
|
||||
#[serde(rename = "charge:confirmed")]
|
||||
Confirmed,
|
||||
#[serde(rename = "charge:created")]
|
||||
Created,
|
||||
#[serde(rename = "charge:pending")]
|
||||
Pending,
|
||||
#[serde(rename = "charge:failed")]
|
||||
Failed,
|
||||
#[serde(rename = "charge:resolved")]
|
||||
Resolved,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct CoinbasePaymentResponseData {
|
||||
pub id: String,
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
pub utxo: bool,
|
||||
pub pricing: HashMap<String, OverpaymentAbsoluteThreshold>,
|
||||
pub fee_rate: f64,
|
||||
pub logo_url: String,
|
||||
pub metadata: Metadata,
|
||||
pub payments: Vec<PaymentElement>,
|
||||
pub resource: String,
|
||||
pub timeline: Vec<Timeline>,
|
||||
pub pwcb_only: bool,
|
||||
pub cancel_url: String,
|
||||
pub created_at: String,
|
||||
pub expires_at: String,
|
||||
pub hosted_url: String,
|
||||
pub brand_color: String,
|
||||
pub description: String,
|
||||
pub confirmed_at: Option<String>,
|
||||
pub fees_settled: bool,
|
||||
pub pricing_type: String,
|
||||
pub redirect_url: String,
|
||||
pub support_email: String,
|
||||
pub brand_logo_url: String,
|
||||
pub offchain_eligible: bool,
|
||||
pub organization_name: String,
|
||||
pub payment_threshold: PaymentThreshold,
|
||||
pub coinbase_managed_merchant: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Default, Deserialize)]
|
||||
pub struct PaymentThreshold {
|
||||
pub overpayment_absolute_threshold: OverpaymentAbsoluteThreshold,
|
||||
pub overpayment_relative_threshold: String,
|
||||
pub underpayment_absolute_threshold: OverpaymentAbsoluteThreshold,
|
||||
pub underpayment_relative_threshold: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq, Eq)]
|
||||
pub struct OverpaymentAbsoluteThreshold {
|
||||
pub amount: String,
|
||||
pub currency: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PaymentElement {
|
||||
pub net: CoinbaseProcessingFee,
|
||||
pub block: Block,
|
||||
pub value: CoinbaseProcessingFee,
|
||||
pub status: String,
|
||||
pub network: String,
|
||||
pub deposited: Deposited,
|
||||
pub payment_id: String,
|
||||
pub detected_at: String,
|
||||
pub transaction_id: String,
|
||||
pub coinbase_processing_fee: CoinbaseProcessingFee,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Block {
|
||||
pub hash: Option<String>,
|
||||
pub height: Option<i64>,
|
||||
pub confirmations: Option<i64>,
|
||||
pub confirmations_required: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CoinbaseProcessingFee {
|
||||
pub local: Option<OverpaymentAbsoluteThreshold>,
|
||||
pub crypto: OverpaymentAbsoluteThreshold,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Deposited {
|
||||
pub amount: Amount,
|
||||
pub status: String,
|
||||
pub destination: String,
|
||||
pub exchange_rate: Option<serde_json::Value>,
|
||||
pub autoconversion_status: String,
|
||||
pub autoconversion_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Amount {
|
||||
pub net: CoinbaseProcessingFee,
|
||||
pub gross: CoinbaseProcessingFee,
|
||||
pub coinbase_fee: CoinbaseProcessingFee,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TimelinePayment {
|
||||
pub value: OverpaymentAbsoluteThreshold,
|
||||
pub network: String,
|
||||
pub transaction_id: String,
|
||||
}
|
||||
575
crates/router/src/connector/opennode.rs
Normal file
575
crates/router/src/connector/opennode.rs
Normal file
@ -0,0 +1,575 @@
|
||||
mod transformers;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use common_utils::crypto;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use transformers as opennode;
|
||||
|
||||
use self::opennode::OpennodeWebhookDetails;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::errors::{self, CustomResult},
|
||||
db, headers,
|
||||
services::{self, ConnectorIntegration},
|
||||
types::{
|
||||
self,
|
||||
api::{self, ConnectorCommon, ConnectorCommonExt},
|
||||
ErrorResponse, Response,
|
||||
},
|
||||
utils::{BytesExt, Encode},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Opennode;
|
||||
|
||||
impl api::Payment for Opennode {}
|
||||
impl api::PaymentSession for Opennode {}
|
||||
impl api::ConnectorAccessToken for Opennode {}
|
||||
impl api::PreVerify for Opennode {}
|
||||
impl api::PaymentAuthorize for Opennode {}
|
||||
impl api::PaymentSync for Opennode {}
|
||||
impl api::PaymentCapture for Opennode {}
|
||||
impl api::PaymentVoid for Opennode {}
|
||||
impl api::Refund for Opennode {}
|
||||
impl api::RefundExecute for Opennode {}
|
||||
impl api::RefundSync for Opennode {}
|
||||
|
||||
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Opennode
|
||||
where
|
||||
Self: ConnectorIntegration<Flow, Request, Response>,
|
||||
{
|
||||
fn build_headers(
|
||||
&self,
|
||||
req: &types::RouterData<Flow, Request, Response>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let mut header = vec![
|
||||
(
|
||||
headers::CONTENT_TYPE.to_string(),
|
||||
self.common_get_content_type().to_string(),
|
||||
),
|
||||
(
|
||||
headers::ACCEPT.to_string(),
|
||||
self.common_get_content_type().to_string(),
|
||||
),
|
||||
];
|
||||
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
|
||||
header.append(&mut api_key);
|
||||
Ok(header)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorCommon for Opennode {
|
||||
fn id(&self) -> &'static str {
|
||||
"opennode"
|
||||
}
|
||||
|
||||
fn common_get_content_type(&self) -> &'static str {
|
||||
"application/json"
|
||||
}
|
||||
|
||||
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
|
||||
connectors.opennode.base_url.as_ref()
|
||||
}
|
||||
|
||||
fn get_auth_header(
|
||||
&self,
|
||||
auth_type: &types::ConnectorAuthType,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let auth = opennode::OpennodeAuthType::try_from(auth_type)
|
||||
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)])
|
||||
}
|
||||
|
||||
fn build_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
let response: opennode::OpennodeErrorResponse = res
|
||||
.response
|
||||
.parse_struct("OpennodeErrorResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
Ok(ErrorResponse {
|
||||
status_code: res.status_code,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
reason: response.reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Opennode
|
||||
{
|
||||
//TODO: implement sessions flow
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
|
||||
for Opennode
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||
for Opennode
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
for Opennode
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsAuthorizeRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!("{}/v1/charges", self.base_url(_connectors)))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req_obj = opennode::OpennodePaymentsRequest::try_from(req)?;
|
||||
let opennode_req =
|
||||
Encode::<opennode::OpennodePaymentsRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(opennode_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.headers(types::PaymentsAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsAuthorizeRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: opennode::OpennodePaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Opennode PaymentsAuthorizeResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
for Opennode
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsSyncRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let connector_id = _req
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
Ok(format!(
|
||||
"{}/v2/charge/{}",
|
||||
self.base_url(_connectors),
|
||||
connector_id
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Get)
|
||||
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
|
||||
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
let response: opennode::OpennodePaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("opennode PaymentsSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
||||
for Opennode
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCaptureRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsCaptureRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
_req: &types::PaymentsCaptureRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into())
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsCaptureRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
|
||||
.headers(types::PaymentsCaptureType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsCaptureRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
|
||||
let response: opennode::OpennodePaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Opennode PaymentsCaptureResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
|
||||
for Opennode
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
|
||||
for Opennode
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::RefundsRouterData<api::Execute>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req_obj = opennode::OpennodeRefundRequest::try_from(req)?;
|
||||
let opennode_req =
|
||||
Encode::<opennode::OpennodeRefundRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(opennode_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
let request = services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
|
||||
.headers(types::RefundExecuteType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::RefundExecuteType::get_request_body(self, req)?)
|
||||
.build();
|
||||
Ok(Some(request))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefundsRouterData<api::Execute>,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
|
||||
let response: opennode::RefundResponse = res
|
||||
.response
|
||||
.parse_struct("opennode RefundResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Opennode {
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::RefundSyncRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Get)
|
||||
.url(&types::RefundSyncType::get_url(self, req, connectors)?)
|
||||
.headers(types::RefundSyncType::get_headers(self, req, connectors)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefundSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
|
||||
let response: opennode::RefundResponse = res
|
||||
.response
|
||||
.parse_struct("opennode RefundSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl api::IncomingWebhook for Opennode {
|
||||
fn get_webhook_source_verification_algorithm(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
|
||||
Ok(Box::new(crypto::HmacSha256))
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_signature(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let notif = serde_urlencoded::from_bytes::<OpennodeWebhookDetails>(request.body)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
let base64_signature = notif.hashed_order;
|
||||
hex::decode(base64_signature)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_message(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
_merchant_id: &str,
|
||||
_secret: &[u8],
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let message = std::str::from_utf8(request.body)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||
Ok(message.to_string().into_bytes())
|
||||
}
|
||||
|
||||
async fn get_webhook_source_verification_merchant_secret(
|
||||
&self,
|
||||
db: &dyn db::StorageInterface,
|
||||
merchant_id: &str,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let key = format!("whsec_verification_{}_{}", self.id(), merchant_id);
|
||||
let secret = db
|
||||
.get_key(&key)
|
||||
.await
|
||||
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?;
|
||||
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
fn get_webhook_object_reference_id(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
||||
let notif = serde_urlencoded::from_bytes::<OpennodeWebhookDetails>(request.body)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(notif.id),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
let notif = serde_urlencoded::from_bytes::<OpennodeWebhookDetails>(request.body)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
|
||||
match notif.status {
|
||||
opennode::OpennodePaymentStatus::Paid => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentIntentSuccess)
|
||||
}
|
||||
opennode::OpennodePaymentStatus::Underpaid
|
||||
| opennode::OpennodePaymentStatus::Expired => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentActionRequired)
|
||||
}
|
||||
opennode::OpennodePaymentStatus::Processing => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentIntentProcessing)
|
||||
}
|
||||
opennode::OpennodePaymentStatus::Refunded => {
|
||||
Ok(api::IncomingWebhookEvent::RefundSuccess)
|
||||
}
|
||||
_ => Ok(api::IncomingWebhookEvent::EventNotSupported),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_webhook_resource_object(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||
let notif = serde_urlencoded::from_bytes::<OpennodeWebhookDetails>(request.body)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
Encode::<OpennodeWebhookDetails>::encode_to_value(¬if.status)
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)
|
||||
}
|
||||
}
|
||||
256
crates/router/src/connector/opennode/transformers.rs
Normal file
256
crates/router/src/connector/opennode/transformers.rs
Normal file
@ -0,0 +1,256 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
connector::utils::{PaymentsAuthorizeRequestData, RouterData},
|
||||
core::errors,
|
||||
services,
|
||||
types::{self, api, storage::enums},
|
||||
};
|
||||
|
||||
//TODO: Fill the struct with respective fields
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
pub struct OpennodePaymentsRequest {
|
||||
amount: i64,
|
||||
currency: String,
|
||||
description: String,
|
||||
auto_settle: bool,
|
||||
success_url: String,
|
||||
callback_url: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for OpennodePaymentsRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
get_crypto_specific_payment_data(item)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Fill the struct with respective fields
|
||||
// Auth Struct
|
||||
pub struct OpennodeAuthType {
|
||||
pub(super) api_key: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::ConnectorAuthType> for OpennodeAuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
match auth_type {
|
||||
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self {
|
||||
api_key: api_key.to_string(),
|
||||
}),
|
||||
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
// PaymentsResponse
|
||||
//TODO: Append the remaining status flags
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OpennodePaymentStatus {
|
||||
Unpaid,
|
||||
Paid,
|
||||
Expired,
|
||||
#[default]
|
||||
Processing,
|
||||
Underpaid,
|
||||
Refunded,
|
||||
}
|
||||
|
||||
impl From<OpennodePaymentStatus> for enums::AttemptStatus {
|
||||
fn from(item: OpennodePaymentStatus) -> Self {
|
||||
match item {
|
||||
OpennodePaymentStatus::Unpaid => Self::AuthenticationPending,
|
||||
OpennodePaymentStatus::Paid => Self::Charged,
|
||||
OpennodePaymentStatus::Expired => Self::Failure,
|
||||
OpennodePaymentStatus::Underpaid => Self::Unresolved,
|
||||
_ => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct OpennodePaymentsResponseData {
|
||||
id: String,
|
||||
hosted_checkout_url: String,
|
||||
status: OpennodePaymentStatus,
|
||||
}
|
||||
|
||||
//TODO: Fill the struct with respective fields
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct OpennodePaymentsResponse {
|
||||
data: OpennodePaymentsResponseData,
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, OpennodePaymentsResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
OpennodePaymentsResponse,
|
||||
T,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let form_fields = HashMap::new();
|
||||
let redirection_data = services::RedirectForm {
|
||||
endpoint: item.response.data.hosted_checkout_url.to_string(),
|
||||
method: services::Method::Get,
|
||||
form_fields,
|
||||
};
|
||||
let connector_id = types::ResponseId::ConnectorTransactionId(item.response.data.id);
|
||||
let attempt_status = item.response.data.status;
|
||||
let response_data = if attempt_status != OpennodePaymentStatus::Underpaid {
|
||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: connector_id,
|
||||
redirection_data: Some(redirection_data),
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
})
|
||||
} else {
|
||||
Ok(types::PaymentsResponseData::TransactionUnresolvedResponse {
|
||||
resource_id: connector_id,
|
||||
reason: Some(api::enums::UnresolvedResponseReason {
|
||||
code: "UNDERPAID".to_string(),
|
||||
message:
|
||||
"Please check the transaction in opennode dashboard and resolve manually"
|
||||
.to_string(),
|
||||
}),
|
||||
})
|
||||
};
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::from(attempt_status),
|
||||
response: response_data,
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Fill the struct with respective fields
|
||||
// REFUND :
|
||||
// Type definition for RefundRequest
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
pub struct OpennodeRefundRequest {
|
||||
pub amount: i64,
|
||||
}
|
||||
|
||||
impl<F> TryFrom<&types::RefundsRouterData<F>> for OpennodeRefundRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
amount: item.request.amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Type definition for Refund Response
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
|
||||
pub enum RefundStatus {
|
||||
Refunded,
|
||||
#[default]
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl From<RefundStatus> for enums::RefundStatus {
|
||||
fn from(item: RefundStatus) -> Self {
|
||||
match item {
|
||||
RefundStatus::Refunded => Self::Success,
|
||||
RefundStatus::Processing => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Fill the struct with respective fields
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RefundResponse {
|
||||
id: String,
|
||||
status: RefundStatus,
|
||||
}
|
||||
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
for types::RefundsRouterData<api::Execute>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
connector_refund_id: item.response.id.to_string(),
|
||||
refund_status: enums::RefundStatus::from(item.response.status),
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
|
||||
for types::RefundsRouterData<api::RSync>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
connector_refund_id: item.response.id.to_string(),
|
||||
refund_status: enums::RefundStatus::from(item.response.status),
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Fill the struct with respective fields
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct OpennodeErrorResponse {
|
||||
pub status_code: u16,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
fn get_crypto_specific_payment_data(
|
||||
item: &types::PaymentsAuthorizeRouterData,
|
||||
) -> Result<OpennodePaymentsRequest, error_stack::Report<errors::ConnectorError>> {
|
||||
let amount = item.request.amount;
|
||||
let currency = item.request.currency.to_string();
|
||||
let description = item.get_description()?;
|
||||
let auto_settle = true;
|
||||
let success_url = item.get_return_url()?;
|
||||
let callback_url = item.request.get_webhook_url()?;
|
||||
|
||||
Ok(OpennodePaymentsRequest {
|
||||
amount,
|
||||
currency,
|
||||
description,
|
||||
auto_settle,
|
||||
success_url,
|
||||
callback_url,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OpennodeWebhookDetails {
|
||||
pub id: String,
|
||||
pub callback_url: String,
|
||||
pub success_url: String,
|
||||
pub status: OpennodePaymentStatus,
|
||||
pub payment_method: String,
|
||||
pub missing_amt: String,
|
||||
pub order_id: String,
|
||||
pub description: String,
|
||||
pub price: String,
|
||||
pub fee: String,
|
||||
pub auto_settle: String,
|
||||
pub fiat_value: String,
|
||||
pub net_fiat_value: String,
|
||||
pub overpaid_by: String,
|
||||
pub hashed_order: String,
|
||||
}
|
||||
@ -1297,6 +1297,11 @@ impl
|
||||
}))
|
||||
}
|
||||
api::PaymentMethodData::Wallet(_) => Ok(Self::Wallet),
|
||||
api::PaymentMethodData::Crypto(_) => Err(errors::ConnectorError::NotSupported {
|
||||
payment_method: format!("{pm_type:?}"),
|
||||
connector: "Stripe",
|
||||
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl.to_string(),
|
||||
})?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ pub trait RouterData {
|
||||
fn get_billing_country(&self) -> Result<api_models::enums::CountryCode, Error>;
|
||||
fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>;
|
||||
fn get_description(&self) -> Result<String, Error>;
|
||||
fn get_return_url(&self) -> Result<String, Error>;
|
||||
fn get_billing_address(&self) -> Result<&api::AddressDetails, Error>;
|
||||
fn get_shipping_address(&self) -> Result<&api::AddressDetails, Error>;
|
||||
fn get_connector_meta(&self) -> Result<pii::SecretSerdeValue, Error>;
|
||||
@ -89,6 +90,11 @@ impl<Flow, Request, Response> RouterData for types::RouterData<Flow, Request, Re
|
||||
.clone()
|
||||
.ok_or_else(missing_field_err("description"))
|
||||
}
|
||||
fn get_return_url(&self) -> Result<String, Error> {
|
||||
self.return_url
|
||||
.clone()
|
||||
.ok_or_else(missing_field_err("return_url"))
|
||||
}
|
||||
fn get_billing_address(&self) -> Result<&api::AddressDetails, Error> {
|
||||
self.address
|
||||
.billing
|
||||
@ -139,6 +145,7 @@ pub trait PaymentsAuthorizeRequestData {
|
||||
fn get_browser_info(&self) -> Result<types::BrowserInformation, Error>;
|
||||
fn get_card(&self) -> Result<api::Card, Error>;
|
||||
fn get_return_url(&self) -> Result<String, Error>;
|
||||
fn get_webhook_url(&self) -> Result<String, Error>;
|
||||
}
|
||||
|
||||
impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
|
||||
@ -164,6 +171,11 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
|
||||
.clone()
|
||||
.ok_or_else(missing_field_err("return_url"))
|
||||
}
|
||||
fn get_webhook_url(&self) -> Result<String, Error> {
|
||||
self.router_return_url
|
||||
.clone()
|
||||
.ok_or_else(missing_field_err("webhook_url"))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PaymentsSyncRequestData {
|
||||
|
||||
@ -616,6 +616,7 @@ pub fn should_call_connector<Op: Debug, F: Clone>(
|
||||
| storage_enums::IntentStatus::Processing
|
||||
| storage_enums::IntentStatus::Succeeded
|
||||
| storage_enums::IntentStatus::RequiresCustomerAction
|
||||
| storage_enums::IntentStatus::RequiresMerchantAction
|
||||
) && payment_data.force_sync.unwrap_or(false)
|
||||
}
|
||||
"PaymentCancel" => matches!(
|
||||
|
||||
@ -81,11 +81,13 @@ default_imp_for_complete_authorize!(
|
||||
connector::Bluesnap,
|
||||
connector::Braintree,
|
||||
connector::Checkout,
|
||||
connector::Coinbase,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
connector::Klarna,
|
||||
connector::Multisafepay,
|
||||
connector::Opennode,
|
||||
connector::Payu,
|
||||
connector::Rapyd,
|
||||
connector::Shift4,
|
||||
@ -121,12 +123,14 @@ default_imp_for_connector_redirect_response!(
|
||||
connector::Bambora,
|
||||
connector::Bluesnap,
|
||||
connector::Braintree,
|
||||
connector::Coinbase,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
connector::Globalpay,
|
||||
connector::Klarna,
|
||||
connector::Multisafepay,
|
||||
connector::Opennode,
|
||||
connector::Payu,
|
||||
connector::Rapyd,
|
||||
connector::Shift4,
|
||||
@ -152,6 +156,7 @@ default_imp_for_connector_request_id!(
|
||||
connector::Bluesnap,
|
||||
connector::Braintree,
|
||||
connector::Checkout,
|
||||
connector::Coinbase,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
@ -160,6 +165,7 @@ default_imp_for_connector_request_id!(
|
||||
connector::Mollie,
|
||||
connector::Multisafepay,
|
||||
connector::Nuvei,
|
||||
connector::Opennode,
|
||||
connector::Payu,
|
||||
connector::Rapyd,
|
||||
connector::Shift4,
|
||||
|
||||
@ -379,6 +379,7 @@ pub fn create_complete_authorize_url(
|
||||
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_recurring_mandate(req: api::MandateValidationFields) -> RouterResult<()> {
|
||||
req.mandate_id.check_value_present("mandate_id")?;
|
||||
|
||||
@ -805,6 +806,7 @@ pub async fn make_pm_data<'a, F: Clone, R>(
|
||||
}
|
||||
(pm @ Some(api::PaymentMethodData::PayLater(_)), _) => Ok(pm.to_owned()),
|
||||
(pm @ Some(api::PaymentMethodData::BankRedirect(_)), _) => Ok(pm.to_owned()),
|
||||
(pm @ Some(api::PaymentMethodData::Crypto(_)), _) => Ok(pm.to_owned()),
|
||||
(pm_opt @ Some(pm @ api::PaymentMethodData::Wallet(_)), _) => {
|
||||
let token = vault::Vault::store_payment_method_data_in_locker(
|
||||
state,
|
||||
|
||||
@ -295,8 +295,8 @@ async fn payment_response_update_tracker<F: Clone, T>(
|
||||
Some(storage::PaymentAttemptUpdate::ErrorUpdate {
|
||||
connector: None,
|
||||
status: storage::enums::AttemptStatus::Failure,
|
||||
error_message: Some(err.message),
|
||||
error_code: Some(err.code),
|
||||
error_message: Some(Some(err.message)),
|
||||
error_code: Some(Some(err.code)),
|
||||
}),
|
||||
Some(storage::ConnectorResponseUpdate::ErrorUpdate {
|
||||
connector_name: Some(router_data.connector.clone()),
|
||||
@ -324,6 +324,13 @@ async fn payment_response_update_tracker<F: Clone, T>(
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not parse the connector response")?;
|
||||
|
||||
// incase of success, update error code and error message
|
||||
let error_status = if router_data.status == enums::AttemptStatus::Charged {
|
||||
Some(None)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if router_data.status == enums::AttemptStatus::Charged {
|
||||
metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]);
|
||||
}
|
||||
@ -339,6 +346,8 @@ async fn payment_response_update_tracker<F: Clone, T>(
|
||||
.clone()
|
||||
.map(|mandate| mandate.mandate_id),
|
||||
connector_metadata,
|
||||
error_code: error_status.clone(),
|
||||
error_message: error_status,
|
||||
};
|
||||
|
||||
let connector_response_update = storage::ConnectorResponseUpdate::ResponseUpdate {
|
||||
@ -353,7 +362,27 @@ async fn payment_response_update_tracker<F: Clone, T>(
|
||||
Some(connector_response_update),
|
||||
)
|
||||
}
|
||||
|
||||
types::PaymentsResponseData::TransactionUnresolvedResponse {
|
||||
resource_id,
|
||||
reason,
|
||||
} => {
|
||||
let connector_transaction_id = match resource_id {
|
||||
types::ResponseId::NoResponseId => None,
|
||||
types::ResponseId::ConnectorTransactionId(id)
|
||||
| types::ResponseId::EncodedData(id) => Some(id),
|
||||
};
|
||||
(
|
||||
Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate {
|
||||
status: router_data.status,
|
||||
connector: None,
|
||||
connector_transaction_id,
|
||||
payment_method_id: Some(router_data.payment_method_id),
|
||||
error_code: Some(reason.clone().map(|cd| cd.code)),
|
||||
error_message: Some(reason.map(|cd| cd.message)),
|
||||
}),
|
||||
None,
|
||||
)
|
||||
}
|
||||
types::PaymentsResponseData::SessionResponse { .. } => (None, None),
|
||||
types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None),
|
||||
},
|
||||
|
||||
@ -522,10 +522,18 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not find event type in incoming webhook body")?;
|
||||
|
||||
if !matches!(
|
||||
event_type,
|
||||
api_models::webhooks::IncomingWebhookEvent::EndpointVerification
|
||||
) {
|
||||
let process_webhook_further = utils::lookup_webhook_event(
|
||||
&*state.store,
|
||||
connector_name,
|
||||
&merchant_account.merchant_id,
|
||||
&event_type,
|
||||
)
|
||||
.await;
|
||||
|
||||
logger::info!(process_webhook=?process_webhook_further);
|
||||
logger::info!(event_type=?event_type);
|
||||
|
||||
if process_webhook_further {
|
||||
let source_verified = connector
|
||||
.verify_webhook_source(
|
||||
&*state.store,
|
||||
@ -536,15 +544,6 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("There was an issue in incoming webhook source verification")?;
|
||||
|
||||
let process_webhook_further = utils::lookup_webhook_event(
|
||||
&*state.store,
|
||||
connector_name,
|
||||
&merchant_account.merchant_id,
|
||||
&event_type,
|
||||
)
|
||||
.await;
|
||||
|
||||
if process_webhook_further {
|
||||
let object_ref_id = connector
|
||||
.get_webhook_object_reference_id(&request_details)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
@ -608,7 +607,6 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
|
||||
.attach_printable("Unsupported Flow Type received in incoming webhooks")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = connector
|
||||
.get_webhook_api_response(&request_details)
|
||||
|
||||
@ -4,7 +4,13 @@ use crate::{
|
||||
};
|
||||
|
||||
fn default_webhook_config() -> api::MerchantWebhookConfig {
|
||||
std::collections::HashSet::from([api::IncomingWebhookEvent::PaymentIntentSuccess])
|
||||
std::collections::HashSet::from([
|
||||
api::IncomingWebhookEvent::PaymentIntentSuccess,
|
||||
api::IncomingWebhookEvent::PaymentIntentFailure,
|
||||
api::IncomingWebhookEvent::PaymentIntentProcessing,
|
||||
api::IncomingWebhookEvent::PaymentActionRequired,
|
||||
api::IncomingWebhookEvent::RefundSuccess,
|
||||
])
|
||||
}
|
||||
|
||||
pub async fn lookup_webhook_event(
|
||||
|
||||
@ -45,6 +45,7 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
pub mod headers {
|
||||
pub const ACCEPT: &str = "Accept";
|
||||
pub const API_KEY: &str = "API-KEY";
|
||||
pub const X_CC_API_KEY: &str = "X-CC-Api-Key";
|
||||
pub const AUTHORIZATION: &str = "Authorization";
|
||||
pub const CONTENT_TYPE: &str = "Content-Type";
|
||||
pub const DATE: &str = "Date";
|
||||
@ -55,6 +56,7 @@ pub mod headers {
|
||||
pub const X_LOGIN: &str = "X-Login";
|
||||
pub const X_TRANS_KEY: &str = "X-Trans-Key";
|
||||
pub const X_VERSION: &str = "X-Version";
|
||||
pub const X_CC_VERSION: &str = "X-CC-Version";
|
||||
pub const X_DATE: &str = "X-Date";
|
||||
}
|
||||
|
||||
|
||||
@ -254,6 +254,11 @@ pub enum PaymentsResponseData {
|
||||
SessionTokenResponse {
|
||||
session_token: String,
|
||||
},
|
||||
TransactionUnresolvedResponse {
|
||||
resource_id: ResponseId,
|
||||
//to add more info on cypto response, like `unresolved` reason(overpaid, underpaid, delayed)
|
||||
reason: Option<api::enums::UnresolvedResponseReason>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
||||
@ -192,6 +192,7 @@ impl ConnectorData {
|
||||
"bluesnap" => Ok(Box::new(&connector::Bluesnap)),
|
||||
"braintree" => Ok(Box::new(&connector::Braintree)),
|
||||
"checkout" => Ok(Box::new(&connector::Checkout)),
|
||||
"coinbase" => Ok(Box::new(&connector::Coinbase)),
|
||||
"cybersource" => Ok(Box::new(&connector::Cybersource)),
|
||||
"dlocal" => Ok(Box::new(&connector::Dlocal)),
|
||||
"fiserv" => Ok(Box::new(&connector::Fiserv)),
|
||||
@ -199,6 +200,7 @@ impl ConnectorData {
|
||||
"klarna" => Ok(Box::new(&connector::Klarna)),
|
||||
"mollie" => Ok(Box::new(&connector::Mollie)),
|
||||
"nuvei" => Ok(Box::new(&connector::Nuvei)),
|
||||
"opennode" => Ok(Box::new(&connector::Opennode)),
|
||||
"payu" => Ok(Box::new(&connector::Payu)),
|
||||
"rapyd" => Ok(Box::new(&connector::Rapyd)),
|
||||
"shift4" => Ok(Box::new(&connector::Shift4)),
|
||||
|
||||
@ -129,6 +129,7 @@ impl ForeignFrom<storage_enums::AttemptStatus> for storage_enums::IntentStatus {
|
||||
|
||||
storage_enums::AttemptStatus::Authorized => Self::RequiresCapture,
|
||||
storage_enums::AttemptStatus::AuthenticationPending => Self::RequiresCustomerAction,
|
||||
storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
||||
|
||||
storage_enums::AttemptStatus::PartialCharged
|
||||
| storage_enums::AttemptStatus::Started
|
||||
@ -156,6 +157,8 @@ impl ForeignTryFrom<api_enums::IntentStatus> for storage_enums::EventType {
|
||||
fn foreign_try_from(value: api_enums::IntentStatus) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
api_enums::IntentStatus::Succeeded => Ok(Self::PaymentSucceeded),
|
||||
api_enums::IntentStatus::Processing => Ok(Self::PaymentProcessing),
|
||||
api_enums::IntentStatus::RequiresMerchantAction => Ok(Self::ActionRequired),
|
||||
_ => Err(errors::ValidationError::IncorrectValueProvided {
|
||||
field_name: "intent_status",
|
||||
}),
|
||||
|
||||
465
crates/router/tests/connectors/coinbase.rs
Normal file
465
crates/router/tests/connectors/coinbase.rs
Normal file
@ -0,0 +1,465 @@
|
||||
use masking::Secret;
|
||||
use router::types::{self, api, storage::enums};
|
||||
|
||||
use crate::{
|
||||
connector_auth,
|
||||
utils::{self, ConnectorActions},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct CoinbaseTest;
|
||||
impl ConnectorActions for CoinbaseTest {}
|
||||
impl utils::Connector for CoinbaseTest {
|
||||
fn get_data(&self) -> types::api::ConnectorData {
|
||||
use router::connector::Coinbase;
|
||||
types::api::ConnectorData {
|
||||
connector: Box::new(&Coinbase),
|
||||
connector_name: types::Connector::Coinbase,
|
||||
get_token: types::api::GetToken::Connector,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auth_token(&self) -> types::ConnectorAuthType {
|
||||
types::ConnectorAuthType::from(
|
||||
connector_auth::ConnectorAuthentication::new()
|
||||
.coinbase
|
||||
.expect("Missing connector authentication configuration"),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
"coinbase".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
static CONNECTOR: CoinbaseTest = CoinbaseTest {};
|
||||
|
||||
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
|
||||
None
|
||||
}
|
||||
|
||||
fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
|
||||
None
|
||||
}
|
||||
|
||||
// Cards Positive Tests
|
||||
// Creates a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_only_authorize_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized);
|
||||
}
|
||||
|
||||
// Captures a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_capture_authorized_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info())
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Partially captures a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_capture_authorized_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_and_capture_payment(
|
||||
payment_method_details(),
|
||||
Some(types::PaymentsCaptureData {
|
||||
amount_to_capture: 50,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Synchronizes a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_authorized_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
|
||||
let response = CONNECTOR
|
||||
.psync_retry_till_status_matches(
|
||||
enums::AttemptStatus::Authorized,
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
txn_id.unwrap(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("PSync response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
|
||||
}
|
||||
|
||||
// Voids a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_void_authorized_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_and_void_payment(
|
||||
payment_method_details(),
|
||||
Some(types::PaymentsCancelData {
|
||||
connector_transaction_id: String::from(""),
|
||||
cancellation_reason: Some("requested_by_customer".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Void payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Voided);
|
||||
}
|
||||
|
||||
// Refunds a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_manually_captured_payment() {
|
||||
let response = CONNECTOR
|
||||
.capture_payment_and_refund(
|
||||
payment_method_details(),
|
||||
None,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Partially refunds a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_refund_manually_captured_payment() {
|
||||
let response = CONNECTOR
|
||||
.capture_payment_and_refund(
|
||||
payment_method_details(),
|
||||
None,
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Synchronizes a refund using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_manually_captured_refund() {
|
||||
let refund_response = CONNECTOR
|
||||
.capture_payment_and_refund(
|
||||
payment_method_details(),
|
||||
None,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let response = CONNECTOR
|
||||
.rsync_retry_till_status_matches(
|
||||
enums::RefundStatus::Success,
|
||||
refund_response.response.unwrap().connector_refund_id,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_make_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Synchronizes a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_auto_captured_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
|
||||
assert_ne!(txn_id, None, "Empty connector transaction id");
|
||||
let response = CONNECTOR
|
||||
.psync_retry_till_status_matches(
|
||||
enums::AttemptStatus::Charged,
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
txn_id.unwrap(),
|
||||
),
|
||||
capture_method: Some(enums::CaptureMethod::Automatic),
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged,);
|
||||
}
|
||||
|
||||
// Refunds a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_auto_captured_payment() {
|
||||
let response = CONNECTOR
|
||||
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Partially refunds a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_refund_succeeded_payment() {
|
||||
let refund_response = CONNECTOR
|
||||
.make_payment_and_refund(
|
||||
payment_method_details(),
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
refund_response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_succeeded_payment_multiple_times() {
|
||||
CONNECTOR
|
||||
.make_payment_and_multiple_refund(
|
||||
payment_method_details(),
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Synchronizes a refund using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_refund() {
|
||||
let refund_response = CONNECTOR
|
||||
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
let response = CONNECTOR
|
||||
.rsync_retry_till_status_matches(
|
||||
enums::RefundStatus::Success,
|
||||
refund_response.response.unwrap().connector_refund_id,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Cards Negative scenerios
|
||||
// Creates a payment with incorrect card number.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_card_number() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_number: Secret::new("1234567891011".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Your card number is incorrect.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with empty card number.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_empty_card_number() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_number: Secret::new(String::from("")),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let x = response.response.unwrap_err();
|
||||
assert_eq!(
|
||||
x.message,
|
||||
"You passed an empty string for 'payment_method_data[card][number]'.",
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect CVC.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_cvc() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_cvc: Secret::new("12345".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Your card's security code is invalid.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect expiry month.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_invalid_exp_month() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_exp_month: Secret::new("20".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Your card's expiration month is invalid.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect expiry year.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_expiry_year() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_exp_year: Secret::new("2000".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Your card's expiration year is invalid.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Voids a payment using automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_fail_void_payment_for_auto_capture() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
|
||||
assert_ne!(txn_id, None, "Empty connector transaction id");
|
||||
let void_response = CONNECTOR
|
||||
.void_payment(txn_id.unwrap(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
void_response.response.unwrap_err().message,
|
||||
"You cannot cancel this PaymentIntent because it has a status of succeeded."
|
||||
);
|
||||
}
|
||||
|
||||
// Captures a payment using invalid connector payment id.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_capture_for_invalid_payment() {
|
||||
let capture_response = CONNECTOR
|
||||
.capture_payment("123456789".to_string(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
capture_response.response.unwrap_err().message,
|
||||
String::from("No such payment_intent: '123456789'")
|
||||
);
|
||||
}
|
||||
|
||||
// Refunds a payment with refund amount higher than payment amount.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
|
||||
let response = CONNECTOR
|
||||
.make_payment_and_refund(
|
||||
payment_method_details(),
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 150,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Refund amount (₹1.50) is greater than charge amount (₹1.00)",
|
||||
);
|
||||
}
|
||||
|
||||
// Connector dependent test cases goes here
|
||||
|
||||
// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests
|
||||
@ -12,6 +12,7 @@ pub(crate) struct ConnectorAuthentication {
|
||||
pub bambora: Option<BodyKey>,
|
||||
pub bluesnap: Option<BodyKey>,
|
||||
pub checkout: Option<BodyKey>,
|
||||
pub coinbase: Option<HeaderKey>,
|
||||
pub cybersource: Option<SignatureKey>,
|
||||
pub dlocal: Option<SignatureKey>,
|
||||
pub fiserv: Option<SignatureKey>,
|
||||
@ -19,6 +20,7 @@ pub(crate) struct ConnectorAuthentication {
|
||||
pub mollie: Option<HeaderKey>,
|
||||
pub multisafepay: Option<HeaderKey>,
|
||||
pub nuvei: Option<SignatureKey>,
|
||||
pub opennode: Option<HeaderKey>,
|
||||
pub paypal: Option<BodyKey>,
|
||||
pub payu: Option<BodyKey>,
|
||||
pub rapyd: Option<BodyKey>,
|
||||
|
||||
@ -7,6 +7,7 @@ mod authorizedotnet;
|
||||
mod bambora;
|
||||
mod bluesnap;
|
||||
mod checkout;
|
||||
mod coinbase;
|
||||
mod connector_auth;
|
||||
mod cybersource;
|
||||
mod dlocal;
|
||||
@ -15,6 +16,7 @@ mod globalpay;
|
||||
mod mollie;
|
||||
mod multisafepay;
|
||||
mod nuvei;
|
||||
mod opennode;
|
||||
mod paypal;
|
||||
mod payu;
|
||||
mod rapyd;
|
||||
|
||||
465
crates/router/tests/connectors/opennode.rs
Normal file
465
crates/router/tests/connectors/opennode.rs
Normal file
@ -0,0 +1,465 @@
|
||||
use masking::Secret;
|
||||
use router::types::{self, api, storage::enums};
|
||||
|
||||
use crate::{
|
||||
connector_auth,
|
||||
utils::{self, ConnectorActions},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct OpennodeTest;
|
||||
impl ConnectorActions for OpennodeTest {}
|
||||
impl utils::Connector for OpennodeTest {
|
||||
fn get_data(&self) -> types::api::ConnectorData {
|
||||
use router::connector::Opennode;
|
||||
types::api::ConnectorData {
|
||||
connector: Box::new(&Opennode),
|
||||
connector_name: types::Connector::Opennode,
|
||||
get_token: types::api::GetToken::Connector,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auth_token(&self) -> types::ConnectorAuthType {
|
||||
types::ConnectorAuthType::from(
|
||||
connector_auth::ConnectorAuthentication::new()
|
||||
.opennode
|
||||
.expect("Missing connector authentication configuration"),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
"opennode".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
static CONNECTOR: OpennodeTest = OpennodeTest {};
|
||||
|
||||
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
|
||||
None
|
||||
}
|
||||
|
||||
fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
|
||||
None
|
||||
}
|
||||
|
||||
// Cards Positive Tests
|
||||
// Creates a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_only_authorize_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized);
|
||||
}
|
||||
|
||||
// Captures a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_capture_authorized_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info())
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Partially captures a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_capture_authorized_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_and_capture_payment(
|
||||
payment_method_details(),
|
||||
Some(types::PaymentsCaptureData {
|
||||
amount_to_capture: 50,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Synchronizes a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_authorized_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
|
||||
let response = CONNECTOR
|
||||
.psync_retry_till_status_matches(
|
||||
enums::AttemptStatus::Authorized,
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
txn_id.unwrap(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("PSync response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
|
||||
}
|
||||
|
||||
// Voids a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_void_authorized_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_and_void_payment(
|
||||
payment_method_details(),
|
||||
Some(types::PaymentsCancelData {
|
||||
connector_transaction_id: String::from(""),
|
||||
cancellation_reason: Some("requested_by_customer".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Void payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Voided);
|
||||
}
|
||||
|
||||
// Refunds a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_manually_captured_payment() {
|
||||
let response = CONNECTOR
|
||||
.capture_payment_and_refund(
|
||||
payment_method_details(),
|
||||
None,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Partially refunds a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_refund_manually_captured_payment() {
|
||||
let response = CONNECTOR
|
||||
.capture_payment_and_refund(
|
||||
payment_method_details(),
|
||||
None,
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Synchronizes a refund using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_manually_captured_refund() {
|
||||
let refund_response = CONNECTOR
|
||||
.capture_payment_and_refund(
|
||||
payment_method_details(),
|
||||
None,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let response = CONNECTOR
|
||||
.rsync_retry_till_status_matches(
|
||||
enums::RefundStatus::Success,
|
||||
refund_response.response.unwrap().connector_refund_id,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_make_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Synchronizes a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_auto_captured_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
|
||||
assert_ne!(txn_id, None, "Empty connector transaction id");
|
||||
let response = CONNECTOR
|
||||
.psync_retry_till_status_matches(
|
||||
enums::AttemptStatus::Charged,
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
txn_id.unwrap(),
|
||||
),
|
||||
capture_method: Some(enums::CaptureMethod::Automatic),
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged,);
|
||||
}
|
||||
|
||||
// Refunds a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_auto_captured_payment() {
|
||||
let response = CONNECTOR
|
||||
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Partially refunds a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_refund_succeeded_payment() {
|
||||
let refund_response = CONNECTOR
|
||||
.make_payment_and_refund(
|
||||
payment_method_details(),
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
refund_response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_succeeded_payment_multiple_times() {
|
||||
CONNECTOR
|
||||
.make_payment_and_multiple_refund(
|
||||
payment_method_details(),
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Synchronizes a refund using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_refund() {
|
||||
let refund_response = CONNECTOR
|
||||
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
let response = CONNECTOR
|
||||
.rsync_retry_till_status_matches(
|
||||
enums::RefundStatus::Success,
|
||||
refund_response.response.unwrap().connector_refund_id,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Cards Negative scenerios
|
||||
// Creates a payment with incorrect card number.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_card_number() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_number: Secret::new("1234567891011".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Your card number is incorrect.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with empty card number.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_empty_card_number() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_number: Secret::new(String::from("")),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let x = response.response.unwrap_err();
|
||||
assert_eq!(
|
||||
x.message,
|
||||
"You passed an empty string for 'payment_method_data[card][number]'.",
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect CVC.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_cvc() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_cvc: Secret::new("12345".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Your card's security code is invalid.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect expiry month.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_invalid_exp_month() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_exp_month: Secret::new("20".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Your card's expiration month is invalid.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect expiry year.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_expiry_year() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_exp_year: Secret::new("2000".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Your card's expiration year is invalid.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Voids a payment using automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_fail_void_payment_for_auto_capture() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
|
||||
assert_ne!(txn_id, None, "Empty connector transaction id");
|
||||
let void_response = CONNECTOR
|
||||
.void_payment(txn_id.unwrap(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
void_response.response.unwrap_err().message,
|
||||
"You cannot cancel this PaymentIntent because it has a status of succeeded."
|
||||
);
|
||||
}
|
||||
|
||||
// Captures a payment using invalid connector payment id.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_capture_for_invalid_payment() {
|
||||
let capture_response = CONNECTOR
|
||||
.capture_payment("123456789".to_string(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
capture_response.response.unwrap_err().message,
|
||||
String::from("No such payment_intent: '123456789'")
|
||||
);
|
||||
}
|
||||
|
||||
// Refunds a payment with refund amount higher than payment amount.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
|
||||
let response = CONNECTOR
|
||||
.make_payment_and_refund(
|
||||
payment_method_details(),
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 150,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"Refund amount (₹1.50) is greater than charge amount (₹1.00)",
|
||||
);
|
||||
}
|
||||
|
||||
// Connector dependent test cases goes here
|
||||
|
||||
// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests
|
||||
@ -73,3 +73,9 @@ key1 = "key1"
|
||||
|
||||
[mollie]
|
||||
api_key = "API Key"
|
||||
|
||||
[coinbase]
|
||||
api_key="API Key"
|
||||
|
||||
[opennode]
|
||||
api_key="API Key"
|
||||
|
||||
@ -397,6 +397,7 @@ pub trait ConnectorActions: Connector {
|
||||
}
|
||||
Ok(types::PaymentsResponseData::SessionResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { .. }) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
@ -576,6 +577,7 @@ pub fn get_connector_transaction_id(
|
||||
}
|
||||
Ok(types::PaymentsResponseData::SessionResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None,
|
||||
Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { .. }) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +51,7 @@ pub enum AttemptStatus {
|
||||
VoidFailed,
|
||||
AutoRefunded,
|
||||
PartialCharged,
|
||||
Unresolved,
|
||||
#[default]
|
||||
Pending,
|
||||
Failure,
|
||||
@ -317,6 +318,8 @@ pub enum EventObjectType {
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum EventType {
|
||||
PaymentSucceeded,
|
||||
PaymentProcessing,
|
||||
ActionRequired,
|
||||
RefundSucceeded,
|
||||
RefundFailed,
|
||||
DisputeOpened,
|
||||
@ -350,6 +353,7 @@ pub enum IntentStatus {
|
||||
Cancelled,
|
||||
Processing,
|
||||
RequiresCustomerAction,
|
||||
RequiresMerchantAction,
|
||||
RequiresPaymentMethod,
|
||||
#[default]
|
||||
RequiresConfirmation,
|
||||
@ -450,6 +454,7 @@ pub enum PaymentMethod {
|
||||
PayLater,
|
||||
Wallet,
|
||||
BankRedirect,
|
||||
Crypto,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@ -618,6 +623,7 @@ pub enum PaymentMethodType {
|
||||
GooglePay,
|
||||
ApplePay,
|
||||
Paypal,
|
||||
CryptoCurrency,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@ -135,6 +135,16 @@ pub enum PaymentAttemptUpdate {
|
||||
payment_method_id: Option<Option<String>>,
|
||||
mandate_id: Option<String>,
|
||||
connector_metadata: Option<serde_json::Value>,
|
||||
error_code: Option<Option<String>>,
|
||||
error_message: Option<Option<String>>,
|
||||
},
|
||||
UnresolvedResponseUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
connector: Option<serde_json::Value>,
|
||||
connector_transaction_id: Option<String>,
|
||||
payment_method_id: Option<Option<String>>,
|
||||
error_code: Option<Option<String>>,
|
||||
error_message: Option<Option<String>>,
|
||||
},
|
||||
StatusUpdate {
|
||||
status: storage_enums::AttemptStatus,
|
||||
@ -142,8 +152,8 @@ pub enum PaymentAttemptUpdate {
|
||||
ErrorUpdate {
|
||||
connector: Option<serde_json::Value>,
|
||||
status: storage_enums::AttemptStatus,
|
||||
error_code: Option<String>,
|
||||
error_message: Option<String>,
|
||||
error_code: Option<Option<String>>,
|
||||
error_message: Option<Option<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
@ -157,14 +167,14 @@ pub struct PaymentAttemptUpdateInternal {
|
||||
connector: Option<serde_json::Value>,
|
||||
authentication_type: Option<storage_enums::AuthenticationType>,
|
||||
payment_method: Option<storage_enums::PaymentMethod>,
|
||||
error_message: Option<String>,
|
||||
error_message: Option<Option<String>>,
|
||||
payment_method_id: Option<Option<String>>,
|
||||
cancellation_reason: Option<String>,
|
||||
modified_at: Option<PrimitiveDateTime>,
|
||||
mandate_id: Option<String>,
|
||||
browser_info: Option<serde_json::Value>,
|
||||
payment_token: Option<String>,
|
||||
error_code: Option<String>,
|
||||
error_code: Option<Option<String>>,
|
||||
connector_metadata: Option<serde_json::Value>,
|
||||
payment_method_data: Option<serde_json::Value>,
|
||||
payment_method_type: Option<storage_enums::PaymentMethodType>,
|
||||
@ -184,7 +194,7 @@ impl PaymentAttemptUpdate {
|
||||
.or(pa_update.connector_transaction_id),
|
||||
authentication_type: pa_update.authentication_type.or(source.authentication_type),
|
||||
payment_method: pa_update.payment_method.or(source.payment_method),
|
||||
error_message: pa_update.error_message.or(source.error_message),
|
||||
error_message: pa_update.error_message.unwrap_or(source.error_message),
|
||||
payment_method_id: pa_update
|
||||
.payment_method_id
|
||||
.unwrap_or(source.payment_method_id),
|
||||
@ -274,6 +284,8 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
payment_method_id,
|
||||
mandate_id,
|
||||
connector_metadata,
|
||||
error_code,
|
||||
error_message,
|
||||
} => Self {
|
||||
status: Some(status),
|
||||
connector,
|
||||
@ -283,6 +295,8 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
modified_at: Some(common_utils::date_time::now()),
|
||||
mandate_id,
|
||||
connector_metadata,
|
||||
error_code,
|
||||
error_message,
|
||||
..Default::default()
|
||||
},
|
||||
PaymentAttemptUpdate::ErrorUpdate {
|
||||
@ -310,6 +324,23 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
connector,
|
||||
..Default::default()
|
||||
},
|
||||
PaymentAttemptUpdate::UnresolvedResponseUpdate {
|
||||
status,
|
||||
connector,
|
||||
connector_transaction_id,
|
||||
payment_method_id,
|
||||
error_code,
|
||||
error_message,
|
||||
} => Self {
|
||||
status: Some(status),
|
||||
connector,
|
||||
connector_transaction_id,
|
||||
payment_method_id,
|
||||
modified_at: Some(common_utils::date_time::now()),
|
||||
error_code,
|
||||
error_message,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,6 +265,7 @@ fn make_client_secret_null_based_on_status(
|
||||
| storage_enums::IntentStatus::Cancelled => Some(None),
|
||||
storage_enums::IntentStatus::Processing
|
||||
| storage_enums::IntentStatus::RequiresCustomerAction
|
||||
| storage_enums::IntentStatus::RequiresMerchantAction
|
||||
| storage_enums::IntentStatus::RequiresPaymentMethod
|
||||
| storage_enums::IntentStatus::RequiresConfirmation
|
||||
| storage_enums::IntentStatus::RequiresCapture => None,
|
||||
|
||||
@ -64,6 +64,7 @@ bambora.base_url = "https://api.na.bambora.com"
|
||||
bluesnap.base_url = "https://sandbox.bluesnap.com/"
|
||||
braintree.base_url = "https://api.sandbox.braintreegateway.com/"
|
||||
checkout.base_url = "https://api.sandbox.checkout.com/"
|
||||
coinbase.base_url = "https://api.commerce.coinbase.com"
|
||||
cybersource.base_url = "https://apitest.cybersource.com/"
|
||||
dlocal.base_url = "https://sandbox.dlocal.com/"
|
||||
fiserv.base_url = "https://cert.api.fiservapps.com/"
|
||||
@ -72,6 +73,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/"
|
||||
mollie.base_url = "https://api.mollie.com/v2/"
|
||||
multisafepay.base_url = "https://testapi.multisafepay.com/"
|
||||
nuvei.base_url = "https://ppp-test.nuvei.com/"
|
||||
opennode.base_url = "https://dev-api.opennode.com"
|
||||
paypal.base_url = "https://www.sandbox.paypal.com/"
|
||||
payu.base_url = "https://secure.snd.payu.com/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
@ -93,6 +95,7 @@ cards = [
|
||||
"bluesnap",
|
||||
"braintree",
|
||||
"checkout",
|
||||
"coinbase",
|
||||
"cybersource",
|
||||
"dlocal",
|
||||
"fiserv",
|
||||
@ -100,6 +103,7 @@ cards = [
|
||||
"mollie",
|
||||
"multisafepay",
|
||||
"nuvei",
|
||||
"opennode",
|
||||
"paypal",
|
||||
"payu",
|
||||
"shift4",
|
||||
|
||||
20
migrations/2023-03-26-163105_add_unresolved_status/down.sql
Normal file
20
migrations/2023-03-26-163105_add_unresolved_status/down.sql
Normal file
@ -0,0 +1,20 @@
|
||||
DELETE FROM pg_enum
|
||||
WHERE enumlabel = 'unresolved'
|
||||
AND enumtypid = (
|
||||
SELECT oid FROM pg_type WHERE typname = 'AttemptStatus'
|
||||
);
|
||||
DELETE FROM pg_enum
|
||||
WHERE enumlabel = 'requires_merchant_action'
|
||||
AND enumtypid = (
|
||||
SELECT oid FROM pg_type WHERE typname = 'IntentStatus'
|
||||
);
|
||||
DELETE FROM pg_enum
|
||||
WHERE enumlabel = 'action_required'
|
||||
AND enumtypid = (
|
||||
SELECT oid FROM pg_type WHERE typname = 'EventType'
|
||||
);
|
||||
DELETE FROM pg_enum
|
||||
WHERE enumlabel = 'payment_processing'
|
||||
AND enumtypid = (
|
||||
SELECT oid FROM pg_type WHERE typname = 'EventType'
|
||||
);
|
||||
@ -0,0 +1,4 @@
|
||||
ALTER TYPE "AttemptStatus" ADD VALUE IF NOT EXISTS 'unresolved';
|
||||
ALTER TYPE "IntentStatus" ADD VALUE IF NOT EXISTS 'requires_merchant_action' after 'requires_customer_action';
|
||||
ALTER TYPE "EventType" ADD VALUE IF NOT EXISTS 'action_required';
|
||||
ALTER TYPE "EventType" ADD VALUE IF NOT EXISTS 'payment_processing';
|
||||
@ -4,7 +4,7 @@ function find_prev_connector() {
|
||||
git checkout $self
|
||||
cp $self $self.tmp
|
||||
# add new connector to existing list and sort it
|
||||
connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout cybersource dlocal fiserv globalpay klarna mollie multisafepay nuvei payu rapyd shift4 stripe trustpay worldline worldpay "$1")
|
||||
connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal fiserv globalpay klarna mollie multisafepay nuvei opennode payu rapyd shift4 stripe trustpay worldline worldpay "$1")
|
||||
IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS
|
||||
res=`echo ${sorted[@]}`
|
||||
sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp
|
||||
|
||||
Reference in New Issue
Block a user