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:
Arvind Patel
2023-04-11 13:05:57 +05:30
committed by GitHub
parent f46eaf3e3d
commit b3d1473734
43 changed files with 3053 additions and 92 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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)?;

View File

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

View File

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

View File

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

View 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(&notif.event)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)
}
}

View 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,
}

View 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(&notif.status)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)
}
}

View 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,
}

View File

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

View File

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

View File

@ -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!(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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";
}

View File

@ -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)]

View File

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

View File

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