feat(connector): [Bambora APAC] add mandate flow (#5376)

This commit is contained in:
Sakil Mostak
2024-07-26 18:45:23 +05:30
committed by GitHub
parent fbff2683ef
commit dbfa006b47
8 changed files with 321 additions and 24 deletions

View File

@ -186,7 +186,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/"
applepay.base_url = "https://apple-pay-gateway.apple.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/"
authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api"
bambora.base_url = "https://api.na.bambora.com" bambora.base_url = "https://api.na.bambora.com"
bamboraapac.base_url = "https://demo.bambora.co.nz/interface/api/dts.asmx" bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api"
bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/"
billwerk.base_url = "https://api.reepay.com/" billwerk.base_url = "https://api.reepay.com/"
billwerk.secondary_base_url = "https://card.reepay.com/" billwerk.secondary_base_url = "https://card.reepay.com/"

View File

@ -26,7 +26,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/"
applepay.base_url = "https://apple-pay-gateway.apple.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/"
authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api"
bambora.base_url = "https://api.na.bambora.com" bambora.base_url = "https://api.na.bambora.com"
bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api/dts.asmx" bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api"
bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/"
billwerk.base_url = "https://api.reepay.com/" billwerk.base_url = "https://api.reepay.com/"
billwerk.secondary_base_url = "https://card.reepay.com/" billwerk.secondary_base_url = "https://card.reepay.com/"

View File

@ -30,7 +30,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/"
applepay.base_url = "https://apple-pay-gateway.apple.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/"
authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api"
bambora.base_url = "https://api.na.bambora.com" bambora.base_url = "https://api.na.bambora.com"
bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api/dts.asmx" bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api"
bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/"
billwerk.base_url = "https://api.reepay.com/" billwerk.base_url = "https://api.reepay.com/"
billwerk.secondary_base_url = "https://card.reepay.com/" billwerk.secondary_base_url = "https://card.reepay.com/"

View File

@ -182,7 +182,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/"
applepay.base_url = "https://apple-pay-gateway.apple.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/"
authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api"
bambora.base_url = "https://api.na.bambora.com" bambora.base_url = "https://api.na.bambora.com"
bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api/dts.asmx" bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api"
bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/"
billwerk.base_url = "https://api.reepay.com/" billwerk.base_url = "https://api.reepay.com/"
billwerk.secondary_base_url = "https://card.reepay.com/" billwerk.secondary_base_url = "https://card.reepay.com/"

View File

@ -115,7 +115,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/"
applepay.base_url = "https://apple-pay-gateway.apple.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/"
authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api"
bambora.base_url = "https://api.na.bambora.com" bambora.base_url = "https://api.na.bambora.com"
bamboraapac.base_url = "https://demo.bambora.co.nz/interface/api/dts.asmx" bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api"
bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/"
billwerk.base_url = "https://api.reepay.com/" billwerk.base_url = "https://api.reepay.com/"
billwerk.secondary_base_url = "https://card.reepay.com/" billwerk.secondary_base_url = "https://card.reepay.com/"

View File

@ -92,6 +92,22 @@ impl ConnectorValidation for Bamboraapac {
), ),
} }
} }
fn validate_mandate_payment(
&self,
_pm_type: Option<enums::PaymentMethodType>,
pm_data: types::domain::payments::PaymentMethodData,
) -> CustomResult<(), errors::ConnectorError> {
let connector = self.id();
match pm_data {
types::domain::payments::PaymentMethodData::Card(_) => Ok(()),
_ => Err(errors::ConnectorError::NotSupported {
message: "mandate payment".to_string(),
connector,
}
.into()),
}
}
} }
impl ConnectorCommon for Bamboraapac { impl ConnectorCommon for Bamboraapac {
@ -167,6 +183,85 @@ impl
types::PaymentsResponseData, types::PaymentsResponseData,
> for Bamboraapac > for Bamboraapac
{ {
fn get_headers(
&self,
req: &types::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}/sipp.asmx", self.base_url(connectors)))
}
fn get_request_body(
&self,
req: &types::SetupMandateRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = bamboraapac::get_setup_mandate_body(req)?;
Ok(RequestContent::RawBytes(connector_req))
}
fn build_request(
&self,
req: &types::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::SetupMandateType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::SetupMandateType::get_headers(self, req, connectors)?)
.set_body(types::SetupMandateType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::SetupMandateRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::SetupMandateRouterData, errors::ConnectorError> {
let response_data = html_to_xml_string_conversion(
String::from_utf8(res.response.to_vec())
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?,
);
let response = response_data
.parse_xml::<bamboraapac::BamboraapacMandateResponse>()
.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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
} }
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData> impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
@ -189,7 +284,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
_req: &types::PaymentsAuthorizeRouterData, _req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Ok(self.base_url(connectors).to_string()) Ok(format!("{}/dts.asmx", self.base_url(connectors)))
} }
fn get_request_body( fn get_request_body(
@ -284,7 +379,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
_req: &types::PaymentsSyncRouterData, _req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Ok(self.base_url(connectors).to_string()) Ok(format!("{}/dts.asmx", self.base_url(connectors)))
} }
fn get_request_body( fn get_request_body(
@ -368,7 +463,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
_req: &types::PaymentsCaptureRouterData, _req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Ok(self.base_url(connectors).to_string()) Ok(format!("{}/dts.asmx", self.base_url(connectors)))
} }
fn get_request_body( fn get_request_body(
@ -467,7 +562,7 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
_req: &types::RefundExecuteRouterData, _req: &types::RefundExecuteRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Ok(self.base_url(connectors).to_string()) Ok(format!("{}/dts.asmx", self.base_url(connectors)))
} }
fn get_request_body( fn get_request_body(
@ -560,7 +655,7 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
_req: &types::RefundSyncRouterData, _req: &types::RefundSyncRouterData,
connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
Ok(self.base_url(connectors).to_string()) Ok(format!("{}/dts.asmx", self.base_url(connectors)))
} }
fn get_request_body( fn get_request_body(

View File

@ -5,7 +5,7 @@ use masking::{PeekInterface, Secret};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
connector::utils::{self, CardData, RouterData}, connector::utils::{self, CardData, PaymentsAuthorizeRequestData, RouterData},
core::errors, core::errors,
types::{self, domain, storage::enums, transformers::ForeignFrom}, types::{self, domain, storage::enums, transformers::ForeignFrom},
}; };
@ -94,6 +94,25 @@ fn get_card_data(req: &types::PaymentsAuthorizeRouterData) -> Result<String, Err
let card_holder_name = req.get_billing_full_name()?; let card_holder_name = req.get_billing_full_name()?;
let card_data = match &req.request.payment_method_data { let card_data = match &req.request.payment_method_data {
domain::PaymentMethodData::Card(card) => { domain::PaymentMethodData::Card(card) => {
if req.request.setup_future_usage == Some(enums::FutureUsage::OffSession) {
format!(
r#"
<CreditCard Registered="False">
<TokeniseAlgorithmID>2</TokeniseAlgorithmID>
<CardNumber>{}</CardNumber>
<ExpM>{}</ExpM>
<ExpY>{}</ExpY>
<CVN>{}</CVN>
<CardHolderName>{}</CardHolderName>
</CreditCard>
"#,
card.card_number.get_card_no(),
card.card_exp_month.peek(),
card.get_expiry_year_4_digit().peek(),
card.card_cvc.peek(),
card_holder_name.peek(),
)
} else {
format!( format!(
r#" r#"
<CreditCard Registered="False"> <CreditCard Registered="False">
@ -111,6 +130,18 @@ fn get_card_data(req: &types::PaymentsAuthorizeRouterData) -> Result<String, Err
card_holder_name.peek(), card_holder_name.peek(),
) )
} }
}
domain::PaymentMethodData::MandatePayment => {
format!(
r#"
<CreditCard>
<TokeniseAlgorithmID>2</TokeniseAlgorithmID>
<CardNumber>{}</CardNumber>
</CreditCard>
"#,
req.request.get_connector_mandate_id()?
)
}
_ => { _ => {
return Err(errors::ConnectorError::NotImplemented( return Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("Bambora APAC"), utils::get_unimplemented_payment_method_error_message("Bambora APAC"),
@ -182,6 +213,7 @@ pub struct SubmitSinglePaymentResult {
pub struct PaymentResponse { pub struct PaymentResponse {
response_code: u8, response_code: u8,
receipt: String, receipt: String,
credit_card_token: Option<String>,
declined_code: Option<String>, declined_code: Option<String>,
declined_message: Option<String>, declined_message: Option<String>,
} }
@ -234,6 +266,23 @@ impl<F>
.submit_single_payment_result .submit_single_payment_result
.response .response
.receipt; .receipt;
let mandate_reference =
if item.data.request.setup_future_usage == Some(enums::FutureUsage::OffSession) {
let connector_mandate_id = item
.response
.body
.submit_single_payment_response
.submit_single_payment_result
.response
.credit_card_token;
Some(types::MandateReference {
connector_mandate_id,
payment_method_id: None,
})
} else {
None
};
// transaction approved // transaction approved
if response_code == 0 { if response_code == 0 {
Ok(Self { Ok(Self {
@ -243,7 +292,7 @@ impl<F>
connector_transaction_id.to_owned(), connector_transaction_id.to_owned(),
), ),
redirection_data: None, redirection_data: None,
mandate_reference: None, mandate_reference,
connector_metadata: None, connector_metadata: None,
network_txn_id: None, network_txn_id: None,
connector_response_reference_id: Some(connector_transaction_id), connector_response_reference_id: Some(connector_transaction_id),
@ -288,6 +337,159 @@ impl<F>
} }
} }
pub fn get_setup_mandate_body(req: &types::SetupMandateRouterData) -> Result<Vec<u8>, Error> {
let card_holder_name = req.get_billing_full_name()?;
let auth_details = BamboraapacAuthType::try_from(&req.connector_auth_type)?;
let body = match &req.request.payment_method_data {
domain::PaymentMethodData::Card(card) => {
format!(
r#"
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:sipp="http://www.ippayments.com.au/interface/api/sipp">
<soapenv:Header/>
<soapenv:Body>
<sipp:TokeniseCreditCard>
<sipp:tokeniseCreditCardXML>
<![CDATA[
<TokeniseCreditCard>
<CardNumber>{}</CardNumber>
<ExpM>{}</ExpM>
<ExpY>{}</ExpY>
<CardHolderName>{}</CardHolderName>
<TokeniseAlgorithmID>2</TokeniseAlgorithmID>
<UserName>{}</UserName>
<Password>{}</Password>
</TokeniseCreditCard>
]]>
</sipp:tokeniseCreditCardXML>
</sipp:TokeniseCreditCard>
</soapenv:Body>
</soapenv:Envelope>
"#,
card.card_number.get_card_no(),
card.card_exp_month.peek(),
card.get_expiry_year_4_digit().peek(),
card_holder_name.peek(),
auth_details.username.peek(),
auth_details.password.peek(),
)
}
_ => {
return Err(errors::ConnectorError::NotImplemented(
utils::get_unimplemented_payment_method_error_message("Bambora APAC"),
))?;
}
};
Ok(body.as_bytes().to_vec())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "Envelope")]
#[serde(rename_all = "PascalCase")]
pub struct BamboraapacMandateResponse {
body: MandateBodyResponse,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct MandateBodyResponse {
tokenise_credit_card_response: TokeniseCreditCardResponse,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct TokeniseCreditCardResponse {
tokenise_credit_card_result: TokeniseCreditCardResult,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct TokeniseCreditCardResult {
tokenise_credit_card_response: MandateResponseBody,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct MandateResponseBody {
return_value: u8,
token: Option<String>,
}
impl<F>
TryFrom<
types::ResponseRouterData<
F,
BamboraapacMandateResponse,
types::SetupMandateRequestData,
types::PaymentsResponseData,
>,
> for types::RouterData<F, types::SetupMandateRequestData, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
BamboraapacMandateResponse,
types::SetupMandateRequestData,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let response_code = item
.response
.body
.tokenise_credit_card_response
.tokenise_credit_card_result
.tokenise_credit_card_response
.return_value;
let connector_mandate_id = item
.response
.body
.tokenise_credit_card_response
.tokenise_credit_card_result
.tokenise_credit_card_response
.token
.ok_or(errors::ConnectorError::MissingConnectorMandateID)?;
// transaction approved
if response_code == 0 {
Ok(Self {
status: enums::AttemptStatus::Charged,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::NoResponseId,
redirection_data: None,
mandate_reference: Some(types::MandateReference {
connector_mandate_id: Some(connector_mandate_id),
payment_method_id: None,
}),
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
incremental_authorization_allowed: None,
charge_id: None,
}),
..item.data
})
}
// transaction failed
else {
Ok(Self {
status: enums::AttemptStatus::Failure,
response: Err(types::ErrorResponse {
status_code: item.http_code,
code: consts::NO_ERROR_CODE.to_string(),
message: consts::NO_ERROR_MESSAGE.to_string(),
reason: None,
attempt_status: None,
connector_transaction_id: None,
}),
..item.data
})
}
}
}
// capture body in soap format // capture body in soap format
pub fn get_capture_body( pub fn get_capture_body(
req: &BamboraapacRouterData<&types::PaymentsCaptureRouterData>, req: &BamboraapacRouterData<&types::PaymentsCaptureRouterData>,

View File

@ -80,7 +80,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/"
applepay.base_url = "https://apple-pay-gateway.apple.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/"
authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api"
bambora.base_url = "https://api.na.bambora.com" bambora.base_url = "https://api.na.bambora.com"
bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api/dts.asmx" bamboraapac.base_url = "https://demo.ippayments.com.au/interface/api"
bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/"
billwerk.base_url = "https://api.reepay.com/" billwerk.base_url = "https://api.reepay.com/"
billwerk.secondary_base_url = "https://card.reepay.com/" billwerk.secondary_base_url = "https://card.reepay.com/"