diff --git a/config/config.example.toml b/config/config.example.toml index b8d5ad0a07..ae626090d3 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -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", diff --git a/config/development.toml b/config/development.toml index 510b1e0b3a..532af8931b 100644 --- a/config/development.toml +++ b/config/development.toml @@ -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/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 21321f5cb5..3e7b9d98a4 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -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", diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 79acaf983c..5af51bc6ec 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -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, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 863ed792fc..fac1949143 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -589,6 +589,7 @@ pub enum PaymentMethodData { BankTransfer(Box), 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 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), } } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index b2bc1a869e..f22d5e4be5 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -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, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 8c8c744f2d..fa6001e713 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -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, }; diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 93dacbfdb1..69fc8f47a0 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -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), diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 2be8a648e4..d0b46998b3 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -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 { - message: format!("{:?}", item.request.payment_method_data), - connector: "AuthorizeDotNet", - payment_experience: api_models::enums::PaymentExperience::RedirectToUrl - .to_string(), - })? - } + | 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(), + })?, }, } } diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs new file mode 100644 index 0000000000..df9f6bb2c9 --- /dev/null +++ b/crates/router/src/connector/cashtocode.rs @@ -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, + auth_type: &types::ConnectorAuthType, +) -> CustomResult)>, 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 ConnectorCommonExt for Cashtocode where + Self: ConnectorIntegration +{ +} + +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)>, 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 { + 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 + for Cashtocode +{ + //TODO: implement sessions flow +} + +impl ConnectorIntegration + for Cashtocode +{ +} + +impl ConnectorIntegration + for Cashtocode +{ +} + +impl ConnectorIntegration + for Cashtocode +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, 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 { + Ok(format!( + "{}/merchant/paytokens", + connectors.cashtocode.base_url + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = cashtocode::CashtocodePaymentsRequest::try_from(req)?; + let cashtocode_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::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, 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 { + 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 { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Cashtocode +{ + fn build_request( + &self, + _req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, 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::RouterData::try_from(types::ResponseRouterData { + response: cashtocode::CashtocodePaymentsSyncResponse {}, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Cashtocode +{ + fn build_request( + &self, + _req: &types::RouterData< + api::Capture, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::FlowNotSupported { + flow: "Capture".to_string(), + connector: "Cashtocode".to_string(), + } + .into()) + } +} + +impl ConnectorIntegration + for Cashtocode +{ + fn build_request( + &self, + _req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, 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, 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, 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 { + 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 { + 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 { + Ok(api::IncomingWebhookEvent::PaymentIntentSuccess) + } + + fn get_webhook_resource_object( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let webhook: transformers::CashtocodeIncomingWebhook = request + .body + .parse_struct("CashtocodeIncomingWebhook") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + let res_json = + utils::Encode::::encode_to_value(&webhook) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + Ok(res_json) + } + + fn get_webhook_api_response( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, 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 + for Cashtocode +{ + fn build_request( + &self, + _req: &types::RouterData< + api::refunds::Execute, + types::RefundsData, + types::RefundsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::FlowNotSupported { + flow: "Refunds".to_string(), + connector: "Cashtocode".to_string(), + } + .into()) + } +} + +impl ConnectorIntegration + for Cashtocode +{ + fn build_request( + &self, + _req: &types::RouterData< + api::refunds::RSync, + types::RefundsData, + types::RefundsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::FlowNotSupported { + flow: "Refund Sync".to_string(), + connector: "Cashtocode".to_string(), + } + .into()) + } +} diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs new file mode 100644 index 0000000000..a0b5cb9c46 --- /dev/null +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -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, + currency: enums::Currency, + first_name: Option>, + last_name: Option>, + user_alias: Secret, + requested_url: String, + cancel_url: String, + email: Option, + mid: String, +} + +pub struct CashToCodeMandatoryParams { + pub user_id: Secret, + pub user_alias: Secret, + pub requested_url: String, + pub cancel_url: String, +} + +fn get_mid( + payment_method_data: &api::payments::PaymentMethodData, +) -> Result { + 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> { + 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; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + 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; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + 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 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 + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CashtocodePaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + 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 + TryFrom< + types::ResponseRouterData< + F, + CashtocodePaymentsSyncResponse, + T, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CashtocodePaymentsSyncResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + 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>, +} + +#[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, +} diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 7e337c3b33..d313767d02 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -2566,14 +2566,13 @@ impl )), } } - api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Crypto(_) => { - Err(errors::ConnectorError::NotSupported { - message: format!("{pm_type:?}"), - connector: "Stripe", - payment_experience: api_models::enums::PaymentExperience::RedirectToUrl - .to_string(), - })? - } + 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(), + })?, } } } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index c1f5af13b6..f01513f99b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -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, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 8e7240bcc8..1a52a681cd 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -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, diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 33aef92659..dcec5776ca 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -40,7 +40,6 @@ pub async fn payments_incoming_webhook_flow( } else { payments::CallConnectorAction::Trigger }; - let payments_response = match webhook_details.object_reference_id { api_models::webhooks::ObjectReferenceId::PaymentId(id) => { payments::payments_core::( diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index f43170cb8d..f4170be30d 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -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, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f153850ce8..ae78a01a50 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -410,8 +410,9 @@ impl Webhooks { .route( web::post().to(receive_incoming_webhook::), ) + .route(web::get().to(receive_incoming_webhook::)) .route( - web::get().to(receive_incoming_webhook::), + web::put().to(receive_incoming_webhook::), ), ) } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 670ef0e42c..5afe9cd368 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -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)), diff --git a/crates/router/tests/connectors/cashtocode.rs b/crates/router/tests/connectors/cashtocode.rs new file mode 100644 index 0000000000..e07e54c54b --- /dev/null +++ b/crates/router/tests/connectors/cashtocode.rs @@ -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, + payment_method_data: types::api::PaymentMethodData, + ) -> Option { + 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 { + 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 + ); +} diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 52cd3562cd..e0299ce3a5 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -13,6 +13,7 @@ pub struct ConnectorAuthentication { pub bambora: Option, pub bitpay: Option, pub bluesnap: Option, + pub cashtocode: Option, pub checkout: Option, pub coinbase: Option, pub cybersource: Option, @@ -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"); diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index dd6342113c..62b39494af 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -13,6 +13,7 @@ mod authorizedotnet; mod bambora; mod bitpay; mod bluesnap; +mod cashtocode; mod checkout; mod coinbase; mod connector_auth; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 06120aa8ee..18d1e22570 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -126,3 +126,7 @@ pypl_email="" pypl_pass="" gmail_email="" gmail_pass="" + +[cashtocode] +api_key="Classic PMT API Key" +key1 = "Evoucher PMT API Key" \ No newline at end of file diff --git a/crates/storage_models/src/enums.rs b/crates/storage_models/src/enums.rs index 465b3a17fa..9e0531a187 100644 --- a/crates/storage_models/src/enums.rs +++ b/crates/storage_models/src/enums.rs @@ -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, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 68c29c1f16..6e87c6c307 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -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", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index d0b4f08045..de14e41049 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -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