feat(connector): [Payme] Implement Card 3DS with sdk flow (#2082)

Co-authored-by: Sangamesh <sangamesh.kulkarni@juspay.in>
This commit is contained in:
Sakil Mostak
2023-09-05 20:53:59 +05:30
committed by GitHub
parent 7f5695df93
commit 99f1780fd7
10 changed files with 575 additions and 73 deletions

View File

@ -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"

View File

@ -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
{

View File

@ -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,
}

View File

@ -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();

View File

@ -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,

View File

@ -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,
})
}
}

View File

@ -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),
})
}
}

View File

@ -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,
})
}
}

View File

@ -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()))
}
}
}
}

View File

@ -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 {