mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 11:06:50 +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