mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	feat(connector): [Payme] Implement Card 3DS with sdk flow (#2082)
Co-authored-by: Sangamesh <sangamesh.kulkarni@juspay.in>
This commit is contained in:
		| @ -370,6 +370,7 @@ stax = { long_lived_token = true, payment_method = "card,bank_debit" } | ||||
| mollie = {long_lived_token = false, payment_method = "card"} | ||||
| square = {long_lived_token = false, payment_method = "card"} | ||||
| braintree = { long_lived_token = false, payment_method = "card" } | ||||
| payme = {long_lived_token = false, payment_method = "card"} | ||||
|  | ||||
| [connector_customer] | ||||
| connector_list = "bluesnap,stax,stripe" | ||||
|  | ||||
| @ -2,6 +2,7 @@ pub mod transformers; | ||||
|  | ||||
| use std::fmt::Debug; | ||||
|  | ||||
| use api_models::enums::AuthenticationType; | ||||
| use common_utils::crypto; | ||||
| use diesel_models::enums; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| @ -11,7 +12,10 @@ use transformers as payme; | ||||
| use crate::{ | ||||
|     configs::settings, | ||||
|     connector::utils as connector_utils, | ||||
|     core::errors::{self, CustomResult}, | ||||
|     core::{ | ||||
|         errors::{self, CustomResult}, | ||||
|         payments, | ||||
|     }, | ||||
|     db, headers, | ||||
|     services::{self, request, ConnectorIntegration, ConnectorValidation}, | ||||
|     types::{ | ||||
| @ -27,6 +31,7 @@ pub struct Payme; | ||||
|  | ||||
| impl api::Payment for Payme {} | ||||
| impl api::PaymentSession for Payme {} | ||||
| impl api::PaymentsCompleteAuthorize for Payme {} | ||||
| impl api::ConnectorAccessToken for Payme {} | ||||
| impl api::PreVerify for Payme {} | ||||
| impl api::PaymentAuthorize for Payme {} | ||||
| @ -38,15 +43,6 @@ impl api::RefundExecute for Payme {} | ||||
| impl api::RefundSync for Payme {} | ||||
| impl api::PaymentToken for Payme {} | ||||
|  | ||||
| impl | ||||
|     ConnectorIntegration< | ||||
|         api::PaymentMethodToken, | ||||
|         types::PaymentMethodTokenizationData, | ||||
|         types::PaymentsResponseData, | ||||
|     > for Payme | ||||
| { | ||||
| } | ||||
|  | ||||
| impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Payme | ||||
| where | ||||
|     Self: ConnectorIntegration<Flow, Request, Response>, | ||||
| @ -116,6 +112,105 @@ impl ConnectorValidation for Payme { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl | ||||
|     ConnectorIntegration< | ||||
|         api::PaymentMethodToken, | ||||
|         types::PaymentMethodTokenizationData, | ||||
|         types::PaymentsResponseData, | ||||
|     > for Payme | ||||
| { | ||||
|     fn get_headers( | ||||
|         &self, | ||||
|         req: &types::TokenizationRouterData, | ||||
|         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::TokenizationRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(format!( | ||||
|             "{}api/capture-buyer-token", | ||||
|             self.base_url(connectors) | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn get_request_body( | ||||
|         &self, | ||||
|         req: &types::TokenizationRouterData, | ||||
|     ) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> { | ||||
|         let req_obj = payme::CaptureBuyerRequest::try_from(req)?; | ||||
|  | ||||
|         let payme_req = types::RequestBody::log_and_get_request_body( | ||||
|             &req_obj, | ||||
|             utils::Encode::<payme::CaptureBuyerRequest>::encode_to_string_of_json, | ||||
|         ) | ||||
|         .change_context(errors::ConnectorError::RequestEncodingFailed)?; | ||||
|         Ok(Some(payme_req)) | ||||
|     } | ||||
|  | ||||
|     fn build_request( | ||||
|         &self, | ||||
|         req: &types::TokenizationRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Option<services::Request>, errors::ConnectorError> { | ||||
|         Ok(match req.auth_type { | ||||
|             AuthenticationType::ThreeDs => 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)?) | ||||
|                     .body(types::TokenizationType::get_request_body(self, req)?) | ||||
|                     .build(), | ||||
|             ), | ||||
|             AuthenticationType::NoThreeDs => None, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn handle_response( | ||||
|         &self, | ||||
|         data: &types::TokenizationRouterData, | ||||
|         res: Response, | ||||
|     ) -> CustomResult<types::TokenizationRouterData, errors::ConnectorError> | ||||
|     where | ||||
|         types::PaymentsResponseData: Clone, | ||||
|     { | ||||
|         let response: payme::CaptureBuyerResponse = res | ||||
|             .response | ||||
|             .parse_struct("Payme CaptureBuyerResponse") | ||||
|             .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; | ||||
|  | ||||
|         types::RouterData::try_from(types::ResponseRouterData { | ||||
|             response, | ||||
|             data: data.clone(), | ||||
|             http_code: res.status_code, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn get_error_response( | ||||
|         &self, | ||||
|         res: Response, | ||||
|     ) -> CustomResult<ErrorResponse, errors::ConnectorError> { | ||||
|         self.build_error_response(res) | ||||
|     } | ||||
|  | ||||
|     fn get_5xx_error_response( | ||||
|         &self, | ||||
|         res: Response, | ||||
|     ) -> CustomResult<ErrorResponse, errors::ConnectorError> { | ||||
|         // we are always getting 500 in error scenarios | ||||
|         self.build_error_response(res) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData> | ||||
|     for Payme | ||||
| { | ||||
| @ -228,6 +323,114 @@ impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::Payments | ||||
| { | ||||
| } | ||||
|  | ||||
| impl services::ConnectorRedirectResponse for Payme { | ||||
|     fn get_flow_type( | ||||
|         &self, | ||||
|         _query_params: &str, | ||||
|         _json_payload: Option<serde_json::Value>, | ||||
|         _action: services::PaymentAction, | ||||
|     ) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> { | ||||
|         Ok(payments::CallConnectorAction::Trigger) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl | ||||
|     ConnectorIntegration< | ||||
|         api::CompleteAuthorize, | ||||
|         types::CompleteAuthorizeData, | ||||
|         types::PaymentsResponseData, | ||||
|     > for Payme | ||||
| { | ||||
|     fn get_headers( | ||||
|         &self, | ||||
|         req: &types::PaymentsCompleteAuthorizeRouterData, | ||||
|         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::PaymentsCompleteAuthorizeRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(format!("{}api/pay-sale", self.base_url(connectors))) | ||||
|     } | ||||
|     fn get_request_body( | ||||
|         &self, | ||||
|         req: &types::PaymentsCompleteAuthorizeRouterData, | ||||
|     ) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> { | ||||
|         let req_obj = payme::Pay3dsRequest::try_from(req)?; | ||||
|         let payme_req = types::RequestBody::log_and_get_request_body( | ||||
|             &req_obj, | ||||
|             utils::Encode::<payme::PayRequest>::encode_to_string_of_json, | ||||
|         ) | ||||
|         .change_context(errors::ConnectorError::RequestEncodingFailed)?; | ||||
|         Ok(Some(payme_req)) | ||||
|     } | ||||
|     fn build_request( | ||||
|         &self, | ||||
|         req: &types::PaymentsCompleteAuthorizeRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Option<services::Request>, errors::ConnectorError> { | ||||
|         Ok(Some( | ||||
|             services::RequestBuilder::new() | ||||
|                 .method(services::Method::Post) | ||||
|                 .url(&types::PaymentsCompleteAuthorizeType::get_url( | ||||
|                     self, req, connectors, | ||||
|                 )?) | ||||
|                 .attach_default_headers() | ||||
|                 .headers(types::PaymentsCompleteAuthorizeType::get_headers( | ||||
|                     self, req, connectors, | ||||
|                 )?) | ||||
|                 .body(types::PaymentsCompleteAuthorizeType::get_request_body( | ||||
|                     self, req, | ||||
|                 )?) | ||||
|                 .build(), | ||||
|         )) | ||||
|     } | ||||
|     fn handle_response( | ||||
|         &self, | ||||
|         data: &types::PaymentsCompleteAuthorizeRouterData, | ||||
|         res: Response, | ||||
|     ) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> { | ||||
|         let response: payme::PaymePaySaleResponse = res | ||||
|             .response | ||||
|             .parse_struct("Payme PaymePaySaleResponse") | ||||
|             .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; | ||||
|         types::RouterData::try_from(types::ResponseRouterData { | ||||
|             response, | ||||
|             data: data.clone(), | ||||
|             http_code: res.status_code, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn get_error_response( | ||||
|         &self, | ||||
|         res: Response, | ||||
|     ) -> CustomResult<ErrorResponse, errors::ConnectorError> { | ||||
|         self.build_error_response(res) | ||||
|     } | ||||
|  | ||||
|     fn get_5xx_error_response( | ||||
|         &self, | ||||
|         res: Response, | ||||
|     ) -> CustomResult<ErrorResponse, errors::ConnectorError> { | ||||
|         self.build_error_response(res) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl | ||||
|     ConnectorIntegration< | ||||
|         api::InitPayment, | ||||
|         types::PaymentsAuthorizeData, | ||||
|         types::PaymentsResponseData, | ||||
|     > for Payme | ||||
| { | ||||
| } | ||||
|  | ||||
| impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData> | ||||
|     for Payme | ||||
| { | ||||
|  | ||||
| @ -1,16 +1,21 @@ | ||||
| use api_models::payments::PaymentMethodData; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use api_models::{enums::AuthenticationType, payments::PaymentMethodData}; | ||||
| use common_utils::pii; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use masking::{ExposeInterface, Secret}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use url::Url; | ||||
|  | ||||
| use crate::{ | ||||
|     connector::utils::{ | ||||
|         self, missing_field_err, AddressDetailsData, CardData, PaymentsAuthorizeRequestData, | ||||
|         PaymentsPreProcessingData, PaymentsSyncRequestData, RouterData, | ||||
|         PaymentsCompleteAuthorizeRequestData, PaymentsPreProcessingData, PaymentsSyncRequestData, | ||||
|         RouterData, | ||||
|     }, | ||||
|     consts, | ||||
|     core::errors, | ||||
|     services, | ||||
|     types::{self, api, storage::enums, MandateReference}, | ||||
| }; | ||||
|  | ||||
| @ -39,6 +44,15 @@ pub struct MandateRequest { | ||||
|     language: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct Pay3dsRequest { | ||||
|     buyer_name: Secret<String>, | ||||
|     buyer_email: pii::Email, | ||||
|     buyer_key: String, | ||||
|     payme_sale_id: String, | ||||
|     meta_data_jwt: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| #[serde(untagged)] | ||||
| pub enum PaymePaymentRequest { | ||||
| @ -65,6 +79,18 @@ pub struct PaymeCard { | ||||
|     credit_card_number: cards::CardNumber, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct CaptureBuyerRequest { | ||||
|     seller_payme_id: Secret<String>, | ||||
|     #[serde(flatten)] | ||||
|     card: PaymeCard, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct CaptureBuyerResponse { | ||||
|     buyer_key: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct GenerateSaleRequest { | ||||
|     currency: enums::Currency, | ||||
| @ -76,9 +102,27 @@ pub struct GenerateSaleRequest { | ||||
|     seller_payme_id: Secret<String>, | ||||
|     sale_callback_url: String, | ||||
|     sale_payment_method: SalePaymentMethod, | ||||
|     services: Option<ThreeDS>, | ||||
|     language: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct ThreeDS { | ||||
|     name: ThreeDSType, | ||||
|     settings: ThreeDSSettings, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub enum ThreeDSType { | ||||
|     #[serde(rename = "3D Secure")] | ||||
|     ThreeDS, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct ThreeDSSettings { | ||||
|     active: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct GenerateSaleResponse { | ||||
|     payme_sale_id: String, | ||||
| @ -156,9 +200,20 @@ impl From<(&PaymePaySaleResponse, u16)> for types::ErrorResponse { | ||||
| impl TryFrom<&PaymePaySaleResponse> for types::PaymentsResponseData { | ||||
|     type Error = error_stack::Report<errors::ConnectorError>; | ||||
|     fn try_from(value: &PaymePaySaleResponse) -> Result<Self, Self::Error> { | ||||
|         let redirection_data = match value.sale_3ds { | ||||
|             Some(true) => value | ||||
|                 .redirect_url | ||||
|                 .clone() | ||||
|                 .map(|url| services::RedirectForm::Form { | ||||
|                     endpoint: url.to_string(), | ||||
|                     method: services::Method::Get, | ||||
|                     form_fields: HashMap::<String, String>::new(), | ||||
|                 }), | ||||
|             _ => None, | ||||
|         }; | ||||
|         Ok(Self::TransactionResponse { | ||||
|             resource_id: types::ResponseId::ConnectorTransactionId(value.payme_sale_id.clone()), | ||||
|             redirection_data: None, | ||||
|             redirection_data, | ||||
|             mandate_reference: value.buyer_key.clone().map(|buyer_key| MandateReference { | ||||
|                 connector_mandate_id: Some(buyer_key.expose()), | ||||
|                 payment_method_id: None, | ||||
| @ -259,6 +314,7 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for GenerateSaleRequest { | ||||
|         let sale_type = SaleType::try_from(item)?; | ||||
|         let seller_payme_id = PaymeAuthType::try_from(&item.connector_auth_type)?.seller_payme_id; | ||||
|         let order_details = item.request.get_order_details()?; | ||||
|         let services = get_services(item); | ||||
|         let product_name = order_details | ||||
|             .first() | ||||
|             .ok_or_else(missing_field_err("order_details"))? | ||||
| @ -280,6 +336,7 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for GenerateSaleRequest { | ||||
|             sale_return_url: item.request.get_return_url()?, | ||||
|             sale_callback_url: item.request.get_webhook_url()?, | ||||
|             language: LANGUAGE.to_string(), | ||||
|             services, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| @ -397,59 +454,82 @@ impl<F> | ||||
|             types::PaymentsResponseData, | ||||
|         >, | ||||
|     ) -> Result<Self, Self::Error> { | ||||
|         let currency_code = item.data.request.get_currency()?; | ||||
|         let amount = item.data.request.get_amount()?; | ||||
|         let amount_in_base_unit = utils::to_currency_base_unit(amount, currency_code)?; | ||||
|         let pmd = item.data.request.payment_method_data.to_owned(); | ||||
|         let payme_auth_type = PaymeAuthType::try_from(&item.data.connector_auth_type)?; | ||||
|         match item.data.auth_type { | ||||
|             AuthenticationType::NoThreeDs => { | ||||
|                 let currency_code = item.data.request.get_currency()?; | ||||
|                 let amount = item.data.request.get_amount()?; | ||||
|                 let amount_in_base_unit = utils::to_currency_base_unit(amount, currency_code)?; | ||||
|                 let pmd = item.data.request.payment_method_data.to_owned(); | ||||
|                 let payme_auth_type = PaymeAuthType::try_from(&item.data.connector_auth_type)?; | ||||
|  | ||||
|         let session_token = match pmd { | ||||
|             Some(PaymentMethodData::Wallet( | ||||
|                 api_models::payments::WalletData::ApplePayThirdPartySdk(_), | ||||
|             )) => Some(api_models::payments::SessionToken::ApplePay(Box::new( | ||||
|                 api_models::payments::ApplepaySessionTokenResponse { | ||||
|                     session_token_data: | ||||
|                         api_models::payments::ApplePaySessionResponse::NoSessionResponse, | ||||
|                     payment_request_data: Some(api_models::payments::ApplePayPaymentRequest { | ||||
|                         country_code: item.data.get_billing_country()?, | ||||
|                         currency_code, | ||||
|                         total: api_models::payments::AmountInfo { | ||||
|                             label: "Apple Pay".to_string(), | ||||
|                             total_type: None, | ||||
|                             amount: amount_in_base_unit, | ||||
|                 let session_token = match pmd { | ||||
|                     Some(PaymentMethodData::Wallet( | ||||
|                         api_models::payments::WalletData::ApplePayThirdPartySdk(_), | ||||
|                     )) => Some(api_models::payments::SessionToken::ApplePay(Box::new( | ||||
|                         api_models::payments::ApplepaySessionTokenResponse { | ||||
|                             session_token_data: | ||||
|                                 api_models::payments::ApplePaySessionResponse::NoSessionResponse, | ||||
|                             payment_request_data: Some( | ||||
|                                 api_models::payments::ApplePayPaymentRequest { | ||||
|                                     country_code: item.data.get_billing_country()?, | ||||
|                                     currency_code, | ||||
|                                     total: api_models::payments::AmountInfo { | ||||
|                                         label: "Apple Pay".to_string(), | ||||
|                                         total_type: None, | ||||
|                                         amount: amount_in_base_unit, | ||||
|                                     }, | ||||
|                                     merchant_capabilities: None, | ||||
|                                     supported_networks: None, | ||||
|                                     merchant_identifier: None, | ||||
|                                 }, | ||||
|                             ), | ||||
|                             connector: "payme".to_string(), | ||||
|                             delayed_session_token: true, | ||||
|                             sdk_next_action: api_models::payments::SdkNextAction { | ||||
|                                 next_action: api_models::payments::NextActionCall::Sync, | ||||
|                             }, | ||||
|                             connector_reference_id: Some(item.response.payme_sale_id.to_owned()), | ||||
|                             connector_sdk_public_key: Some( | ||||
|                                 payme_auth_type.payme_public_key.expose(), | ||||
|                             ), | ||||
|                             connector_merchant_id: payme_auth_type | ||||
|                                 .payme_merchant_id | ||||
|                                 .map(|mid| mid.expose()), | ||||
|                         }, | ||||
|                         merchant_capabilities: None, | ||||
|                         supported_networks: None, | ||||
|                         merchant_identifier: None, | ||||
|                     ))), | ||||
|                     _ => None, | ||||
|                 }; | ||||
|                 Ok(Self { | ||||
|                     // We don't get any status from payme, so defaulting it to pending | ||||
|                     status: enums::AttemptStatus::Pending, | ||||
|                     preprocessing_id: Some(item.response.payme_sale_id.to_owned()), | ||||
|                     response: Ok(types::PaymentsResponseData::PreProcessingResponse { | ||||
|                         pre_processing_id: types::PreprocessingResponseId::ConnectorTransactionId( | ||||
|                             item.response.payme_sale_id, | ||||
|                         ), | ||||
|                         connector_metadata: None, | ||||
|                         session_token, | ||||
|                         connector_response_reference_id: None, | ||||
|                     }), | ||||
|                     connector: "payme".to_string(), | ||||
|                     delayed_session_token: true, | ||||
|                     sdk_next_action: api_models::payments::SdkNextAction { | ||||
|                         next_action: api_models::payments::NextActionCall::Sync, | ||||
|                     }, | ||||
|                     connector_reference_id: Some(item.response.payme_sale_id.to_owned()), | ||||
|                     connector_sdk_public_key: Some(payme_auth_type.payme_public_key.expose()), | ||||
|                     connector_merchant_id: payme_auth_type | ||||
|                         .payme_merchant_id | ||||
|                         .map(|mid| mid.expose()), | ||||
|                 }, | ||||
|             ))), | ||||
|             _ => None, | ||||
|         }; | ||||
|         Ok(Self { | ||||
|             // We don't get any status from payme, so defaulting it to pending | ||||
|             status: enums::AttemptStatus::Pending, | ||||
|             preprocessing_id: Some(item.response.payme_sale_id.to_owned()), | ||||
|             response: Ok(types::PaymentsResponseData::PreProcessingResponse { | ||||
|                 pre_processing_id: types::PreprocessingResponseId::ConnectorTransactionId( | ||||
|                     item.response.payme_sale_id, | ||||
|                 ), | ||||
|                 connector_metadata: None, | ||||
|                 session_token, | ||||
|                 connector_response_reference_id: None, | ||||
|                     ..item.data | ||||
|                 }) | ||||
|             } | ||||
|             AuthenticationType::ThreeDs => Ok(Self { | ||||
|                 status: enums::AttemptStatus::AuthenticationPending, | ||||
|                 preprocessing_id: Some(item.response.payme_sale_id.to_owned()), | ||||
|                 response: Ok(types::PaymentsResponseData::TransactionResponse { | ||||
|                     resource_id: types::ResponseId::ConnectorTransactionId( | ||||
|                         item.response.payme_sale_id.to_owned(), | ||||
|                     ), | ||||
|                     redirection_data: Some(services::RedirectForm::Payme), | ||||
|                     mandate_reference: None, | ||||
|                     connector_metadata: None, | ||||
|                     network_txn_id: None, | ||||
|                     connector_response_reference_id: None, | ||||
|                 }), | ||||
|                 ..item.data | ||||
|             }), | ||||
|             ..item.data | ||||
|         }) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -507,6 +587,100 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayRequest { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct PaymePayloadData { | ||||
|     meta_data: String, | ||||
| } | ||||
|  | ||||
| impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for Pay3dsRequest { | ||||
|     type Error = error_stack::Report<errors::ConnectorError>; | ||||
|     fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> { | ||||
|         match item.request.payment_method_data.clone() { | ||||
|             Some(api::PaymentMethodData::Card(_)) => { | ||||
|                 let buyer_email = item.request.get_email()?; | ||||
|                 let buyer_name = item.get_billing_address()?.get_full_name()?; | ||||
|  | ||||
|                 let payload_data = item.request.get_redirect_response_payload()?.expose(); | ||||
|  | ||||
|                 let jwt_data: PaymePayloadData = serde_json::from_value(payload_data) | ||||
|                     .into_report() | ||||
|                     .change_context(errors::ConnectorError::MissingConnectorRedirectionPayload { | ||||
|                         field_name: "meta_data_jwt", | ||||
|                     })?; | ||||
|  | ||||
|                 let payme_sale_id = item | ||||
|                     .request | ||||
|                     .connector_transaction_id | ||||
|                     .clone() | ||||
|                     .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; | ||||
|                 let buyer_key = match item.payment_method_token.clone() { | ||||
|                     Some(key) => key, | ||||
|                     None => Err(errors::ConnectorError::MissingRequiredField { | ||||
|                         field_name: "buyer_key", | ||||
|                     })?, | ||||
|                 }; | ||||
|                 Ok(Self { | ||||
|                     buyer_email, | ||||
|                     buyer_key, | ||||
|                     buyer_name, | ||||
|                     payme_sale_id, | ||||
|                     meta_data_jwt: jwt_data.meta_data, | ||||
|                 }) | ||||
|             } | ||||
|             Some(api::PaymentMethodData::CardRedirect(_)) | ||||
|             | Some(api::PaymentMethodData::Wallet(_)) | ||||
|             | Some(api::PaymentMethodData::PayLater(_)) | ||||
|             | Some(api::PaymentMethodData::BankRedirect(_)) | ||||
|             | Some(api::PaymentMethodData::BankDebit(_)) | ||||
|             | Some(api::PaymentMethodData::BankTransfer(_)) | ||||
|             | Some(api::PaymentMethodData::Crypto(_)) | ||||
|             | Some(api::PaymentMethodData::MandatePayment) | ||||
|             | Some(api::PaymentMethodData::Reward) | ||||
|             | Some(api::PaymentMethodData::Upi(_)) | ||||
|             | Some(api::PaymentMethodData::Voucher(_)) | ||||
|             | Some(api::PaymentMethodData::GiftCard(_)) | ||||
|             | None => { | ||||
|                 Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl TryFrom<&types::TokenizationRouterData> for CaptureBuyerRequest { | ||||
|     type Error = error_stack::Report<errors::ConnectorError>; | ||||
|     fn try_from(item: &types::TokenizationRouterData) -> Result<Self, Self::Error> { | ||||
|         match item.request.payment_method_data.clone() { | ||||
|             api::PaymentMethodData::Card(req_card) => { | ||||
|                 let seller_payme_id = | ||||
|                     PaymeAuthType::try_from(&item.connector_auth_type)?.seller_payme_id; | ||||
|                 let card = PaymeCard { | ||||
|                     credit_card_cvv: req_card.card_cvc.clone(), | ||||
|                     credit_card_exp: req_card | ||||
|                         .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), | ||||
|                     credit_card_number: req_card.card_number, | ||||
|                 }; | ||||
|                 Ok(Self { | ||||
|                     card, | ||||
|                     seller_payme_id, | ||||
|                 }) | ||||
|             } | ||||
|             api::PaymentMethodData::Wallet(_) | ||||
|             | api::PaymentMethodData::CardRedirect(_) | ||||
|             | api::PaymentMethodData::PayLater(_) | ||||
|             | api::PaymentMethodData::BankRedirect(_) | ||||
|             | api::PaymentMethodData::BankDebit(_) | ||||
|             | api::PaymentMethodData::BankTransfer(_) | ||||
|             | api::PaymentMethodData::Crypto(_) | ||||
|             | api::PaymentMethodData::MandatePayment | ||||
|             | api::PaymentMethodData::Reward | ||||
|             | api::PaymentMethodData::Upi(_) | ||||
|             | api::PaymentMethodData::Voucher(_) | ||||
|             | api::PaymentMethodData::GiftCard(_) => { | ||||
|                 Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Auth Struct | ||||
| pub struct PaymeAuthType { | ||||
| @ -612,6 +786,8 @@ pub struct PaymePaySaleResponse { | ||||
|     buyer_key: Option<Secret<String>>, | ||||
|     status_error_details: Option<String>, | ||||
|     status_error_code: Option<u32>, | ||||
|     sale_3ds: Option<bool>, | ||||
|     redirect_url: Option<Url>, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize)] | ||||
| @ -619,6 +795,24 @@ pub struct PaymeMetadata { | ||||
|     payme_transaction_id: String, | ||||
| } | ||||
|  | ||||
| impl<F, T> | ||||
|     TryFrom<types::ResponseRouterData<F, CaptureBuyerResponse, T, types::PaymentsResponseData>> | ||||
|     for types::RouterData<F, T, types::PaymentsResponseData> | ||||
| { | ||||
|     type Error = error_stack::Report<errors::ConnectorError>; | ||||
|     fn try_from( | ||||
|         item: types::ResponseRouterData<F, CaptureBuyerResponse, T, types::PaymentsResponseData>, | ||||
|     ) -> Result<Self, Self::Error> { | ||||
|         Ok(Self { | ||||
|             payment_method_token: Some(item.response.buyer_key.clone()), | ||||
|             response: Ok(types::PaymentsResponseData::TokenizationResponse { | ||||
|                 token: item.response.buyer_key, | ||||
|             }), | ||||
|             ..item.data | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct PaymentCaptureRequest { | ||||
|     payme_sale_id: String, | ||||
| @ -745,6 +939,19 @@ impl<F, T> | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn get_services(item: &types::PaymentsPreProcessingRouterData) -> Option<ThreeDS> { | ||||
|     match item.auth_type { | ||||
|         api_models::enums::AuthenticationType::ThreeDs => { | ||||
|             let settings = ThreeDSSettings { active: true }; | ||||
|             Some(ThreeDS { | ||||
|                 name: ThreeDSType::ThreeDS, | ||||
|                 settings, | ||||
|             }) | ||||
|         } | ||||
|         api_models::enums::AuthenticationType::NoThreeDs => None, | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] | ||||
| pub struct PaymeErrorResponse { | ||||
|     pub status_code: u16, | ||||
| @ -794,6 +1001,8 @@ impl From<WebhookEventDataResource> for PaymePaySaleResponse { | ||||
|             payme_sale_id: value.payme_sale_id, | ||||
|             payme_transaction_id: value.payme_transaction_id, | ||||
|             buyer_key: value.buyer_key, | ||||
|             sale_3ds: None, | ||||
|             redirect_url: None, | ||||
|             status_error_code: value.status_error_code, | ||||
|             status_error_details: value.status_error_details, | ||||
|         } | ||||
|  | ||||
| @ -599,6 +599,7 @@ where | ||||
|         &connector, | ||||
|         payment_data, | ||||
|         router_data, | ||||
|         operation, | ||||
|         should_continue_further, | ||||
|     ) | ||||
|     .await?; | ||||
| @ -846,11 +847,12 @@ where | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn complete_preprocessing_steps_if_required<F, Req>( | ||||
| async fn complete_preprocessing_steps_if_required<F, Req, Q>( | ||||
|     state: &AppState, | ||||
|     connector: &api::ConnectorData, | ||||
|     payment_data: &PaymentData<F>, | ||||
|     mut router_data: router_types::RouterData<F, Req, router_types::PaymentsResponseData>, | ||||
|     operation: &BoxedOperation<'_, F, Q>, | ||||
|     should_continue_payment: bool, | ||||
| ) -> RouterResult<( | ||||
|     router_types::RouterData<F, Req, router_types::PaymentsResponseData>, | ||||
| @ -892,7 +894,9 @@ where | ||||
|             } | ||||
|         } | ||||
|         Some(api_models::payments::PaymentMethodData::Card(_)) => { | ||||
|             if connector.connector_name == router_types::Connector::Payme { | ||||
|             if connector.connector_name == router_types::Connector::Payme | ||||
|                 && !matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") | ||||
|             { | ||||
|                 router_data = router_data.preprocessing_steps(state, connector).await?; | ||||
|  | ||||
|                 let is_error_in_response = router_data.response.is_err(); | ||||
|  | ||||
| @ -163,7 +163,6 @@ default_imp_for_complete_authorize!( | ||||
|     connector::Opayo, | ||||
|     connector::Opennode, | ||||
|     connector::Payeezy, | ||||
|     connector::Payme, | ||||
|     connector::Payu, | ||||
|     connector::Rapyd, | ||||
|     connector::Square, | ||||
| @ -302,7 +301,6 @@ default_imp_for_connector_redirect_response!( | ||||
|     connector::Opayo, | ||||
|     connector::Opennode, | ||||
|     connector::Payeezy, | ||||
|     connector::Payme, | ||||
|     connector::Payu, | ||||
|     connector::Powertranz, | ||||
|     connector::Rapyd, | ||||
|  | ||||
| @ -368,6 +368,7 @@ impl TryFrom<types::PaymentsAuthorizeData> for types::PaymentsPreProcessingData | ||||
|             order_details: data.order_details, | ||||
|             router_return_url: data.router_return_url, | ||||
|             webhook_url: data.webhook_url, | ||||
|             complete_authorize_url: data.complete_authorize_url, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,12 +3,13 @@ use async_trait::async_trait; | ||||
| use super::{ConstructFlowSpecificData, Feature}; | ||||
| use crate::{ | ||||
|     core::{ | ||||
|         errors::{ConnectorErrorExt, RouterResult}, | ||||
|         errors::{self, ConnectorErrorExt, RouterResult}, | ||||
|         payments::{self, access_token, transformers, PaymentData}, | ||||
|     }, | ||||
|     routes::AppState, | ||||
|     services, | ||||
|     types::{self, api, domain}, | ||||
|     utils::OptionExt, | ||||
| }; | ||||
|  | ||||
| #[async_trait] | ||||
| @ -94,6 +95,28 @@ impl Feature<api::CompleteAuthorize, types::CompleteAuthorizeData> | ||||
|         access_token::add_access_token(state, connector, merchant_account, self).await | ||||
|     } | ||||
|  | ||||
|     async fn add_payment_method_token<'a>( | ||||
|         &mut self, | ||||
|         state: &AppState, | ||||
|         connector: &api::ConnectorData, | ||||
|         _tokenization_action: &payments::TokenizationAction, | ||||
|     ) -> RouterResult<Option<String>> { | ||||
|         // TODO: remove this and handle it in core | ||||
|         if matches!(connector.connector_name, types::Connector::Payme) { | ||||
|             let request = self.request.clone(); | ||||
|             payments::tokenization::add_payment_method_token( | ||||
|                 state, | ||||
|                 connector, | ||||
|                 &payments::TokenizationAction::TokenizeInConnector, | ||||
|                 self, | ||||
|                 types::PaymentMethodTokenizationData::try_from(request)?, | ||||
|             ) | ||||
|             .await | ||||
|         } else { | ||||
|             Ok(None) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async fn build_flow_specific_connector_request( | ||||
|         &mut self, | ||||
|         state: &AppState, | ||||
| @ -119,3 +142,18 @@ impl Feature<api::CompleteAuthorize, types::CompleteAuthorizeData> | ||||
|         Ok((request, true)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl TryFrom<types::CompleteAuthorizeData> for types::PaymentMethodTokenizationData { | ||||
|     type Error = error_stack::Report<errors::ApiErrorResponse>; | ||||
|  | ||||
|     fn try_from(data: types::CompleteAuthorizeData) -> Result<Self, Self::Error> { | ||||
|         Ok(Self { | ||||
|             payment_method_data: data | ||||
|                 .payment_method_data | ||||
|                 .get_required_value("payment_method_data")?, | ||||
|             browser_info: data.browser_info, | ||||
|             currency: data.currency, | ||||
|             amount: Some(data.amount), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1294,6 +1294,11 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsPreProce | ||||
|             connector_name, | ||||
|             payment_data.creds_identifier.as_deref(), | ||||
|         )); | ||||
|         let complete_authorize_url = Some(helpers::create_complete_authorize_url( | ||||
|             router_base_url, | ||||
|             attempt, | ||||
|             connector_name, | ||||
|         )); | ||||
|  | ||||
|         Ok(Self { | ||||
|             payment_method_data, | ||||
| @ -1306,6 +1311,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsPreProce | ||||
|             order_details, | ||||
|             router_return_url, | ||||
|             webhook_url, | ||||
|             complete_authorize_url, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -679,6 +679,7 @@ pub enum RedirectForm { | ||||
|     BlueSnap { | ||||
|         payment_fields_token: String, // payment-field-token | ||||
|     }, | ||||
|     Payme, | ||||
| } | ||||
|  | ||||
| impl From<(url::Url, Method)> for RedirectForm { | ||||
| @ -1124,6 +1125,33 @@ pub fn build_redirection_form( | ||||
|                 "))) | ||||
|                 }} | ||||
|         } | ||||
|         RedirectForm::Payme => { | ||||
|             maud::html! { | ||||
|                 (maud::DOCTYPE) | ||||
|                 head { | ||||
|                     (PreEscaped(r#"<script src="https://cdn.paymeservice.com/hf/v1/hostedfields.js"></script>"#)) | ||||
|                 } | ||||
|                 (PreEscaped("<script> | ||||
|                     var f = document.createElement('form'); | ||||
|                     f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/payme\"); | ||||
|                     f.method='POST'; | ||||
|                     PayMe.clientData() | ||||
|                     .then((data) => {{ | ||||
|                         var i=document.createElement('input'); | ||||
|                         i.type='hidden'; | ||||
|                         i.name='meta_data'; | ||||
|                         i.value=data.hash; | ||||
|                         f.appendChild(i); | ||||
|                         document.body.appendChild(f); | ||||
|                         f.submit(); | ||||
|                     }}) | ||||
|                     .catch((error) => {{ | ||||
|                         f.submit(); | ||||
|                     }}); | ||||
|             </script> | ||||
|                 ".to_string())) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -26,8 +26,12 @@ pub use crate::core::payments::{CustomerDetails, PaymentAddress}; | ||||
| #[cfg(feature = "payouts")] | ||||
| use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW; | ||||
| use crate::{ | ||||
|     core::{errors, payments::RecurringMandatePaymentData}, | ||||
|     core::{ | ||||
|         errors::{self, RouterResult}, | ||||
|         payments::RecurringMandatePaymentData, | ||||
|     }, | ||||
|     services, | ||||
|     utils::OptionExt, | ||||
| }; | ||||
|  | ||||
| pub type PaymentsAuthorizeRouterData = | ||||
| @ -387,6 +391,7 @@ pub struct PaymentsPreProcessingData { | ||||
|     pub order_details: Option<Vec<api_models::payments::OrderDetailsWithAmount>>, | ||||
|     pub router_return_url: Option<String>, | ||||
|     pub webhook_url: Option<String>, | ||||
|     pub complete_authorize_url: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| @ -926,26 +931,35 @@ impl<F> From<&RouterData<F, PaymentsAuthorizeData, PaymentsResponseData>> | ||||
| } | ||||
|  | ||||
| pub trait Tokenizable { | ||||
|     fn get_pm_data(&self) -> payments::PaymentMethodData; | ||||
|     fn get_pm_data(&self) -> RouterResult<payments::PaymentMethodData>; | ||||
|     fn set_session_token(&mut self, token: Option<String>); | ||||
| } | ||||
|  | ||||
| impl Tokenizable for VerifyRequestData { | ||||
|     fn get_pm_data(&self) -> payments::PaymentMethodData { | ||||
|         self.payment_method_data.clone() | ||||
|     fn get_pm_data(&self) -> RouterResult<payments::PaymentMethodData> { | ||||
|         Ok(self.payment_method_data.clone()) | ||||
|     } | ||||
|     fn set_session_token(&mut self, _token: Option<String>) {} | ||||
| } | ||||
|  | ||||
| impl Tokenizable for PaymentsAuthorizeData { | ||||
|     fn get_pm_data(&self) -> payments::PaymentMethodData { | ||||
|         self.payment_method_data.clone() | ||||
|     fn get_pm_data(&self) -> RouterResult<payments::PaymentMethodData> { | ||||
|         Ok(self.payment_method_data.clone()) | ||||
|     } | ||||
|     fn set_session_token(&mut self, token: Option<String>) { | ||||
|         self.session_token = token; | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Tokenizable for CompleteAuthorizeData { | ||||
|     fn get_pm_data(&self) -> RouterResult<payments::PaymentMethodData> { | ||||
|         self.payment_method_data | ||||
|             .clone() | ||||
|             .get_required_value("payment_method_data") | ||||
|     } | ||||
|     fn set_session_token(&mut self, _token: Option<String>) {} | ||||
| } | ||||
|  | ||||
| impl From<&VerifyRouterData> for PaymentsAuthorizeData { | ||||
|     fn from(data: &VerifyRouterData) -> Self { | ||||
|         Self { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Sakil Mostak
					Sakil Mostak