mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +08:00 
			
		
		
		
	feat(connector): [Bambora APAC] add mandate flow (#5376)
This commit is contained in:
		| @ -186,7 +186,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" | ||||
| applepay.base_url = "https://apple-pay-gateway.apple.com/" | ||||
| authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" | ||||
| 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/" | ||||
| billwerk.base_url = "https://api.reepay.com/" | ||||
| billwerk.secondary_base_url = "https://card.reepay.com/" | ||||
|  | ||||
| @ -26,7 +26,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" | ||||
| applepay.base_url = "https://apple-pay-gateway.apple.com/" | ||||
| authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" | ||||
| 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/" | ||||
| billwerk.base_url = "https://api.reepay.com/" | ||||
| billwerk.secondary_base_url = "https://card.reepay.com/" | ||||
|  | ||||
| @ -30,7 +30,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" | ||||
| applepay.base_url = "https://apple-pay-gateway.apple.com/" | ||||
| authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" | ||||
| 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/" | ||||
| billwerk.base_url = "https://api.reepay.com/" | ||||
| billwerk.secondary_base_url = "https://card.reepay.com/" | ||||
|  | ||||
| @ -182,7 +182,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" | ||||
| applepay.base_url = "https://apple-pay-gateway.apple.com/" | ||||
| authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" | ||||
| 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/" | ||||
| billwerk.base_url = "https://api.reepay.com/" | ||||
| billwerk.secondary_base_url = "https://card.reepay.com/" | ||||
|  | ||||
| @ -115,7 +115,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" | ||||
| applepay.base_url = "https://apple-pay-gateway.apple.com/" | ||||
| authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" | ||||
| 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/" | ||||
| billwerk.base_url = "https://api.reepay.com/" | ||||
| billwerk.secondary_base_url = "https://card.reepay.com/" | ||||
|  | ||||
| @ -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 { | ||||
| @ -167,6 +183,85 @@ impl | ||||
|         types::PaymentsResponseData, | ||||
|     > 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> | ||||
| @ -189,7 +284,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P | ||||
|         _req: &types::PaymentsAuthorizeRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(self.base_url(connectors).to_string()) | ||||
|         Ok(format!("{}/dts.asmx", self.base_url(connectors))) | ||||
|     } | ||||
|  | ||||
|     fn get_request_body( | ||||
| @ -284,7 +379,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe | ||||
|         _req: &types::PaymentsSyncRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(self.base_url(connectors).to_string()) | ||||
|         Ok(format!("{}/dts.asmx", self.base_url(connectors))) | ||||
|     } | ||||
|  | ||||
|     fn get_request_body( | ||||
| @ -368,7 +463,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme | ||||
|         _req: &types::PaymentsCaptureRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(self.base_url(connectors).to_string()) | ||||
|         Ok(format!("{}/dts.asmx", self.base_url(connectors))) | ||||
|     } | ||||
|  | ||||
|     fn get_request_body( | ||||
| @ -467,7 +562,7 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon | ||||
|         _req: &types::RefundExecuteRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(self.base_url(connectors).to_string()) | ||||
|         Ok(format!("{}/dts.asmx", self.base_url(connectors))) | ||||
|     } | ||||
|  | ||||
|     fn get_request_body( | ||||
| @ -560,7 +655,7 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse | ||||
|         _req: &types::RefundSyncRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(self.base_url(connectors).to_string()) | ||||
|         Ok(format!("{}/dts.asmx", self.base_url(connectors))) | ||||
|     } | ||||
|  | ||||
|     fn get_request_body( | ||||
|  | ||||
| @ -5,7 +5,7 @@ use masking::{PeekInterface, Secret}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{ | ||||
|     connector::utils::{self, CardData, RouterData}, | ||||
|     connector::utils::{self, CardData, PaymentsAuthorizeRequestData, RouterData}, | ||||
|     core::errors, | ||||
|     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_data = match &req.request.payment_method_data { | ||||
|         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!( | ||||
|                     r#" | ||||
|                     <CreditCard Registered="False"> | ||||
| @ -111,6 +130,18 @@ fn get_card_data(req: &types::PaymentsAuthorizeRouterData) -> Result<String, Err | ||||
|                     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( | ||||
|                 utils::get_unimplemented_payment_method_error_message("Bambora APAC"), | ||||
| @ -182,6 +213,7 @@ pub struct SubmitSinglePaymentResult { | ||||
| pub struct PaymentResponse { | ||||
|     response_code: u8, | ||||
|     receipt: String, | ||||
|     credit_card_token: Option<String>, | ||||
|     declined_code: Option<String>, | ||||
|     declined_message: Option<String>, | ||||
| } | ||||
| @ -234,6 +266,23 @@ impl<F> | ||||
|             .submit_single_payment_result | ||||
|             .response | ||||
|             .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 | ||||
|         if response_code == 0 { | ||||
|             Ok(Self { | ||||
| @ -243,7 +292,7 @@ impl<F> | ||||
|                         connector_transaction_id.to_owned(), | ||||
|                     ), | ||||
|                     redirection_data: None, | ||||
|                     mandate_reference: None, | ||||
|                     mandate_reference, | ||||
|                     connector_metadata: None, | ||||
|                     network_txn_id: None, | ||||
|                     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 | ||||
| pub fn get_capture_body( | ||||
|     req: &BamboraapacRouterData<&types::PaymentsCaptureRouterData>, | ||||
|  | ||||
| @ -80,7 +80,7 @@ airwallex.base_url = "https://api-demo.airwallex.com/" | ||||
| applepay.base_url = "https://apple-pay-gateway.apple.com/" | ||||
| authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" | ||||
| 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/" | ||||
| billwerk.base_url = "https://api.reepay.com/" | ||||
| billwerk.secondary_base_url = "https://card.reepay.com/" | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Sakil Mostak
					Sakil Mostak