mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
feat(connector): [cryptopay] add new connector cryptopay, authorize, sync, webhook and testcases (#1511)
Co-authored-by: arvindpatel24 <arvind.patel@juspay.in>
This commit is contained in:
@ -22,6 +22,7 @@ aci = "aci" # Name of a connector
|
||||
encrypter = "encrypter" # Used by the `ring` crate
|
||||
nin = "nin" # National identification number, a field used by PayU connector
|
||||
substituters = "substituters" # Present in `flake.nix`
|
||||
unsuccess = "unsuccess" # Used in cryptopay request
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
|
||||
@ -161,6 +161,7 @@ braintree.base_url = "https://api.sandbox.braintreegateway.com/"
|
||||
cashtocode.base_url = "https://cluster05.api-test.cashtocode.com"
|
||||
checkout.base_url = "https://api.sandbox.checkout.com/"
|
||||
coinbase.base_url = "https://api.commerce.coinbase.com"
|
||||
cryptopay.base_url = "https://business-sandbox.cryptopay.me"
|
||||
cybersource.base_url = "https://apitest.cybersource.com/"
|
||||
dlocal.base_url = "https://sandbox.dlocal.com/"
|
||||
dummyconnector.base_url = "http://localhost:8080/dummy-connector"
|
||||
@ -215,6 +216,7 @@ cards = [
|
||||
"adyen",
|
||||
"authorizedotnet",
|
||||
"coinbase",
|
||||
"cryptopay",
|
||||
"braintree",
|
||||
"checkout",
|
||||
"cybersource",
|
||||
|
||||
@ -65,6 +65,7 @@ cards = [
|
||||
"braintree",
|
||||
"checkout",
|
||||
"coinbase",
|
||||
"cryptopay",
|
||||
"cybersource",
|
||||
"dlocal",
|
||||
"dummyconnector",
|
||||
@ -118,6 +119,7 @@ braintree.base_url = "https://api.sandbox.braintreegateway.com/"
|
||||
cashtocode.base_url = "https://cluster05.api-test.cashtocode.com"
|
||||
checkout.base_url = "https://api.sandbox.checkout.com/"
|
||||
coinbase.base_url = "https://api.commerce.coinbase.com"
|
||||
cryptopay.base_url = "https://business-sandbox.cryptopay.me"
|
||||
cybersource.base_url = "https://apitest.cybersource.com/"
|
||||
dlocal.base_url = "https://sandbox.dlocal.com/"
|
||||
dummyconnector.base_url = "http://localhost:8080/dummy-connector"
|
||||
|
||||
@ -82,6 +82,7 @@ braintree.base_url = "https://api.sandbox.braintreegateway.com/"
|
||||
cashtocode.base_url = "https://cluster05.api-test.cashtocode.com"
|
||||
checkout.base_url = "https://api.sandbox.checkout.com/"
|
||||
coinbase.base_url = "https://api.commerce.coinbase.com"
|
||||
cryptopay.base_url = "https://business-sandbox.cryptopay.me"
|
||||
cybersource.base_url = "https://apitest.cybersource.com/"
|
||||
dlocal.base_url = "https://sandbox.dlocal.com/"
|
||||
dummyconnector.base_url = "http://localhost:8080/dummy-connector"
|
||||
@ -127,6 +128,7 @@ cards = [
|
||||
"braintree",
|
||||
"checkout",
|
||||
"coinbase",
|
||||
"cryptopay",
|
||||
"cybersource",
|
||||
"dlocal",
|
||||
"dummyconnector",
|
||||
|
||||
@ -599,6 +599,7 @@ pub enum Connector {
|
||||
Cashtocode,
|
||||
Checkout,
|
||||
Coinbase,
|
||||
Cryptopay,
|
||||
Cybersource,
|
||||
Iatapay,
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
@ -699,6 +700,7 @@ pub enum RoutableConnectors {
|
||||
Cashtocode,
|
||||
Checkout,
|
||||
Coinbase,
|
||||
Cryptopay,
|
||||
Cybersource,
|
||||
Dlocal,
|
||||
Fiserv,
|
||||
|
||||
@ -771,7 +771,9 @@ pub struct SepaAndBacsBillingDetails {
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CryptoData {}
|
||||
pub struct CryptoData {
|
||||
pub pay_currency: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||
pub struct SofortBilling {
|
||||
|
||||
@ -154,6 +154,34 @@ impl DecodeMessage for NoAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the HMAC-SHA-1 algorithm
|
||||
#[derive(Debug)]
|
||||
pub struct HmacSha1;
|
||||
|
||||
impl SignMessage for HmacSha1 {
|
||||
fn sign_message(
|
||||
&self,
|
||||
secret: &[u8],
|
||||
msg: &[u8],
|
||||
) -> CustomResult<Vec<u8>, errors::CryptoError> {
|
||||
let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, secret);
|
||||
Ok(hmac::sign(&key, msg).as_ref().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl VerifySignature for HmacSha1 {
|
||||
fn verify_signature(
|
||||
&self,
|
||||
secret: &[u8],
|
||||
signature: &[u8],
|
||||
msg: &[u8],
|
||||
) -> CustomResult<bool, errors::CryptoError> {
|
||||
let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, secret);
|
||||
|
||||
Ok(hmac::verify(&key, msg, signature).is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the HMAC-SHA-256 algorithm
|
||||
#[derive(Debug)]
|
||||
pub struct HmacSha256;
|
||||
|
||||
@ -388,6 +388,7 @@ pub struct Connectors {
|
||||
pub cashtocode: ConnectorParams,
|
||||
pub checkout: ConnectorParams,
|
||||
pub coinbase: ConnectorParams,
|
||||
pub cryptopay: ConnectorParams,
|
||||
pub cybersource: ConnectorParams,
|
||||
pub dlocal: ConnectorParams,
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
|
||||
@ -9,6 +9,7 @@ pub mod braintree;
|
||||
pub mod cashtocode;
|
||||
pub mod checkout;
|
||||
pub mod coinbase;
|
||||
pub mod cryptopay;
|
||||
pub mod cybersource;
|
||||
pub mod dlocal;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
@ -44,10 +45,10 @@ pub use self::dummyconnector::DummyConnector;
|
||||
pub use self::{
|
||||
aci::Aci, adyen::Adyen, airwallex::Airwallex, authorizedotnet::Authorizedotnet,
|
||||
bambora::Bambora, bitpay::Bitpay, bluesnap::Bluesnap, braintree::Braintree,
|
||||
cashtocode::Cashtocode, checkout::Checkout, coinbase::Coinbase, cybersource::Cybersource,
|
||||
dlocal::Dlocal, fiserv::Fiserv, forte::Forte, globalpay::Globalpay, iatapay::Iatapay,
|
||||
klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi,
|
||||
noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme,
|
||||
paypal::Paypal, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay,
|
||||
worldline::Worldline, worldpay::Worldpay, zen::Zen,
|
||||
cashtocode::Cashtocode, checkout::Checkout, coinbase::Coinbase, cryptopay::Cryptopay,
|
||||
cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, forte::Forte, globalpay::Globalpay,
|
||||
iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay,
|
||||
nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode,
|
||||
payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, rapyd::Rapyd, shift4::Shift4,
|
||||
stripe::Stripe, trustpay::Trustpay, worldline::Worldline, worldpay::Worldpay, zen::Zen,
|
||||
};
|
||||
|
||||
456
crates/router/src/connector/cryptopay.rs
Normal file
456
crates/router/src/connector/cryptopay.rs
Normal file
@ -0,0 +1,456 @@
|
||||
mod transformers;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use base64::Engine;
|
||||
use common_utils::{
|
||||
crypto::{self, GenerateDigest, SignMessage},
|
||||
date_time,
|
||||
ext_traits::ByteSliceExt,
|
||||
};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use hex::encode;
|
||||
use masking::PeekInterface;
|
||||
use transformers as cryptopay;
|
||||
|
||||
use self::cryptopay::CryptopayWebhookDetails;
|
||||
use super::utils;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
consts,
|
||||
core::errors::{self, CustomResult},
|
||||
db, headers,
|
||||
services::{
|
||||
self,
|
||||
request::{self, Mask},
|
||||
ConnectorIntegration,
|
||||
},
|
||||
types::{
|
||||
self,
|
||||
api::{self, ConnectorCommon, ConnectorCommonExt},
|
||||
ErrorResponse, Response,
|
||||
},
|
||||
utils::{BytesExt, Encode},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cryptopay;
|
||||
|
||||
impl api::Payment for Cryptopay {}
|
||||
impl api::PaymentSession for Cryptopay {}
|
||||
impl api::ConnectorAccessToken for Cryptopay {}
|
||||
impl api::PreVerify for Cryptopay {}
|
||||
impl api::PaymentAuthorize for Cryptopay {}
|
||||
impl api::PaymentSync for Cryptopay {}
|
||||
impl api::PaymentCapture for Cryptopay {}
|
||||
impl api::PaymentVoid for Cryptopay {}
|
||||
impl api::Refund for Cryptopay {}
|
||||
impl api::RefundExecute for Cryptopay {}
|
||||
impl api::RefundSync for Cryptopay {}
|
||||
impl api::PaymentToken for Cryptopay {}
|
||||
|
||||
impl
|
||||
ConnectorIntegration<
|
||||
api::PaymentMethodToken,
|
||||
types::PaymentMethodTokenizationData,
|
||||
types::PaymentsResponseData,
|
||||
> for Cryptopay
|
||||
{
|
||||
// Not Implemented (R)
|
||||
}
|
||||
|
||||
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Cryptopay
|
||||
where
|
||||
Self: ConnectorIntegration<Flow, Request, Response>,
|
||||
{
|
||||
fn build_headers(
|
||||
&self,
|
||||
req: &types::RouterData<Flow, Request, Response>,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
|
||||
let api_method;
|
||||
let payload = match self.get_request_body(req)? {
|
||||
Some(val) => {
|
||||
let body = types::RequestBody::get_inner_value(val).peek().to_owned();
|
||||
api_method = "POST".to_string();
|
||||
let md5_payload = crypto::Md5
|
||||
.generate_digest(body.as_bytes())
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
encode(md5_payload)
|
||||
}
|
||||
None => {
|
||||
api_method = "GET".to_string();
|
||||
String::default()
|
||||
}
|
||||
};
|
||||
|
||||
let now = date_time::date_as_yyyymmddthhmmssmmmz()
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
let date = format!("{}+00:00", now.split_at(now.len() - 5).0);
|
||||
|
||||
let content_type = self.get_content_type().to_string();
|
||||
|
||||
let api = (self.get_url(req, connectors)?).replace(self.base_url(connectors), "");
|
||||
|
||||
let auth = cryptopay::CryptopayAuthType::try_from(&req.connector_auth_type)?;
|
||||
|
||||
let sign_req: String = format!(
|
||||
"{}\n{}\n{}\n{}\n{}",
|
||||
api_method, payload, content_type, date, api
|
||||
);
|
||||
let authz = crypto::HmacSha1::sign_message(
|
||||
&crypto::HmacSha1,
|
||||
auth.api_secret.peek().as_bytes(),
|
||||
sign_req.as_bytes(),
|
||||
)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)
|
||||
.attach_printable("Failed to sign the message")?;
|
||||
let authz = consts::BASE64_ENGINE.encode(authz);
|
||||
let auth_string: String = format!("HMAC {}:{}", auth.api_key.peek(), authz);
|
||||
|
||||
let headers = vec![
|
||||
(
|
||||
headers::AUTHORIZATION.to_string(),
|
||||
auth_string.into_masked(),
|
||||
),
|
||||
(headers::DATE.to_string(), date.into()),
|
||||
(
|
||||
headers::CONTENT_TYPE.to_string(),
|
||||
Self.get_content_type().to_string().into(),
|
||||
),
|
||||
];
|
||||
Ok(headers)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorCommon for Cryptopay {
|
||||
fn id(&self) -> &'static str {
|
||||
"cryptopay"
|
||||
}
|
||||
|
||||
fn common_get_content_type(&self) -> &'static str {
|
||||
"application/json"
|
||||
}
|
||||
|
||||
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
|
||||
connectors.cryptopay.base_url.as_ref()
|
||||
}
|
||||
|
||||
fn get_auth_header(
|
||||
&self,
|
||||
auth_type: &types::ConnectorAuthType,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
|
||||
let auth = cryptopay::CryptopayAuthType::try_from(auth_type)
|
||||
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
Ok(vec![(
|
||||
headers::AUTHORIZATION.to_string(),
|
||||
auth.api_key.peek().to_owned().into_masked(),
|
||||
)])
|
||||
}
|
||||
|
||||
fn build_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
let response: cryptopay::CryptopayErrorResponse = res
|
||||
.response
|
||||
.parse_struct("CryptopayErrorResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
Ok(ErrorResponse {
|
||||
status_code: res.status_code,
|
||||
code: response.error.code,
|
||||
message: response.error.message,
|
||||
reason: response.error.reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Cryptopay
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
|
||||
for Cryptopay
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||
for Cryptopay
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
for Cryptopay
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<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!("{}/api/invoices", self.base_url(connectors)))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
|
||||
let connector_request = cryptopay::CryptopayPaymentsRequest::try_from(req)?;
|
||||
let cryptopay_req = types::RequestBody::log_and_get_request_body(
|
||||
&connector_request,
|
||||
Encode::<cryptopay::CryptopayPaymentsRequest>::encode_to_string_of_json,
|
||||
)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(cryptopay_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,
|
||||
)?)
|
||||
.attach_default_headers()
|
||||
.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: cryptopay::CryptopayPaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Cryptopay PaymentsAuthorizeResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
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 Cryptopay
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<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!(
|
||||
"{}/api/invoices/{}",
|
||||
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)?)
|
||||
.attach_default_headers()
|
||||
.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: cryptopay::CryptopayPaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("cryptopay PaymentsSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
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 Cryptopay
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
|
||||
for Cryptopay
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
|
||||
for Cryptopay
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
|
||||
for Cryptopay
|
||||
{
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl api::IncomingWebhook for Cryptopay {
|
||||
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-Cryptopay-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 = utils::get_webhook_merchant_secret_key(self.id(), merchant_id);
|
||||
let secret = match db.find_config_by_key(&key).await {
|
||||
Ok(config) => Some(config),
|
||||
Err(e) => {
|
||||
crate::logger::warn!("Unable to fetch merchant webhook secret from DB: {:#?}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(secret
|
||||
.map(|conf| conf.config.into_bytes())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
fn get_webhook_object_reference_id(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
||||
let notif: CryptopayWebhookDetails =
|
||||
request
|
||||
.body
|
||||
.parse_struct("CryptopayWebhookDetails")
|
||||
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
|
||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(notif.data.id),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
let notif: CryptopayWebhookDetails =
|
||||
request
|
||||
.body
|
||||
.parse_struct("CryptopayWebhookDetails")
|
||||
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||
match notif.data.status {
|
||||
cryptopay::CryptopayPaymentStatus::Completed => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentIntentSuccess)
|
||||
}
|
||||
cryptopay::CryptopayPaymentStatus::Unresolved => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentActionRequired)
|
||||
}
|
||||
cryptopay::CryptopayPaymentStatus::Cancelled => {
|
||||
Ok(api::IncomingWebhookEvent::PaymentIntentFailure)
|
||||
}
|
||||
_ => Ok(api::IncomingWebhookEvent::EventNotSupported),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_webhook_resource_object(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||
let notif: CryptopayWebhookDetails =
|
||||
request
|
||||
.body
|
||||
.parse_struct("CryptopayWebhookDetails")
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
Encode::<CryptopayWebhookDetails>::encode_to_value(¬if)
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)
|
||||
}
|
||||
}
|
||||
174
crates/router/src/connector/cryptopay/transformers.rs
Normal file
174
crates/router/src/connector/cryptopay/transformers.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use masking::Secret;
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
connector::utils::CryptoData,
|
||||
core::errors,
|
||||
services,
|
||||
types::{self, api, storage::enums},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
pub struct CryptopayPaymentsRequest {
|
||||
price_amount: i64,
|
||||
price_currency: enums::Currency,
|
||||
pay_currency: String,
|
||||
success_redirect_url: Option<String>,
|
||||
unsuccess_redirect_url: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for CryptopayPaymentsRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
let cryptopay_request = match item.request.payment_method_data {
|
||||
api::PaymentMethodData::Crypto(ref cryptodata) => {
|
||||
let pay_currency = cryptodata.get_pay_currency()?;
|
||||
Ok(Self {
|
||||
price_amount: item.request.amount,
|
||||
price_currency: item.request.currency,
|
||||
pay_currency,
|
||||
success_redirect_url: item.clone().request.router_return_url,
|
||||
unsuccess_redirect_url: item.clone().request.router_return_url,
|
||||
})
|
||||
}
|
||||
_ => Err(errors::ConnectorError::NotImplemented(
|
||||
"payment method".to_string(),
|
||||
)),
|
||||
}?;
|
||||
Ok(cryptopay_request)
|
||||
}
|
||||
}
|
||||
|
||||
// Auth Struct
|
||||
pub struct CryptopayAuthType {
|
||||
pub(super) api_key: Secret<String>,
|
||||
pub(super) api_secret: Secret<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::ConnectorAuthType> for CryptopayAuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
if let types::ConnectorAuthType::BodyKey { api_key, key1 } = auth_type {
|
||||
Ok(Self {
|
||||
api_key: api_key.to_string().into(),
|
||||
api_secret: key1.to_string().into(),
|
||||
})
|
||||
} else {
|
||||
Err(errors::ConnectorError::FailedToObtainAuthType.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
// PaymentsResponse
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CryptopayPaymentStatus {
|
||||
#[default]
|
||||
New,
|
||||
Completed,
|
||||
Unresolved,
|
||||
Refunded,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl From<CryptopayPaymentStatus> for enums::AttemptStatus {
|
||||
fn from(item: CryptopayPaymentStatus) -> Self {
|
||||
match item {
|
||||
CryptopayPaymentStatus::New => Self::AuthenticationPending,
|
||||
CryptopayPaymentStatus::Completed => Self::Charged,
|
||||
CryptopayPaymentStatus::Cancelled => Self::Failure,
|
||||
CryptopayPaymentStatus::Unresolved => Self::Unresolved,
|
||||
_ => Self::Voided,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub struct CryptopayPaymentsResponse {
|
||||
data: CryptopayPaymentResponseData,
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, CryptopayPaymentsResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
CryptopayPaymentsResponse,
|
||||
T,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let redirection_data = item
|
||||
.response
|
||||
.data
|
||||
.hosted_page_url
|
||||
.map(|x| services::RedirectForm::from((x, services::Method::Get)));
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::from(item.response.data.status),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.data.id),
|
||||
redirection_data,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub struct CryptopayErrorData {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub struct CryptopayErrorResponse {
|
||||
pub error: CryptopayErrorData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct CryptopayPaymentResponseData {
|
||||
pub id: String,
|
||||
pub customer_id: Option<String>,
|
||||
pub status: CryptopayPaymentStatus,
|
||||
pub status_context: Option<String>,
|
||||
pub address: Option<String>,
|
||||
pub network: Option<String>,
|
||||
pub uri: Option<String>,
|
||||
pub price_amount: Option<String>,
|
||||
pub price_currency: Option<String>,
|
||||
pub pay_amount: Option<String>,
|
||||
pub pay_currency: Option<String>,
|
||||
pub fee: Option<String>,
|
||||
pub fee_currency: Option<String>,
|
||||
pub paid_amount: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub success_redirect_url: Option<String>,
|
||||
pub unsuccess_redirect_url: Option<String>,
|
||||
pub hosted_page_url: Option<Url>,
|
||||
pub created_at: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CryptopayWebhookDetails {
|
||||
#[serde(rename = "type")]
|
||||
pub service_type: String,
|
||||
pub event: WebhookEvent,
|
||||
pub data: CryptopayPaymentResponseData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WebhookEvent {
|
||||
TransactionCreated,
|
||||
TransactionConfirmed,
|
||||
StatusChanged,
|
||||
}
|
||||
@ -595,6 +595,19 @@ impl ApplePay for payments::ApplePayWalletData {
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CryptoData {
|
||||
fn get_pay_currency(&self) -> Result<String, Error>;
|
||||
}
|
||||
|
||||
impl CryptoData for api::CryptoData {
|
||||
fn get_pay_currency(&self) -> Result<String, Error> {
|
||||
self.pay_currency
|
||||
.clone()
|
||||
.ok_or_else(missing_field_err("crypto_data.pay_currency"))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PhoneDetailsData {
|
||||
fn get_number(&self) -> Result<Secret<String>, Error>;
|
||||
fn get_country_code(&self) -> Result<String, Error>;
|
||||
|
||||
@ -145,6 +145,7 @@ default_imp_for_complete_authorize!(
|
||||
connector::Cashtocode,
|
||||
connector::Checkout,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
@ -206,6 +207,7 @@ default_imp_for_create_customer!(
|
||||
connector::Cashtocode,
|
||||
connector::Checkout,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
@ -270,6 +272,7 @@ default_imp_for_connector_redirect_response!(
|
||||
connector::Braintree,
|
||||
connector::Cashtocode,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
@ -313,6 +316,7 @@ default_imp_for_connector_request_id!(
|
||||
connector::Cashtocode,
|
||||
connector::Checkout,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
@ -380,6 +384,7 @@ default_imp_for_accept_dispute!(
|
||||
connector::Braintree,
|
||||
connector::Cashtocode,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
@ -468,6 +473,7 @@ default_imp_for_file_upload!(
|
||||
connector::Braintree,
|
||||
connector::Cashtocode,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
@ -534,6 +540,7 @@ default_imp_for_submit_evidence!(
|
||||
connector::Cashtocode,
|
||||
connector::Cybersource,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
connector::Forte,
|
||||
@ -599,6 +606,7 @@ default_imp_for_defend_dispute!(
|
||||
connector::Cashtocode,
|
||||
connector::Cybersource,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
connector::Forte,
|
||||
@ -665,6 +673,7 @@ default_imp_for_pre_processing_steps!(
|
||||
connector::Cashtocode,
|
||||
connector::Checkout,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Iatapay,
|
||||
|
||||
@ -212,6 +212,7 @@ impl ConnectorData {
|
||||
enums::Connector::Cashtocode => Ok(Box::new(&connector::Cashtocode)),
|
||||
enums::Connector::Checkout => Ok(Box::new(&connector::Checkout)),
|
||||
enums::Connector::Coinbase => Ok(Box::new(&connector::Coinbase)),
|
||||
enums::Connector::Cryptopay => Ok(Box::new(&connector::Cryptopay)),
|
||||
enums::Connector::Cybersource => Ok(Box::new(&connector::Cybersource)),
|
||||
enums::Connector::Dlocal => Ok(Box::new(&connector::Dlocal)),
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
pub use api_models::payments::{
|
||||
AcceptanceType, Address, AddressDetails, Amount, AuthenticationForStartResponse, Card,
|
||||
CustomerAcceptance, MandateData, MandateTransactionType, MandateType, MandateValidationFields,
|
||||
NextActionType, OnlineMandate, PayLaterData, PaymentIdType, PaymentListConstraints,
|
||||
PaymentListResponse, PaymentMethodData, PaymentMethodDataResponse, PaymentOp,
|
||||
PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsCancelRequest,
|
||||
CryptoData, CustomerAcceptance, MandateData, MandateTransactionType, MandateType,
|
||||
MandateValidationFields, NextActionType, OnlineMandate, PayLaterData, PaymentIdType,
|
||||
PaymentListConstraints, PaymentListResponse, PaymentMethodData, PaymentMethodDataResponse,
|
||||
PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsCancelRequest,
|
||||
PaymentsCaptureRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, PaymentsRequest,
|
||||
PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest,
|
||||
PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, PhoneDetails,
|
||||
|
||||
@ -64,7 +64,9 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
amount: 1,
|
||||
currency: enums::Currency::USD,
|
||||
payment_method_data: types::api::PaymentMethodData::Crypto(CryptoData {}),
|
||||
payment_method_data: types::api::PaymentMethodData::Crypto(CryptoData {
|
||||
pay_currency: None,
|
||||
}),
|
||||
confirm: true,
|
||||
statement_descriptor_suffix: None,
|
||||
statement_descriptor: None,
|
||||
|
||||
@ -66,7 +66,9 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
amount: 1,
|
||||
currency: enums::Currency::USD,
|
||||
payment_method_data: types::api::PaymentMethodData::Crypto(CryptoData {}),
|
||||
payment_method_data: types::api::PaymentMethodData::Crypto(CryptoData {
|
||||
pay_currency: None,
|
||||
}),
|
||||
confirm: true,
|
||||
statement_descriptor_suffix: None,
|
||||
statement_descriptor: None,
|
||||
|
||||
@ -16,6 +16,7 @@ pub struct ConnectorAuthentication {
|
||||
pub cashtocode: Option<BodyKey>,
|
||||
pub checkout: Option<SignatureKey>,
|
||||
pub coinbase: Option<HeaderKey>,
|
||||
pub cryptopay: Option<BodyKey>,
|
||||
pub cybersource: Option<SignatureKey>,
|
||||
pub dlocal: Option<SignatureKey>,
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
|
||||
149
crates/router/tests/connectors/cryptopay.rs
Normal file
149
crates/router/tests/connectors/cryptopay.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use api_models::payments::CryptoData;
|
||||
use masking::Secret;
|
||||
use router::types::{self, api, storage::enums, PaymentAddress};
|
||||
|
||||
use crate::{
|
||||
connector_auth,
|
||||
utils::{self, ConnectorActions},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct CryptopayTest;
|
||||
impl ConnectorActions for CryptopayTest {}
|
||||
impl utils::Connector for CryptopayTest {
|
||||
fn get_data(&self) -> types::api::ConnectorData {
|
||||
use router::connector::Cryptopay;
|
||||
types::api::ConnectorData {
|
||||
connector: Box::new(&Cryptopay),
|
||||
connector_name: types::Connector::Cryptopay,
|
||||
get_token: types::api::GetToken::Connector,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auth_token(&self) -> types::ConnectorAuthType {
|
||||
types::ConnectorAuthType::from(
|
||||
connector_auth::ConnectorAuthentication::new()
|
||||
.cryptopay
|
||||
.expect("Missing connector authentication configuration"),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
"cryptopay".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
static CONNECTOR: CryptopayTest = CryptopayTest {};
|
||||
|
||||
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
|
||||
Some(utils::PaymentInfo {
|
||||
address: Some(PaymentAddress {
|
||||
billing: Some(api::Address {
|
||||
address: Some(api::AddressDetails {
|
||||
first_name: Some(Secret::new("first".to_string())),
|
||||
last_name: Some(Secret::new("last".to_string())),
|
||||
line1: Some(Secret::new("line1".to_string())),
|
||||
line2: Some(Secret::new("line2".to_string())),
|
||||
city: Some("city".to_string()),
|
||||
zip: Some(Secret::new("zip".to_string())),
|
||||
country: Some(api_models::enums::CountryAlpha2::IN),
|
||||
..Default::default()
|
||||
}),
|
||||
phone: Some(api::PhoneDetails {
|
||||
number: Some(Secret::new("1234567890".to_string())),
|
||||
country_code: Some("+91".to_string()),
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
return_url: Some(String::from("https://google.com")),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
amount: 1,
|
||||
currency: enums::Currency::USD,
|
||||
payment_method_data: types::api::PaymentMethodData::Crypto(CryptoData {
|
||||
pay_currency: Some("XRP".to_string()),
|
||||
}),
|
||||
confirm: true,
|
||||
statement_descriptor_suffix: None,
|
||||
statement_descriptor: None,
|
||||
setup_future_usage: None,
|
||||
mandate_id: None,
|
||||
off_session: None,
|
||||
setup_mandate_details: None,
|
||||
browser_info: None,
|
||||
order_details: None,
|
||||
order_category: None,
|
||||
email: None,
|
||||
payment_experience: None,
|
||||
payment_method_type: None,
|
||||
session_token: None,
|
||||
enrolled_for_3ds: false,
|
||||
related_transaction_id: None,
|
||||
router_return_url: Some(String::from("https://google.com/")),
|
||||
webhook_url: None,
|
||||
complete_authorize_url: None,
|
||||
capture_method: None,
|
||||
customer_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
// Creates a payment using the manual capture flow
|
||||
#[actix_web::test]
|
||||
async fn should_only_authorize_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_payment(payment_method_details(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::AuthenticationPending);
|
||||
let resp = response.response.ok().unwrap();
|
||||
let endpoint = match resp {
|
||||
types::PaymentsResponseData::TransactionResponse {
|
||||
redirection_data, ..
|
||||
} => Some(redirection_data),
|
||||
_ => None,
|
||||
};
|
||||
assert!(endpoint.is_some())
|
||||
}
|
||||
|
||||
// Synchronizes a successful transaction.
|
||||
#[actix_web::test]
|
||||
async fn should_sync_authorized_payment() {
|
||||
let response = CONNECTOR
|
||||
.psync_retry_till_status_matches(
|
||||
enums::AttemptStatus::Authorized,
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
"ea684036-2b54-44fa-bffe-8256650dce7c".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("PSync response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Synchronizes a unresolved(underpaid) transaction.
|
||||
#[actix_web::test]
|
||||
async fn should_sync_unresolved_payment() {
|
||||
let response = CONNECTOR
|
||||
.psync_retry_till_status_matches(
|
||||
enums::AttemptStatus::Authorized,
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
"7993d4c2-efbc-4360-b8ce-d1e957e6f827".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("PSync response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Unresolved);
|
||||
}
|
||||
@ -21,6 +21,7 @@ mod checkout;
|
||||
mod checkout_ui;
|
||||
mod coinbase;
|
||||
mod connector_auth;
|
||||
mod cryptopay;
|
||||
mod cybersource;
|
||||
mod dlocal;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
|
||||
@ -65,7 +65,9 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
amount: 1,
|
||||
currency: enums::Currency::USD,
|
||||
payment_method_data: types::api::PaymentMethodData::Crypto(CryptoData {}),
|
||||
payment_method_data: types::api::PaymentMethodData::Crypto(CryptoData {
|
||||
pay_currency: None,
|
||||
}),
|
||||
confirm: true,
|
||||
statement_descriptor_suffix: None,
|
||||
statement_descriptor: None,
|
||||
|
||||
@ -130,6 +130,10 @@ pypl_pass=""
|
||||
gmail_email=""
|
||||
gmail_pass=""
|
||||
|
||||
[cryptopay]
|
||||
api_key = "api_key"
|
||||
key1 = "key1"
|
||||
|
||||
[payme]
|
||||
api_key="API Key"
|
||||
|
||||
|
||||
@ -69,6 +69,7 @@ braintree.base_url = "https://api.sandbox.braintreegateway.com/"
|
||||
cashtocode.base_url = "https://cluster05.api-test.cashtocode.com"
|
||||
checkout.base_url = "https://api.sandbox.checkout.com/"
|
||||
coinbase.base_url = "https://api.commerce.coinbase.com"
|
||||
cryptopay.base_url = "https://business-sandbox.cryptopay.me"
|
||||
cybersource.base_url = "https://apitest.cybersource.com/"
|
||||
dlocal.base_url = "https://sandbox.dlocal.com/"
|
||||
dummyconnector.base_url = "http://localhost:8080/dummy-connector"
|
||||
@ -113,6 +114,7 @@ cards = [
|
||||
"braintree",
|
||||
"checkout",
|
||||
"coinbase",
|
||||
"cryptopay",
|
||||
"cybersource",
|
||||
"dlocal",
|
||||
"dummyconnector",
|
||||
|
||||
@ -4,7 +4,7 @@ function find_prev_connector() {
|
||||
git checkout $self
|
||||
cp $self $self.tmp
|
||||
# add new connector to existing list and sort it
|
||||
connectors=(aci adyen airwallex applepay authorizedotnet bambora bitpay bluesnap braintree cashtocode checkout coinbase cybersource dlocal dummyconnector fiserv forte globalpay iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu rapyd shift4 stripe trustpay worldline worldpay "$1")
|
||||
connectors=(aci adyen airwallex applepay authorizedotnet bambora bitpay bluesnap braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu rapyd shift4 stripe trustpay worldline worldpay "$1")
|
||||
IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS
|
||||
res=`echo ${sorted[@]}`
|
||||
sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp
|
||||
|
||||
Reference in New Issue
Block a user