feat(connector): Add connector cashtocode (#1429)

This commit is contained in:
BallaNitesh
2023-06-27 15:05:23 +05:30
committed by GitHub
parent 9969c930a9
commit 784847b08c
25 changed files with 888 additions and 26 deletions

View File

@ -158,6 +158,7 @@ bambora.base_url = "https://api.na.bambora.com"
bitpay.base_url = "https://test.bitpay.com"
bluesnap.base_url = "https://sandbox.bluesnap.com/"
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"
cybersource.base_url = "https://apitest.cybersource.com/"
@ -207,6 +208,7 @@ stripe = { banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,ba
# This data is used to call respective connectors for wallets and cards
[connectors.supported]
wallets = ["klarna", "braintree", "applepay"]
rewards = ["cashtocode"]
cards = [
"adyen",
"authorizedotnet",

View File

@ -53,6 +53,7 @@ tunnel_private_key = ""
[connectors.supported]
wallets = ["klarna", "braintree", "applepay"]
rewards = ["cashtocode",]
cards = [
"aci",
"adyen",
@ -112,6 +113,7 @@ bambora.base_url = "https://api.na.bambora.com"
bitpay.base_url = "https://test.bitpay.com"
bluesnap.base_url = "https://sandbox.bluesnap.com/"
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"
cybersource.base_url = "https://apitest.cybersource.com/"

View File

@ -79,6 +79,7 @@ bambora.base_url = "https://api.na.bambora.com"
bitpay.base_url = "https://test.bitpay.com"
bluesnap.base_url = "https://sandbox.bluesnap.com/"
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"
cybersource.base_url = "https://apitest.cybersource.com/"
@ -112,6 +113,7 @@ zen.base_url = "https://api.zen-test.com/"
[connectors.supported]
wallets = ["klarna", "braintree", "applepay"]
rewards = ["cashtocode",]
cards = [
"aci",
"adyen",

View File

@ -421,10 +421,13 @@ pub enum PaymentMethodType {
BancontactCard,
Becs,
Blik,
#[serde(rename = "classic")]
ClassicReward,
Credit,
CryptoCurrency,
Debit,
Eps,
Evoucher,
Giropay,
GooglePay,
Ideal,
@ -473,6 +476,7 @@ pub enum PaymentMethod {
BankTransfer,
Crypto,
BankDebit,
Reward,
}
#[derive(
@ -591,6 +595,7 @@ pub enum Connector {
Bitpay,
Bluesnap,
Braintree,
Cashtocode,
Checkout,
Coinbase,
Cybersource,
@ -688,6 +693,7 @@ pub enum RoutableConnectors {
Bambora,
Bluesnap,
Braintree,
Cashtocode,
Checkout,
Coinbase,
Cybersource,

View File

@ -589,6 +589,7 @@ pub enum PaymentMethodData {
BankTransfer(Box<BankTransferData>),
Crypto(CryptoData),
MandatePayment,
Reward(RewardData),
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@ -607,6 +608,7 @@ pub enum AdditionalPaymentData {
Crypto {},
BankDebit {},
MandatePayment {},
Reward {},
}
impl From<&PaymentMethodData> for AdditionalPaymentData {
@ -634,6 +636,7 @@ impl From<&PaymentMethodData> for AdditionalPaymentData {
PaymentMethodData::Crypto(_) => Self::Crypto {},
PaymentMethodData::BankDebit(_) => Self::BankDebit {},
PaymentMethodData::MandatePayment => Self::MandatePayment {},
PaymentMethodData::Reward(_) => Self::Reward {},
}
}
}
@ -940,6 +943,13 @@ pub struct CardResponse {
exp_year: String,
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub struct RewardData {
/// The merchant ID with which we have to call the connector
pub merchant_id: String,
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PaymentMethodDataResponse {
@ -953,6 +963,7 @@ pub enum PaymentMethodDataResponse {
Crypto(CryptoData),
BankDebit(BankDebitData),
MandatePayment,
Reward(RewardData),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)]
@ -1582,6 +1593,7 @@ impl From<PaymentMethodData> for PaymentMethodDataResponse {
PaymentMethodData::Crypto(crpto_data) => Self::Crypto(crpto_data),
PaymentMethodData::BankDebit(bank_debit_data) => Self::BankDebit(bank_debit_data),
PaymentMethodData::MandatePayment => Self::MandatePayment,
PaymentMethodData::Reward(reward_data) => Self::Reward(reward_data),
}
}
}

View File

@ -385,6 +385,7 @@ pub struct Connectors {
pub bitpay: ConnectorParams,
pub bluesnap: ConnectorParams,
pub braintree: ConnectorParams,
pub cashtocode: ConnectorParams,
pub checkout: ConnectorParams,
pub coinbase: ConnectorParams,
pub cybersource: ConnectorParams,

View File

@ -6,6 +6,7 @@ pub mod bambora;
pub mod bitpay;
pub mod bluesnap;
pub mod braintree;
pub mod cashtocode;
pub mod checkout;
pub mod coinbase;
pub mod cybersource;
@ -40,10 +41,11 @@ pub mod zen;
pub use self::dummyconnector::DummyConnector;
pub use self::{
aci::Aci, adyen::Adyen, airwallex::Airwallex, authorizedotnet::Authorizedotnet,
bambora::Bambora, bitpay::Bitpay, bluesnap::Bluesnap, braintree::Braintree, 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,
opennode::Opennode, payeezy::Payeezy, paypal::Paypal, payu::Payu, rapyd::Rapyd, shift4::Shift4,
stripe::Stripe, trustpay::Trustpay, worldline::Worldline, worldpay::Worldpay, zen::Zen,
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, opennode::Opennode, payeezy::Payeezy, paypal::Paypal, payu::Payu,
rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, worldline::Worldline,
worldpay::Worldpay, zen::Zen,
};

View File

@ -298,6 +298,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest {
api::PaymentMethodData::Crypto(_)
| api::PaymentMethodData::BankDebit(_)
| api::PaymentMethodData::BankTransfer(_)
| api::PaymentMethodData::Reward(_)
| api::PaymentMethodData::MandatePayment => {
Err(errors::ConnectorError::NotSupported {
message: format!("{:?}", item.payment_method),

View File

@ -139,14 +139,12 @@ fn get_pm_and_subsequent_auth_detail(
api::PaymentMethodData::Crypto(_)
| api::PaymentMethodData::BankDebit(_)
| api::PaymentMethodData::MandatePayment
| api::PaymentMethodData::BankTransfer(_) => {
Err(errors::ConnectorError::NotSupported {
| api::PaymentMethodData::BankTransfer(_)
| api::PaymentMethodData::Reward(_) => Err(errors::ConnectorError::NotSupported {
message: format!("{:?}", item.request.payment_method_data),
connector: "AuthorizeDotNet",
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl
.to_string(),
})?
}
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl.to_string(),
})?,
},
}
}

View File

@ -0,0 +1,461 @@
mod transformers;
use std::fmt::Debug;
use error_stack::{IntoReport, ResultExt};
use transformers as cashtocode;
use crate::{
configs::settings,
connector::utils as conn_utils,
core::errors::{self, CustomResult},
db::StorageInterface,
headers,
services::{
self,
request::{self, Mask},
ConnectorIntegration,
},
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
storage::{self},
ErrorResponse, Response,
},
utils::{self, ByteSliceExt, BytesExt},
};
#[derive(Debug, Clone)]
pub struct Cashtocode;
impl api::Payment for Cashtocode {}
impl api::PaymentSession for Cashtocode {}
impl api::ConnectorAccessToken for Cashtocode {}
impl api::PreVerify for Cashtocode {}
impl api::PaymentAuthorize for Cashtocode {}
impl api::PaymentSync for Cashtocode {}
impl api::PaymentCapture for Cashtocode {}
impl api::PaymentVoid for Cashtocode {}
impl api::PaymentToken for Cashtocode {}
impl api::Refund for Cashtocode {}
impl api::RefundExecute for Cashtocode {}
impl api::RefundSync for Cashtocode {}
fn get_auth_cashtocode(
payment_method_type: &Option<storage::enums::PaymentMethodType>,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
match payment_method_type
.clone()
.ok_or_else(conn_utils::missing_field_err("payment_method_type"))
{
Ok(reward_type) => match reward_type {
storage::enums::PaymentMethodType::ClassicReward => match auth_type {
types::ConnectorAuthType::BodyKey { api_key, .. } => Ok(vec![(
headers::AUTHORIZATION.to_string(),
format!("Basic {api_key}").into_masked(),
)]),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
},
storage::enums::PaymentMethodType::Evoucher => match auth_type {
types::ConnectorAuthType::BodyKey { key1, .. } => Ok(vec![(
headers::AUTHORIZATION.to_string(),
format!("Basic {key1}").into_masked(),
)]),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
},
_ => Err(error_stack::report!(errors::ConnectorError::NotSupported {
message: reward_type.to_string(),
connector: "cashtocode",
payment_experience: "Try with a different payment method".to_string(),
})),
},
Err(_) => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
}
}
impl
ConnectorIntegration<
api::PaymentMethodToken,
types::PaymentMethodTokenizationData,
types::PaymentsResponseData,
> for Cashtocode
{
// Not Implemented (R)
}
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Cashtocode where
Self: ConnectorIntegration<Flow, Request, Response>
{
}
impl ConnectorCommon for Cashtocode {
fn id(&self) -> &'static str {
"cashtocode"
}
fn common_get_content_type(&self) -> &'static str {
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.cashtocode.base_url.as_ref()
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth = cashtocode::CashtocodeAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(
headers::AUTHORIZATION.to_string(),
auth.api_key.into_masked(),
)])
}
fn build_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: cashtocode::CashtocodeErrorResponse = res
.response
.parse_struct("CashtocodeErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
status_code: res.status_code,
code: response.error.to_string(),
message: response.error_description,
reason: None,
})
}
}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Cashtocode
{
//TODO: implement sessions flow
}
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
for Cashtocode
{
}
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
for Cashtocode
{
}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Cashtocode
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let mut header = vec![(
headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self)
.to_owned()
.into(),
)];
let auth_differentiator =
get_auth_cashtocode(&req.request.payment_method_type, &req.connector_auth_type);
let mut api_key = match auth_differentiator {
Ok(auth_type) => auth_type,
Err(err) => return Err(err),
};
header.append(&mut api_key);
Ok(header)
}
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!(
"{}/merchant/paytokens",
connectors.cashtocode.base_url
))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let req_obj = cashtocode::CashtocodePaymentsRequest::try_from(req)?;
let cashtocode_req = types::RequestBody::log_and_get_request_body(
&req_obj,
utils::Encode::<cashtocode::CashtocodePaymentsRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(cashtocode_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: cashtocode::CashtocodePaymentsResponse = res
.response
.parse_struct("Cashtocode 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 Cashtocode
{
fn build_request(
&self,
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Err(errors::ConnectorError::FlowNotSupported {
flow: "Payments Sync".to_string(),
connector: "Cashtocode".to_string(),
}
.into())
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
types::RouterData::try_from(types::ResponseRouterData {
response: cashtocode::CashtocodePaymentsSyncResponse {},
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 Cashtocode
{
fn build_request(
&self,
_req: &types::RouterData<
api::Capture,
types::PaymentsCaptureData,
types::PaymentsResponseData,
>,
_connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Err(errors::ConnectorError::FlowNotSupported {
flow: "Capture".to_string(),
connector: "Cashtocode".to_string(),
}
.into())
}
}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Cashtocode
{
fn build_request(
&self,
_req: &types::RouterData<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>,
_connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Err(errors::ConnectorError::FlowNotSupported {
flow: "Payments Cancel".to_string(),
connector: "Cashtocode".to_string(),
}
.into())
}
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Cashtocode {
fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let base64_signature = conn_utils::get_header_key_value("authorization", request.headers)?;
let signature = base64_signature.as_bytes().to_owned();
Ok(signature)
}
async fn get_webhook_source_verification_merchant_secret(
&self,
db: &dyn StorageInterface,
merchant_id: &str,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let key = conn_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())
}
async fn verify_webhook_source(
&self,
db: &dyn StorageInterface,
request: &api::IncomingWebhookRequestDetails<'_>,
merchant_id: &str,
) -> CustomResult<bool, errors::ConnectorError> {
let signature = self
.get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self
.get_webhook_source_verification_merchant_secret(db, merchant_id)
.await
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret_auth = String::from_utf8(secret.to_vec())
.into_report()
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Could not convert secret to UTF-8")?;
let signature_auth = String::from_utf8(signature.to_vec())
.into_report()
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Could not convert secret to UTF-8")?;
Ok(signature_auth == secret_auth)
}
fn get_webhook_object_reference_id(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
let webhook: transformers::CashtocodeObjectId = request
.body
.parse_struct("CashtocodeObjectId")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(webhook.transaction_id),
))
}
fn get_webhook_event_type(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Ok(api::IncomingWebhookEvent::PaymentIntentSuccess)
}
fn get_webhook_resource_object(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
let webhook: transformers::CashtocodeIncomingWebhook = request
.body
.parse_struct("CashtocodeIncomingWebhook")
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
let res_json =
utils::Encode::<transformers::CashtocodeIncomingWebhook>::encode_to_value(&webhook)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
Ok(res_json)
}
fn get_webhook_api_response(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<services::api::ApplicationResponse<serde_json::Value>, errors::ConnectorError>
{
let status = "EXECUTED".to_string();
let obj: transformers::CashtocodeObjectId = request
.body
.parse_struct("CashtocodeObjectId")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
let response: serde_json::Value =
serde_json::json!({ "status": status, "transactionId" : obj.transaction_id});
Ok(services::api::ApplicationResponse::Json(response))
}
}
impl ConnectorIntegration<api::refunds::Execute, types::RefundsData, types::RefundsResponseData>
for Cashtocode
{
fn build_request(
&self,
_req: &types::RouterData<
api::refunds::Execute,
types::RefundsData,
types::RefundsResponseData,
>,
_connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Err(errors::ConnectorError::FlowNotSupported {
flow: "Refunds".to_string(),
connector: "Cashtocode".to_string(),
}
.into())
}
}
impl ConnectorIntegration<api::refunds::RSync, types::RefundsData, types::RefundsResponseData>
for Cashtocode
{
fn build_request(
&self,
_req: &types::RouterData<
api::refunds::RSync,
types::RefundsData,
types::RefundsResponseData,
>,
_connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Err(errors::ConnectorError::FlowNotSupported {
flow: "Refund Sync".to_string(),
connector: "Cashtocode".to_string(),
}
.into())
}
}

View File

@ -0,0 +1,226 @@
use common_utils::pii::Email;
use masking::Secret;
use serde::{Deserialize, Serialize};
use crate::{
connector::utils::RouterData,
core::errors,
services,
types::{self, api, storage::enums},
};
#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CashtocodePaymentsRequest {
amount: i64,
transaction_id: String,
user_id: Secret<String>,
currency: enums::Currency,
first_name: Option<Secret<String>>,
last_name: Option<Secret<String>>,
user_alias: Secret<String>,
requested_url: String,
cancel_url: String,
email: Option<Email>,
mid: String,
}
pub struct CashToCodeMandatoryParams {
pub user_id: Secret<String>,
pub user_alias: Secret<String>,
pub requested_url: String,
pub cancel_url: String,
}
fn get_mid(
payment_method_data: &api::payments::PaymentMethodData,
) -> Result<String, errors::ConnectorError> {
match payment_method_data {
api_models::payments::PaymentMethodData::Reward(reward_data) => {
Ok(reward_data.merchant_id.to_string())
}
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
)),
}
}
fn get_mandatory_params(
item: &types::PaymentsAuthorizeRouterData,
) -> Result<CashToCodeMandatoryParams, error_stack::Report<errors::ConnectorError>> {
let customer_id = item.get_customer_id()?;
let url = item.get_return_url()?;
Ok(CashToCodeMandatoryParams {
user_id: Secret::new(customer_id.to_owned()),
user_alias: Secret::new(customer_id),
requested_url: url.to_owned(),
cancel_url: url,
})
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for CashtocodePaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
let params: CashToCodeMandatoryParams = get_mandatory_params(item)?;
let mid = get_mid(&item.request.payment_method_data)?;
match item.payment_method {
storage_models::enums::PaymentMethod::Reward => Ok(Self {
amount: item.request.amount,
transaction_id: item.attempt_id.clone(),
currency: item.request.currency,
user_id: params.user_id,
first_name: None,
last_name: None,
user_alias: params.user_alias,
requested_url: params.requested_url,
cancel_url: params.cancel_url,
email: item.request.email.clone(),
mid,
}),
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
}
}
}
pub struct CashtocodeAuthType {
pub(super) api_key: String,
}
impl TryFrom<&types::ConnectorAuthType> for CashtocodeAuthType {
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()),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CashtocodePaymentStatus {
Succeeded,
#[default]
Processing,
}
impl From<CashtocodePaymentStatus> for enums::AttemptStatus {
fn from(item: CashtocodePaymentStatus) -> Self {
match item {
CashtocodePaymentStatus::Succeeded => Self::Charged,
CashtocodePaymentStatus::Processing => Self::AuthenticationPending,
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct CashtocodeErrors {
pub message: String,
pub path: String,
#[serde(rename = "type")]
pub event_type: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CashtocodePaymentsResponse {
pub pay_url: String,
}
pub struct CashtocodePaymentsSyncResponse {}
impl<F, T>
TryFrom<
types::ResponseRouterData<F, CashtocodePaymentsResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
CashtocodePaymentsResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let redirection_data = services::RedirectForm::Form {
endpoint: item.response.pay_url.clone(),
method: services::Method::Post,
form_fields: Default::default(),
};
Ok(Self {
status: enums::AttemptStatus::AuthenticationPending,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
item.data.attempt_id.clone(),
),
redirection_data: Some(redirection_data),
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
}),
..item.data
})
}
}
impl<F, T>
TryFrom<
types::ResponseRouterData<
F,
CashtocodePaymentsSyncResponse,
T,
types::PaymentsResponseData,
>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
CashtocodePaymentsSyncResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: enums::AttemptStatus::Charged,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
item.data.attempt_id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
}),
..item.data
})
}
}
#[derive(Debug, Deserialize)]
pub struct CashtocodeErrorResponse {
pub error: String,
pub error_description: String,
pub errors: Option<Vec<CashtocodeErrors>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CashtocodeIncomingWebhook {
pub amount: i64,
pub currency: String,
pub foreign_transaction_id: String,
#[serde(rename = "type")]
pub event_type: String,
pub transaction_id: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CashtocodeObjectId {
pub transaction_id: String,
}

View File

@ -2566,14 +2566,13 @@ impl
)),
}
}
api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Crypto(_) => {
Err(errors::ConnectorError::NotSupported {
api::PaymentMethodData::MandatePayment
| api::PaymentMethodData::Crypto(_)
| api::PaymentMethodData::Reward(_) => Err(errors::ConnectorError::NotSupported {
message: format!("{pm_type:?}"),
connector: "Stripe",
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl
.to_string(),
})?
}
payment_experience: api_models::enums::PaymentExperience::RedirectToUrl.to_string(),
})?,
}
}
}

View File

@ -142,6 +142,7 @@ default_imp_for_complete_authorize!(
connector::Authorizedotnet,
connector::Bitpay,
connector::Braintree,
connector::Cashtocode,
connector::Checkout,
connector::Coinbase,
connector::Cybersource,
@ -200,6 +201,7 @@ default_imp_for_create_customer!(
connector::Bambora,
connector::Bitpay,
connector::Braintree,
connector::Cashtocode,
connector::Checkout,
connector::Coinbase,
connector::Cybersource,
@ -262,6 +264,7 @@ default_imp_for_connector_redirect_response!(
connector::Authorizedotnet,
connector::Bitpay,
connector::Braintree,
connector::Cashtocode,
connector::Coinbase,
connector::Cybersource,
connector::Dlocal,
@ -301,6 +304,7 @@ default_imp_for_connector_request_id!(
connector::Bitpay,
connector::Bluesnap,
connector::Braintree,
connector::Cashtocode,
connector::Checkout,
connector::Coinbase,
connector::Cybersource,
@ -366,6 +370,7 @@ default_imp_for_accept_dispute!(
connector::Bitpay,
connector::Bluesnap,
connector::Braintree,
connector::Cashtocode,
connector::Coinbase,
connector::Cybersource,
connector::Dlocal,
@ -451,6 +456,7 @@ default_imp_for_file_upload!(
connector::Bitpay,
connector::Bluesnap,
connector::Braintree,
connector::Cashtocode,
connector::Coinbase,
connector::Cybersource,
connector::Dlocal,
@ -513,6 +519,7 @@ default_imp_for_submit_evidence!(
connector::Bitpay,
connector::Bluesnap,
connector::Braintree,
connector::Cashtocode,
connector::Cybersource,
connector::Coinbase,
connector::Dlocal,
@ -575,6 +582,7 @@ default_imp_for_defend_dispute!(
connector::Bitpay,
connector::Bluesnap,
connector::Braintree,
connector::Cashtocode,
connector::Cybersource,
connector::Coinbase,
connector::Dlocal,
@ -638,6 +646,7 @@ default_imp_for_pre_processing_steps!(
connector::Bitpay,
connector::Bluesnap,
connector::Braintree,
connector::Cashtocode,
connector::Checkout,
connector::Coinbase,
connector::Cybersource,

View File

@ -1271,6 +1271,7 @@ pub async fn make_pm_data<'a, F: Clone, R>(
(pm @ Some(api::PaymentMethodData::BankRedirect(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::Crypto(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::BankDebit(_)), _) => Ok(pm.to_owned()),
(pm @ Some(api::PaymentMethodData::Reward(_)), _) => Ok(pm.to_owned()),
(pm_opt @ Some(pm @ api::PaymentMethodData::BankTransfer(_)), _) => {
let token = vault::Vault::store_payment_method_data_in_locker(
state,

View File

@ -40,7 +40,6 @@ pub async fn payments_incoming_webhook_flow<W: api::OutgoingWebhookType>(
} else {
payments::CallConnectorAction::Trigger
};
let payments_response = match webhook_details.object_reference_id {
api_models::webhooks::ObjectReferenceId::PaymentId(id) => {
payments::payments_core::<api::PSync, api::PaymentsResponse, _, _, _>(

View File

@ -169,6 +169,7 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payments::WeChatPayRedirection,
api_models::payments::BankDebitBilling,
api_models::payments::CryptoData,
api_models::payments::RewardData,
api_models::payments::Address,
api_models::payments::BankRedirectData,
api_models::payments::BankRedirectBilling,

View File

@ -410,8 +410,9 @@ impl Webhooks {
.route(
web::post().to(receive_incoming_webhook::<webhook_type::OutgoingWebhook>),
)
.route(web::get().to(receive_incoming_webhook::<webhook_type::OutgoingWebhook>))
.route(
web::get().to(receive_incoming_webhook::<webhook_type::OutgoingWebhook>),
web::put().to(receive_incoming_webhook::<webhook_type::OutgoingWebhook>),
),
)
}

View File

@ -209,6 +209,7 @@ impl ConnectorData {
enums::Connector::Bitpay => Ok(Box::new(&connector::Bitpay)),
enums::Connector::Bluesnap => Ok(Box::new(&connector::Bluesnap)),
enums::Connector::Braintree => Ok(Box::new(&connector::Braintree)),
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::Cybersource => Ok(Box::new(&connector::Cybersource)),

View File

@ -0,0 +1,127 @@
use api_models::payments::{Address, AddressDetails};
use router::types::{self, storage::enums};
use crate::{
connector_auth,
utils::{self, ConnectorActions},
};
#[derive(Clone, Copy)]
struct CashtocodeTest;
impl ConnectorActions for CashtocodeTest {}
impl utils::Connector for CashtocodeTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Cashtocode;
types::api::ConnectorData {
connector: Box::new(&Cashtocode),
connector_name: types::Connector::Cashtocode,
get_token: types::api::GetToken::Connector,
}
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new()
.cashtocode
.expect("Missing connector authentication configuration"),
)
}
fn get_name(&self) -> String {
"cashtocode".to_string()
}
}
static CONNECTOR: CashtocodeTest = CashtocodeTest {};
impl CashtocodeTest {
fn get_payment_authorize_data(
payment_method_type: Option<enums::PaymentMethodType>,
payment_method_data: types::api::PaymentMethodData,
) -> Option<types::PaymentsAuthorizeData> {
Some(types::PaymentsAuthorizeData {
amount: 3500,
currency: enums::Currency::USD,
payment_method_data,
confirm: true,
statement_descriptor_suffix: None,
statement_descriptor: None,
setup_future_usage: None,
mandate_id: None,
off_session: None,
setup_mandate_details: None,
capture_method: None,
browser_info: None,
order_details: None,
order_category: None,
email: None,
payment_experience: None,
payment_method_type,
session_token: None,
enrolled_for_3ds: false,
related_transaction_id: None,
router_return_url: Some(String::from("http://localhost:8080")),
webhook_url: None,
complete_authorize_url: None,
customer_id: Some("John Doe".to_owned()),
})
}
fn get_payment_info() -> Option<utils::PaymentInfo> {
Some(utils::PaymentInfo {
address: Some(types::PaymentAddress {
billing: Some(Address {
address: Some(AddressDetails {
country: Some(api_models::enums::CountryAlpha2::US),
..Default::default()
}),
phone: None,
}),
..Default::default()
}),
return_url: Some("https://google.com".to_owned()),
..Default::default()
})
}
}
//fetch payurl for payment's create
#[actix_web::test]
async fn should_fetch_pay_url_classic() {
let authorize_response = CONNECTOR
.make_payment(
CashtocodeTest::get_payment_authorize_data(
Some(enums::PaymentMethodType::ClassicReward),
api_models::payments::PaymentMethodData::Reward(api_models::payments::RewardData {
merchant_id: "1bc20b0a".to_owned(),
}),
),
CashtocodeTest::get_payment_info(),
)
.await
.unwrap();
assert_eq!(
authorize_response.status,
enums::AttemptStatus::AuthenticationPending
);
}
#[actix_web::test]
async fn should_fetch_pay_url_evoucher() {
let authorize_response = CONNECTOR
.make_payment(
CashtocodeTest::get_payment_authorize_data(
Some(enums::PaymentMethodType::Evoucher),
api_models::payments::PaymentMethodData::Reward(api_models::payments::RewardData {
merchant_id: "befb46ee".to_owned(),
}),
),
CashtocodeTest::get_payment_info(),
)
.await
.unwrap();
assert_eq!(
authorize_response.status,
enums::AttemptStatus::AuthenticationPending
);
}

View File

@ -13,6 +13,7 @@ pub struct ConnectorAuthentication {
pub bambora: Option<BodyKey>,
pub bitpay: Option<HeaderKey>,
pub bluesnap: Option<BodyKey>,
pub cashtocode: Option<BodyKey>,
pub checkout: Option<SignatureKey>,
pub coinbase: Option<HeaderKey>,
pub cybersource: Option<SignatureKey>,
@ -48,7 +49,6 @@ pub struct ConnectorAuthentication {
impl ConnectorAuthentication {
#[allow(clippy::expect_used)]
pub(crate) fn new() -> Self {
// Do `export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml"`
// before running tests
let path = env::var("CONNECTOR_AUTH_FILE_PATH")
.expect("connector authentication file path not set");

View File

@ -13,6 +13,7 @@ mod authorizedotnet;
mod bambora;
mod bitpay;
mod bluesnap;
mod cashtocode;
mod checkout;
mod coinbase;
mod connector_auth;

View File

@ -126,3 +126,7 @@ pypl_email=""
pypl_pass=""
gmail_email=""
gmail_pass=""
[cashtocode]
api_key="Classic PMT API Key"
key1 = "Evoucher PMT API Key"

View File

@ -464,6 +464,7 @@ pub enum PaymentMethod {
BankTransfer,
Crypto,
BankDebit,
Reward,
}
#[derive(
@ -679,10 +680,13 @@ pub enum PaymentMethodType {
BancontactCard,
Becs,
Blik,
#[serde(rename = "classic")]
ClassicReward,
Credit,
CryptoCurrency,
Debit,
Eps,
Evoucher,
Giropay,
GooglePay,
Ideal,

View File

@ -66,6 +66,7 @@ bambora.base_url = "https://api.na.bambora.com"
bitpay.base_url = "https://test.bitpay.com"
bluesnap.base_url = "https://sandbox.bluesnap.com/"
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"
cybersource.base_url = "https://apitest.cybersource.com/"
@ -98,6 +99,7 @@ zen.base_url = "https://api.zen-test.com/"
[connectors.supported]
wallets = ["klarna", "braintree", "applepay"]
rewards = ["cashtocode",]
cards = [
"aci",
"adyen",

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 checkout coinbase cybersource dlocal dummyconnector fiserv forte globalpay iatapay klarna mollie multisafepay nexinets noon nuvei opennode payeezy paypal payu rapyd shift4 stripe trustpay worldline worldpay "$1")
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 opennode payeezy 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