mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +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:
@ -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,77 +544,67 @@ 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;
|
||||
let object_ref_id = connector
|
||||
.get_webhook_object_reference_id(&request_details)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not find object reference id in incoming webhook body")?;
|
||||
|
||||
if process_webhook_further {
|
||||
let object_ref_id = connector
|
||||
.get_webhook_object_reference_id(&request_details)
|
||||
let event_object = connector
|
||||
.get_webhook_resource_object(&request_details)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not find resource object in incoming webhook body")?;
|
||||
|
||||
let webhook_details = api::IncomingWebhookDetails {
|
||||
object_reference_id: object_ref_id,
|
||||
resource_object: Encode::<serde_json::Value>::encode_to_vec(&event_object)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not find object reference id in incoming webhook body")?;
|
||||
.attach_printable(
|
||||
"There was an issue when encoding the incoming webhook body to bytes",
|
||||
)?,
|
||||
};
|
||||
|
||||
let event_object = connector
|
||||
.get_webhook_resource_object(&request_details)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not find resource object in incoming webhook body")?;
|
||||
let flow_type: api::WebhookFlow = event_type.to_owned().into();
|
||||
match flow_type {
|
||||
api::WebhookFlow::Payment => payments_incoming_webhook_flow::<W>(
|
||||
state.clone(),
|
||||
merchant_account,
|
||||
webhook_details,
|
||||
source_verified,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Incoming webhook flow for payments failed")?,
|
||||
|
||||
let webhook_details = api::IncomingWebhookDetails {
|
||||
object_reference_id: object_ref_id,
|
||||
resource_object: Encode::<serde_json::Value>::encode_to_vec(&event_object)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"There was an issue when encoding the incoming webhook body to bytes",
|
||||
)?,
|
||||
};
|
||||
api::WebhookFlow::Refund => refunds_incoming_webhook_flow::<W>(
|
||||
state.clone(),
|
||||
merchant_account,
|
||||
webhook_details,
|
||||
connector_name,
|
||||
source_verified,
|
||||
event_type,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Incoming webhook flow for refunds failed")?,
|
||||
|
||||
let flow_type: api::WebhookFlow = event_type.to_owned().into();
|
||||
match flow_type {
|
||||
api::WebhookFlow::Payment => payments_incoming_webhook_flow::<W>(
|
||||
state.clone(),
|
||||
merchant_account,
|
||||
webhook_details,
|
||||
source_verified,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Incoming webhook flow for payments failed")?,
|
||||
api::WebhookFlow::Dispute => disputes_incoming_webhook_flow::<W>(
|
||||
state.clone(),
|
||||
merchant_account,
|
||||
webhook_details,
|
||||
source_verified,
|
||||
*connector,
|
||||
&request_details,
|
||||
event_type,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Incoming webhook flow for disputes failed")?,
|
||||
|
||||
api::WebhookFlow::Refund => refunds_incoming_webhook_flow::<W>(
|
||||
state.clone(),
|
||||
merchant_account,
|
||||
webhook_details,
|
||||
connector_name,
|
||||
source_verified,
|
||||
event_type,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Incoming webhook flow for refunds failed")?,
|
||||
api::WebhookFlow::ReturnResponse => {}
|
||||
|
||||
api::WebhookFlow::Dispute => disputes_incoming_webhook_flow::<W>(
|
||||
state.clone(),
|
||||
merchant_account,
|
||||
webhook_details,
|
||||
source_verified,
|
||||
*connector,
|
||||
&request_details,
|
||||
event_type,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Incoming webhook flow for disputes failed")?,
|
||||
|
||||
api::WebhookFlow::ReturnResponse => {}
|
||||
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError)
|
||||
.into_report()
|
||||
.attach_printable("Unsupported Flow Type received in incoming webhooks")?,
|
||||
}
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError)
|
||||
.into_report()
|
||||
.attach_printable("Unsupported Flow Type received in incoming webhooks")?,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user