diff --git a/config/config.example.toml b/config/config.example.toml index ee868beb092b..c1ba88db0195 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -171,6 +171,7 @@ authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" billwerk.base_url = "https://api.reepay.com/" +billwerk.secondary_base_url = "https://card.reepay.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -349,6 +350,7 @@ stax = { long_lived_token = true, payment_method = "card,bank_debit" } square = { long_lived_token = false, payment_method = "card" } braintree = { long_lived_token = false, payment_method = "card" } gocardless = { long_lived_token = true, payment_method = "bank_debit" } +billwerk = {long_lived_token = false, payment_method = "card"} [temp_locker_enable_config] stripe = { payment_method = "bank_transfer" } diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 10776c766789..77040f79a4a1 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -21,6 +21,7 @@ authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" billwerk.base_url = "https://api.reepay.com/" +billwerk.secondary_base_url = "https://card.reepay.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -292,6 +293,7 @@ payme = { long_lived_token = false, payment_method = "card" } square = { long_lived_token = false, payment_method = "card" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } +billwerk = {long_lived_token = false, payment_method = "card"} [webhooks] outgoing_enabled = true diff --git a/config/deployments/production.toml b/config/deployments/production.toml index a6405e4a3b2d..23df14862b9b 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -25,6 +25,7 @@ authorizedotnet.base_url = "https://api.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" bankofamerica.base_url = "https://api.merchant-services.bankofamerica.com/" billwerk.base_url = "https://api.reepay.com/" +billwerk.secondary_base_url = "https://card.reepay.com/" bitpay.base_url = "https://bitpay.com" bluesnap.base_url = "https://ws.bluesnap.com/" bluesnap.secondary_base_url = "https://pay.bluesnap.com/" @@ -306,6 +307,7 @@ payme = { long_lived_token = false, payment_method = "card" } square = { long_lived_token = false, payment_method = "card" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } +billwerk = {long_lived_token = false, payment_method = "card"} [webhooks] outgoing_enabled = true diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 035c90af3789..8ae12dc54a62 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -25,6 +25,7 @@ authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" billwerk.base_url = "https://api.reepay.com/" +billwerk.secondary_base_url = "https://card.reepay.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -308,6 +309,7 @@ payme = { long_lived_token = false, payment_method = "card" } square = { long_lived_token = false, payment_method = "card" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } +billwerk = {long_lived_token = false, payment_method = "card"} [webhooks] outgoing_enabled = true diff --git a/config/development.toml b/config/development.toml index 5c063acf2f71..8ba5c076071a 100644 --- a/config/development.toml +++ b/config/development.toml @@ -167,6 +167,7 @@ authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" billwerk.base_url = "https://api.reepay.com/" +billwerk.secondary_base_url = "https://card.reepay.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -450,6 +451,7 @@ square = { long_lived_token = false, payment_method = "card" } braintree = { long_lived_token = false, payment_method = "card" } payme = { long_lived_token = false, payment_method = "card" } gocardless = { long_lived_token = true, payment_method = "bank_debit" } +billwerk = {long_lived_token = false, payment_method = "card"} [temp_locker_enable_config] stripe = { payment_method = "bank_transfer" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 0e034f2e1457..eb32b47e3b41 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -105,6 +105,7 @@ authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" billwerk.base_url = "https://api.reepay.com/" +billwerk.secondary_base_url = "https://card.reepay.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -251,6 +252,7 @@ stax = { long_lived_token = true, payment_method = "card,bank_debit" } square = { long_lived_token = false, payment_method = "card" } braintree = { long_lived_token = false, payment_method = "card" } gocardless = { long_lived_token = true, payment_method = "bank_debit" } +billwerk = {long_lived_token = false, payment_method = "card"} [temp_locker_enable_config] stripe = { payment_method = "bank_transfer" } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index b29377886d5e..354dec29237b 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -78,7 +78,7 @@ pub enum Connector { Authorizedotnet, Bambora, Bankofamerica, - // Billwerk, Added as template code for future usage + Billwerk, Bitpay, Bluesnap, Boku, @@ -166,7 +166,7 @@ impl Connector { | Self::Authorizedotnet | Self::Bambora | Self::Bankofamerica - // | Self::Billwerk Added as template code for future usage + | Self::Billwerk | Self::Bitpay | Self::Bluesnap | Self::Boku @@ -223,7 +223,7 @@ impl Connector { | Self::Authorizedotnet | Self::Bambora | Self::Bankofamerica - // | Self::Billwerk Added as template for future usage + | Self::Billwerk | Self::Bitpay | Self::Bluesnap | Self::Boku diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 548e721f0d39..497f1f58729d 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -115,7 +115,7 @@ pub enum RoutableConnectors { Airwallex, Authorizedotnet, Bankofamerica, - // Billwerk, Added as template code for future usage + Billwerk, Bitpay, Bambora, Bluesnap, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 74317319f500..5a67847a9e51 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -228,7 +228,7 @@ impl ConnectorConfig { Connector::Airwallex => Ok(connector_data.airwallex), Connector::Authorizedotnet => Ok(connector_data.authorizedotnet), Connector::Bankofamerica => Ok(connector_data.bankofamerica), - // Connector::Billwerk => Ok(connector_data.billwerk), Added as template code for future usage + Connector::Billwerk => Ok(connector_data.billwerk), Connector::Bitpay => Ok(connector_data.bitpay), Connector::Bluesnap => Ok(connector_data.bluesnap), Connector::Boku => Ok(connector_data.boku), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 0e18bb33ed29..0233b916668c 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -2587,4 +2587,45 @@ api_key="Api Key" [threedsecureio.metadata] mcc="MCC" merchant_country_code="3 digit numeric country code" -merchant_name="Name of the merchant" \ No newline at end of file +merchant_name="Name of the merchant" + +[billwerk] +[[billwerk.credit]] + payment_method_type = "Mastercard" +[[billwerk.credit]] + payment_method_type = "Visa" +[[billwerk.credit]] + payment_method_type = "Interac" +[[billwerk.credit]] + payment_method_type = "AmericanExpress" +[[billwerk.credit]] + payment_method_type = "JCB" +[[billwerk.credit]] + payment_method_type = "DinersClub" +[[billwerk.credit]] + payment_method_type = "Discover" +[[billwerk.credit]] + payment_method_type = "CartesBancaires" +[[billwerk.credit]] + payment_method_type = "UnionPay" +[[billwerk.debit]] + payment_method_type = "Mastercard" +[[billwerk.debit]] + payment_method_type = "Visa" +[[billwerk.debit]] + payment_method_type = "Interac" +[[billwerk.debit]] + payment_method_type = "AmericanExpress" +[[billwerk.debit]] + payment_method_type = "JCB" +[[billwerk.debit]] + payment_method_type = "DinersClub" +[[billwerk.debit]] + payment_method_type = "Discover" +[[billwerk.debit]] + payment_method_type = "CartesBancaires" +[[billwerk.debit]] + payment_method_type = "UnionPay" +[billwerk.connector_auth.BodyKey] +api_key="Private Api Key" +key1="Public Api Key" \ No newline at end of file diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 079ead7e4ced..58f9ccfa2c39 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1913,4 +1913,45 @@ terminal_uuid="Terminal UUID" pay_wall_secret="Pay Wall Secret" [zen.metadata.google_pay] terminal_uuid="Terminal UUID" -pay_wall_secret="Pay Wall Secret" \ No newline at end of file +pay_wall_secret="Pay Wall Secret" + +[billwerk] +[[billwerk.credit]] + payment_method_type = "Mastercard" +[[billwerk.credit]] + payment_method_type = "Visa" +[[billwerk.credit]] + payment_method_type = "Interac" +[[billwerk.credit]] + payment_method_type = "AmericanExpress" +[[billwerk.credit]] + payment_method_type = "JCB" +[[billwerk.credit]] + payment_method_type = "DinersClub" +[[billwerk.credit]] + payment_method_type = "Discover" +[[billwerk.credit]] + payment_method_type = "CartesBancaires" +[[billwerk.credit]] + payment_method_type = "UnionPay" +[[billwerk.debit]] + payment_method_type = "Mastercard" +[[billwerk.debit]] + payment_method_type = "Visa" +[[billwerk.debit]] + payment_method_type = "Interac" +[[billwerk.debit]] + payment_method_type = "AmericanExpress" +[[billwerk.debit]] + payment_method_type = "JCB" +[[billwerk.debit]] + payment_method_type = "DinersClub" +[[billwerk.debit]] + payment_method_type = "Discover" +[[billwerk.debit]] + payment_method_type = "CartesBancaires" +[[billwerk.debit]] + payment_method_type = "UnionPay" +[billwerk.connector_auth.BodyKey] +api_key="Private Api Key" +key1="Public Api Key" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index f484e9145318..9103c548e658 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -2589,4 +2589,45 @@ api_key="Api Key" [threedsecureio.metadata] mcc="MCC" merchant_country_code="3 digit numeric country code" -merchant_name="Name of the merchant" \ No newline at end of file +merchant_name="Name of the merchant" + +[billwerk] +[[billwerk.credit]] + payment_method_type = "Mastercard" +[[billwerk.credit]] + payment_method_type = "Visa" +[[billwerk.credit]] + payment_method_type = "Interac" +[[billwerk.credit]] + payment_method_type = "AmericanExpress" +[[billwerk.credit]] + payment_method_type = "JCB" +[[billwerk.credit]] + payment_method_type = "DinersClub" +[[billwerk.credit]] + payment_method_type = "Discover" +[[billwerk.credit]] + payment_method_type = "CartesBancaires" +[[billwerk.credit]] + payment_method_type = "UnionPay" +[[billwerk.debit]] + payment_method_type = "Mastercard" +[[billwerk.debit]] + payment_method_type = "Visa" +[[billwerk.debit]] + payment_method_type = "Interac" +[[billwerk.debit]] + payment_method_type = "AmericanExpress" +[[billwerk.debit]] + payment_method_type = "JCB" +[[billwerk.debit]] + payment_method_type = "DinersClub" +[[billwerk.debit]] + payment_method_type = "Discover" +[[billwerk.debit]] + payment_method_type = "CartesBancaires" +[[billwerk.debit]] + payment_method_type = "UnionPay" +[billwerk.connector_auth.BodyKey] +api_key="Private Api Key" +key1="Public Api Key" diff --git a/crates/router/src/connector/billwerk.rs b/crates/router/src/connector/billwerk.rs index 154b729abc9a..b6839f5de24d 100644 --- a/crates/router/src/connector/billwerk.rs +++ b/crates/router/src/connector/billwerk.rs @@ -2,12 +2,15 @@ pub mod transformers; use std::fmt::Debug; +use base64::Engine; use error_stack::{report, ResultExt}; -use masking::ExposeInterface; +use masking::PeekInterface; use transformers as billwerk; +use super::utils::RefundsRequestData; use crate::{ configs::settings, + consts, core::errors::{self, CustomResult}, events::connector_api_logs::ConnectorEvent, headers, @@ -40,16 +43,6 @@ impl api::RefundExecute for Billwerk {} impl api::RefundSync for Billwerk {} impl api::PaymentToken for Billwerk {} -impl - ConnectorIntegration< - api::PaymentMethodToken, - types::PaymentMethodTokenizationData, - types::PaymentsResponseData, - > for Billwerk -{ - // Not Implemented (R) -} - impl ConnectorCommonExt for Billwerk where Self: ConnectorIntegration, @@ -92,9 +85,10 @@ impl ConnectorCommon for Billwerk { ) -> CustomResult)>, errors::ConnectorError> { let auth = billwerk::BillwerkAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let encoded_api_key = consts::BASE64_ENGINE.encode(format!("{}:", auth.api_key.peek())); Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), + format!("Basic {encoded_api_key}").into_masked(), )]) } @@ -113,9 +107,13 @@ impl ConnectorCommon for Billwerk { Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response + .code + .map_or(consts::NO_ERROR_CODE.to_string(), |code| code.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(response.error), attempt_status: None, connector_transaction_id: None, }) @@ -123,7 +121,20 @@ impl ConnectorCommon for Billwerk { } impl ConnectorValidation for Billwerk { - //TODO: implement functions when support enabled + fn validate_capture_method( + &self, + capture_method: Option, + _pmt: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + common_enums::CaptureMethod::Automatic | common_enums::CaptureMethod::Manual => Ok(()), + common_enums::CaptureMethod::ManualMultiple + | common_enums::CaptureMethod::Scheduled => Err( + super::utils::construct_not_implemented_error_report(capture_method, self.id()), + ), + } + } } impl ConnectorIntegration @@ -146,6 +157,105 @@ impl { } +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Billwerk +{ + fn get_headers( + &self, + req: &types::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, 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::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let base_url = connectors + .billwerk + .secondary_base_url + .as_ref() + .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; + Ok(format!("{base_url}v1/token")) + } + + fn get_request_body( + &self, + req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = billwerk::BillwerkTokenRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &types::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::TokenizationType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::TokenizationType::get_headers(self, req, connectors)?) + .set_body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::TokenizationRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: billwerk::BillwerkTokenResponse = res + .response + .parse_struct("BillwerkTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + impl ConnectorIntegration for Billwerk { @@ -164,9 +274,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}v1/charge", self.base_url(connectors))) } fn get_request_body( @@ -214,7 +324,7 @@ impl ConnectorIntegration CustomResult { let response: billwerk::BillwerkPaymentsResponse = res .response - .parse_struct("Billwerk PaymentsAuthorizeResponse") + .parse_struct("Billwerk BillwerkPaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -232,6 +342,14 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res, event_builder) } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } impl ConnectorIntegration @@ -251,10 +369,14 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}v1/charge/{}", + self.base_url(connectors), + req.connector_request_reference_id + )) } fn build_request( @@ -280,7 +402,7 @@ impl ConnectorIntegration CustomResult { let response: billwerk::BillwerkPaymentsResponse = res .response - .parse_struct("billwerk PaymentsSyncResponse") + .parse_struct("billwerk BillwerkPaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -298,6 +420,14 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res, event_builder) } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } impl ConnectorIntegration @@ -317,18 +447,29 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_transaction_id = &req.request.connector_transaction_id; + Ok(format!( + "{}v1/charge/{connector_transaction_id}/settle", + self.base_url(connectors) + )) } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let connector_router_data = billwerk::BillwerkRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_req = billwerk::BillwerkCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -359,7 +500,7 @@ impl ConnectorIntegration CustomResult { let response: billwerk::BillwerkPaymentsResponse = res .response - .parse_struct("Billwerk PaymentsCaptureResponse") + .parse_struct("Billwerk BillwerkPaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -377,11 +518,92 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res, event_builder) } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } impl ConnectorIntegration for Billwerk { + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, 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::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_transaction_id = &req.request.connector_transaction_id; + Ok(format!( + "{}v1/charge/{connector_transaction_id}/cancel", + self.base_url(connectors) + )) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: billwerk::BillwerkPaymentsResponse = res + .response + .parse_struct("Billwerk BillwerkPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } impl ConnectorIntegration @@ -402,9 +624,9 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}v1/refund", self.base_url(connectors))) } fn get_request_body( @@ -467,6 +689,14 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res, event_builder) } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } impl ConnectorIntegration for Billwerk { @@ -484,10 +714,14 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let refund_id = req.request.get_connector_refund_id()?; + Ok(format!( + "{}v1/refund/{refund_id}", + self.base_url(connectors) + )) } fn build_request( @@ -534,6 +768,14 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res, event_builder) } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } #[async_trait::async_trait] diff --git a/crates/router/src/connector/billwerk/transformers.rs b/crates/router/src/connector/billwerk/transformers.rs index c6f025b78986..ffcd14bf05f9 100644 --- a/crates/router/src/connector/billwerk/transformers.rs +++ b/crates/router/src/connector/billwerk/transformers.rs @@ -1,15 +1,16 @@ -use masking::Secret; +use common_utils::pii::{Email, SecretSerdeValue}; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{self, CardData, PaymentsAuthorizeRequestData, RouterData}, + consts, core::errors, types::{self, api, storage::enums}, }; -//TODO: Fill the struct with respective fields pub struct BillwerkRouterData { - pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: i64, pub router_data: T, } @@ -30,7 +31,6 @@ impl T, ), ) -> Result { - //Todo : use utils to convert the amount to the type of amount that a connector accepts Ok(Self { amount, router_data: item, @@ -38,89 +38,213 @@ impl } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct BillwerkPaymentsRequest { - amount: i64, - card: BillwerkCard, +pub struct BillwerkAuthType { + pub(super) api_key: Secret, + pub(super) public_api_key: Secret, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct BillwerkCard { +impl TryFrom<&types::ConnectorAuthType> for BillwerkAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + api_key: api_key.to_owned(), + public_api_key: key1.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BillwerkTokenRequestIntent { + ChargeAndStore, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BillwerkStrongAuthRule { + UseScaIfAvailableAuth, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BillwerkTokenRequest { number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + month: Secret, + year: Secret, + cvv: Secret, + pkey: Secret, + recurring: Option, + intent: Option, + strong_authentication_rule: Option, } -impl TryFrom<&BillwerkRouterData<&types::PaymentsAuthorizeRouterData>> for BillwerkPaymentsRequest { +impl TryFrom<&types::TokenizationRouterData> for BillwerkTokenRequest { type Error = error_stack::Report; - fn try_from( - item: &BillwerkRouterData<&types::PaymentsAuthorizeRouterData>, - ) -> Result { - match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(req_card) => { - let card = BillwerkCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; + fn try_from(item: &types::TokenizationRouterData) -> Result { + match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ccard) => { + let connector_auth = &item.connector_auth_type; + let auth_type = BillwerkAuthType::try_from(connector_auth)?; Ok(Self { - amount: item.amount.to_owned(), - card, + number: ccard.card_number.clone(), + month: ccard.card_exp_month.clone(), + year: ccard.get_card_expiry_year_2_digit()?, + cvv: ccard.card_cvc, + pkey: auth_type.public_api_key, + recurring: None, + intent: None, + strong_authentication_rule: None, }) } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + api_models::payments::PaymentMethodData::Wallet(_) + | api_models::payments::PaymentMethodData::CardRedirect(_) + | api_models::payments::PaymentMethodData::PayLater(_) + | api_models::payments::PaymentMethodData::BankRedirect(_) + | api_models::payments::PaymentMethodData::BankDebit(_) + | api_models::payments::PaymentMethodData::BankTransfer(_) + | api_models::payments::PaymentMethodData::Crypto(_) + | api_models::payments::PaymentMethodData::MandatePayment + | api_models::payments::PaymentMethodData::Reward + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::Voucher(_) + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("billwerk"), + ) + .into()) + } } } } -//TODO: Fill the struct with respective fields -// Auth Struct -pub struct BillwerkAuthType { - pub(super) api_key: Secret, +#[derive(Debug, Deserialize, Serialize)] +pub struct BillwerkTokenResponse { + id: Secret, + recurring: Option, } -impl TryFrom<&types::ConnectorAuthType> for BillwerkAuthType { +impl + TryFrom< + types::ResponseRouterData< + api::PaymentMethodToken, + BillwerkTokenResponse, + T, + types::PaymentsResponseData, + >, + > for types::RouterData +{ 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_owned(), + fn try_from( + item: types::ResponseRouterData< + api::PaymentMethodToken, + BillwerkTokenResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(types::PaymentsResponseData::TokenizationResponse { + token: item.response.id.expose(), }), - _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), - } + ..item.data + }) + } +} + +#[derive(Debug, Serialize)] +pub struct BillwerkCustomerObject { + handle: Option, + email: Option, + address: Option>, + address2: Option>, + city: Option, + country: Option, + first_name: Option>, + last_name: Option>, +} + +#[derive(Debug, Serialize)] +pub struct BillwerkPaymentsRequest { + handle: String, + amount: i64, + source: Secret, + currency: common_enums::Currency, + customer: BillwerkCustomerObject, + metadata: Option, + settle: bool, +} + +impl TryFrom<&BillwerkRouterData<&types::PaymentsAuthorizeRouterData>> for BillwerkPaymentsRequest { + type Error = error_stack::Report; + fn try_from( + item: &BillwerkRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + if item.router_data.is_three_ds() { + return Err(errors::ConnectorError::NotImplemented( + "Three_ds payments through Billwerk".to_string(), + ) + .into()); + }; + let source = match item.router_data.get_payment_method_token()? { + types::PaymentMethodToken::Token(pm_token) => Ok(Secret::new(pm_token)), + _ => Err(errors::ConnectorError::MissingRequiredField { + field_name: "payment_method_token", + }), + }?; + Ok(Self { + handle: item.router_data.connector_request_reference_id.clone(), + amount: item.amount, + source, + currency: item.router_data.request.currency, + customer: BillwerkCustomerObject { + handle: item.router_data.customer_id.clone(), + email: item.router_data.request.email.clone(), + address: item.router_data.get_optional_billing_line1(), + address2: item.router_data.get_optional_billing_line2(), + city: item.router_data.get_optional_billing_city(), + country: item.router_data.get_optional_billing_country(), + first_name: item.router_data.get_optional_billing_first_name(), + last_name: item.router_data.get_optional_billing_last_name(), + }, + metadata: item.router_data.request.metadata.clone(), + settle: item.router_data.request.is_auto_capture()?, + }) } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] -pub enum BillwerkPaymentStatus { - Succeeded, +pub enum BillwerkPaymentState { + Created, + Authorized, + Pending, + Settled, Failed, - #[default] - Processing, + Cancelled, } -impl From for enums::AttemptStatus { - fn from(item: BillwerkPaymentStatus) -> Self { +impl From for enums::AttemptStatus { + fn from(item: BillwerkPaymentState) -> Self { match item { - BillwerkPaymentStatus::Succeeded => Self::Charged, - BillwerkPaymentStatus::Failed => Self::Failure, - BillwerkPaymentStatus::Processing => Self::Authorizing, + BillwerkPaymentState::Created | BillwerkPaymentState::Pending => Self::Pending, + BillwerkPaymentState::Authorized => Self::Authorized, + BillwerkPaymentState::Settled => Self::Charged, + BillwerkPaymentState::Failed => Self::Failure, + BillwerkPaymentState::Cancelled => Self::Voided, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct BillwerkPaymentsResponse { - status: BillwerkPaymentStatus, - id: String, + state: BillwerkPaymentState, + handle: String, + error: Option, + error_state: Option, } impl @@ -136,28 +260,65 @@ impl types::PaymentsResponseData, >, ) -> Result { + let error_response = if item.response.error.is_some() || item.response.error_state.is_some() + { + Some(types::ErrorResponse { + code: item + .response + .error_state + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: item + .response + .error_state + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: item.response.error, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.handle.clone()), + }) + } else { + None + }; + let payments_response = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.handle.clone()), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.handle), + incremental_authorization_allowed: None, + }; Ok(Self { - status: enums::AttemptStatus::from(item.response.status), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - }), + status: enums::AttemptStatus::from(item.response.state), + response: error_response.map_or_else(|| Ok(payments_response), Err), ..item.data }) } } -//TODO: Fill the struct with respective fields -// REFUND : +#[derive(Debug, Serialize)] +pub struct BillwerkCaptureRequest { + amount: i64, +} + +impl TryFrom<&BillwerkRouterData<&types::PaymentsCaptureRouterData>> for BillwerkCaptureRequest { + type Error = error_stack::Report; + fn try_from( + item: &BillwerkRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + Ok(Self { + amount: item.amount, + }) + } +} + // Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] pub struct BillwerkRefundRequest { + pub invoice: String, pub amount: i64, + pub text: Option, } impl TryFrom<&BillwerkRouterData<&types::RefundsRouterData>> for BillwerkRefundRequest { @@ -166,38 +327,36 @@ impl TryFrom<&BillwerkRouterData<&types::RefundsRouterData>> for BillwerkR item: &BillwerkRouterData<&types::RefundsRouterData>, ) -> Result { Ok(Self { - amount: item.amount.to_owned(), + amount: item.amount, + invoice: item.router_data.request.connector_transaction_id.clone(), + text: item.router_data.request.reason.clone(), }) } } // Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RefundState { + Refunded, Failed, - #[default] Processing, } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { +impl From for enums::RefundStatus { + fn from(item: RefundState) -> Self { match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + RefundState::Refunded => Self::Success, + RefundState::Failed => Self::Failure, + RefundState::Processing => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct RefundResponse { id: String, - status: RefundStatus, + state: RefundState, } impl TryFrom> @@ -210,7 +369,7 @@ impl TryFrom> Ok(Self { response: Ok(types::RefundsResponseData { connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + refund_status: enums::RefundStatus::from(item.response.state), }), ..item.data }) @@ -227,18 +386,16 @@ impl TryFrom> Ok(Self { response: Ok(types::RefundsResponseData { connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + refund_status: enums::RefundStatus::from(item.response.state), }), ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize)] pub struct BillwerkErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, + pub code: Option, + pub error: String, + pub message: Option, } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 89b88243057c..804ad3ef3d00 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -88,6 +88,14 @@ pub trait RouterData { fn get_optional_billing(&self) -> Option<&api::Address>; fn get_optional_shipping(&self) -> Option<&api::Address>; + fn get_optional_billing_line1(&self) -> Option>; + fn get_optional_billing_line2(&self) -> Option>; + fn get_optional_billing_city(&self) -> Option; + fn get_optional_billing_country(&self) -> Option; + fn get_optional_billing_zip(&self) -> Option>; + fn get_optional_billing_state(&self) -> Option>; + fn get_optional_billing_first_name(&self) -> Option>; + fn get_optional_billing_last_name(&self) -> Option>; } pub trait PaymentResponseRouterData { @@ -202,6 +210,94 @@ impl RouterData for types::RouterData Option> { + self.address + .get_payment_method_billing() + .and_then(|billing_address| { + billing_address + .clone() + .address + .and_then(|billing_address_details| billing_address_details.line1) + }) + } + + fn get_optional_billing_line2(&self) -> Option> { + self.address + .get_payment_method_billing() + .and_then(|billing_address| { + billing_address + .clone() + .address + .and_then(|billing_address_details| billing_address_details.line2) + }) + } + + fn get_optional_billing_city(&self) -> Option { + self.address + .get_payment_method_billing() + .and_then(|billing_address| { + billing_address + .clone() + .address + .and_then(|billing_address_details| billing_address_details.city) + }) + } + + fn get_optional_billing_country(&self) -> Option { + self.address + .get_payment_method_billing() + .and_then(|billing_address| { + billing_address + .clone() + .address + .and_then(|billing_address_details| billing_address_details.country) + }) + } + + fn get_optional_billing_zip(&self) -> Option> { + self.address + .get_payment_method_billing() + .and_then(|billing_address| { + billing_address + .clone() + .address + .and_then(|billing_address_details| billing_address_details.zip) + }) + } + + fn get_optional_billing_state(&self) -> Option> { + self.address + .get_payment_method_billing() + .and_then(|billing_address| { + billing_address + .clone() + .address + .and_then(|billing_address_details| billing_address_details.state) + }) + } + + fn get_optional_billing_first_name(&self) -> Option> { + self.address + .get_payment_method_billing() + .and_then(|billing_address| { + billing_address + .clone() + .address + .and_then(|billing_address_details| billing_address_details.first_name) + }) + } + + fn get_optional_billing_last_name(&self) -> Option> { + self.address + .get_payment_method_billing() + .and_then(|billing_address| { + billing_address + .clone() + .address + .and_then(|billing_address_details| billing_address_details.last_name) + }) + } + fn to_connector_meta(&self) -> Result where T: serde::de::DeserializeOwned, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index f48227b926ef..ab402f3a891f 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1731,10 +1731,10 @@ pub(crate) fn validate_auth_and_metadata_type( bankofamerica::transformers::BankOfAmericaAuthType::try_from(val)?; Ok(()) } - // api_enums::Connector::Billwerk => { - // billwerk::transformers::BillwerkAuthType::try_from(val)?; - // Ok(()) - // } Added as template code for future usage + api_enums::Connector::Billwerk => { + billwerk::transformers::BillwerkAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Bitpay => { bitpay::transformers::BitpayAuthType::try_from(val)?; Ok(()) diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 4b72c7ac2e63..b5a4d04c9271 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -326,7 +326,7 @@ impl ConnectorData { enums::Connector::Authorizedotnet => Ok(Box::new(&connector::Authorizedotnet)), enums::Connector::Bambora => Ok(Box::new(&connector::Bambora)), enums::Connector::Bankofamerica => Ok(Box::new(&connector::Bankofamerica)), - // enums::Connector::Billwerk => Ok(Box::new(&connector::Billwerk)), Added as template code for future usage + enums::Connector::Billwerk => Ok(Box::new(&connector::Billwerk)), enums::Connector::Bitpay => Ok(Box::new(&connector::Bitpay)), enums::Connector::Bluesnap => Ok(Box::new(&connector::Bluesnap)), enums::Connector::Boku => Ok(Box::new(&connector::Boku)), diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 237dea196d68..3362aad6fc55 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -188,7 +188,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Authorizedotnet => Self::Authorizedotnet, api_enums::Connector::Bambora => Self::Bambora, api_enums::Connector::Bankofamerica => Self::Bankofamerica, - // api_enums::Connector::Billwerk => Self::Billwerk, Added as template code for future usage + api_enums::Connector::Billwerk => Self::Billwerk, api_enums::Connector::Bitpay => Self::Bitpay, api_enums::Connector::Bluesnap => Self::Bluesnap, api_enums::Connector::Boku => Self::Boku, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 9a9a7606ca1d..72eb6f255595 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -72,6 +72,7 @@ authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" billwerk.base_url = "https://api.reepay.com/" +billwerk.secondary_base_url = "https://card.reepay.com/" bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" @@ -219,6 +220,7 @@ checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_ mollie = {long_lived_token = false, payment_method = "card"} braintree = { long_lived_token = false, payment_method = "card" } gocardless = {long_lived_token = true, payment_method = "bank_debit"} +billwerk = {long_lived_token = false, payment_method = "card"} [connector_customer] connector_list = "gocardless,stax,stripe" diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 1e16d0724718..595b18307220 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7067,6 +7067,7 @@ "authorizedotnet", "bambora", "bankofamerica", + "billwerk", "bitpay", "bluesnap", "boku", @@ -16813,6 +16814,7 @@ "airwallex", "authorizedotnet", "bankofamerica", + "billwerk", "bitpay", "bambora", "bluesnap",