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:
Arvind Patel
2023-06-30 14:57:35 +05:30
committed by GitHub
parent 15c2a70b42
commit 7bb0aa5ceb
24 changed files with 872 additions and 15 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
}

View File

@ -21,6 +21,7 @@ mod checkout;
mod checkout_ui;
mod coinbase;
mod connector_auth;
mod cryptopay;
mod cybersource;
mod dlocal;
#[cfg(feature = "dummy_connector")]

View File

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

View File

@ -130,6 +130,10 @@ pypl_pass=""
gmail_email=""
gmail_pass=""
[cryptopay]
api_key = "api_key"
key1 = "key1"
[payme]
api_key="API Key"

View File

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

View File

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