mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
feat(connector): [worldpay] add support for mandates (#6479)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -13,6 +13,7 @@ use common_utils::{
|
||||
};
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::{
|
||||
payment_method_data::PaymentMethodData,
|
||||
router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData},
|
||||
router_flow_types::{
|
||||
access_token_auth::AccessTokenAuth,
|
||||
@ -29,7 +30,7 @@ use hyperswitch_domain_models::{
|
||||
types::{
|
||||
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
|
||||
PaymentsCompleteAuthorizeRouterData, PaymentsSyncRouterData, RefundExecuteRouterData,
|
||||
RefundSyncRouterData, RefundsRouterData,
|
||||
RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData,
|
||||
},
|
||||
};
|
||||
use hyperswitch_interfaces::{
|
||||
@ -50,15 +51,17 @@ use requests::{
|
||||
use response::{
|
||||
EventType, ResponseIdStr, WorldpayErrorResponse, WorldpayEventResponse,
|
||||
WorldpayPaymentsResponse, WorldpayWebhookEventType, WorldpayWebhookTransactionId,
|
||||
WP_CORRELATION_ID,
|
||||
};
|
||||
use transformers::{self as worldpay, WP_CORRELATION_ID};
|
||||
use ring::hmac;
|
||||
use transformers::{self as worldpay};
|
||||
|
||||
use crate::{
|
||||
constants::headers,
|
||||
types::ResponseRouterData,
|
||||
utils::{
|
||||
construct_not_implemented_error_report, convert_amount, get_header_key_value,
|
||||
ForeignTryFrom, RefundsRequestData,
|
||||
is_mandate_supported, ForeignTryFrom, PaymentMethodDataType, RefundsRequestData,
|
||||
},
|
||||
};
|
||||
|
||||
@ -171,6 +174,19 @@ impl ConnectorValidation for Worldpay {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mandate_payment(
|
||||
&self,
|
||||
pm_type: Option<enums::PaymentMethodType>,
|
||||
pm_data: PaymentMethodData,
|
||||
) -> CustomResult<(), errors::ConnectorError> {
|
||||
let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]);
|
||||
is_mandate_supported(pm_data.clone(), pm_type, mandate_supported_pmd, self.id())
|
||||
}
|
||||
|
||||
fn is_webhook_source_verification_mandatory(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl api::Payment for Worldpay {}
|
||||
@ -179,15 +195,108 @@ impl api::MandateSetup for Worldpay {}
|
||||
impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsResponseData>
|
||||
for Worldpay
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &SetupMandateRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, masking::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: &SetupMandateRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!("{}api/payments", self.base_url(connectors)))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &SetupMandateRouterData,
|
||||
_connectors: &Connectors,
|
||||
) -> CustomResult<RequestContent, errors::ConnectorError> {
|
||||
let auth = worldpay::WorldpayAuthType::try_from(&req.connector_auth_type)
|
||||
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
let connector_router_data = worldpay::WorldpayRouterData::try_from((
|
||||
&self.get_currency_unit(),
|
||||
req.request.currency,
|
||||
req.request.minor_amount.unwrap_or_default(),
|
||||
req,
|
||||
))?;
|
||||
let connector_req =
|
||||
WorldpayPaymentsRequest::try_from((&connector_router_data, &auth.entity_id))?;
|
||||
|
||||
Ok(RequestContent::Json(Box::new(connector_req)))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
_req: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
|
||||
_connectors: &Connectors,
|
||||
req: &SetupMandateRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Option<Request>, errors::ConnectorError> {
|
||||
Err(
|
||||
errors::ConnectorError::NotImplemented("Setup Mandate flow for Worldpay".to_string())
|
||||
.into(),
|
||||
)
|
||||
Ok(Some(
|
||||
RequestBuilder::new()
|
||||
.method(Method::Post)
|
||||
.url(&types::SetupMandateType::get_url(self, req, connectors)?)
|
||||
.attach_default_headers()
|
||||
.headers(types::SetupMandateType::get_headers(self, req, connectors)?)
|
||||
.set_body(types::SetupMandateType::get_request_body(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &SetupMandateRouterData,
|
||||
event_builder: Option<&mut ConnectorEvent>,
|
||||
res: Response,
|
||||
) -> CustomResult<SetupMandateRouterData, errors::ConnectorError> {
|
||||
let response: WorldpayPaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Worldpay PaymentsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
event_builder.map(|i| i.set_response_body(&response));
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
let optional_correlation_id = res.headers.and_then(|headers| {
|
||||
headers
|
||||
.get(WP_CORRELATION_ID)
|
||||
.and_then(|header_value| header_value.to_str().ok())
|
||||
.map(|id| id.to_string())
|
||||
});
|
||||
|
||||
RouterData::foreign_try_from((
|
||||
ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
},
|
||||
optional_correlation_id,
|
||||
))
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
event_builder: Option<&mut ConnectorEvent>,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res, event_builder)
|
||||
}
|
||||
|
||||
fn get_5xx_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
event_builder: Option<&mut ConnectorEvent>,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res, event_builder)
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,6 +510,7 @@ impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Wor
|
||||
enums::AttemptStatus::Authorizing
|
||||
| enums::AttemptStatus::Authorized
|
||||
| enums::AttemptStatus::CaptureInitiated
|
||||
| enums::AttemptStatus::Charged
|
||||
| enums::AttemptStatus::Pending
|
||||
| enums::AttemptStatus::VoidInitiated,
|
||||
EventType::Authorized,
|
||||
@ -587,6 +697,7 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
|
||||
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
let connector_req =
|
||||
WorldpayPaymentsRequest::try_from((&connector_router_data, &auth.entity_id))?;
|
||||
|
||||
Ok(RequestContent::Json(Box::new(connector_req)))
|
||||
}
|
||||
|
||||
@ -739,7 +850,7 @@ impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResp
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
let optional_correlation_id = res.headers.and_then(|headers| {
|
||||
headers
|
||||
.get("WP-CorrelationId")
|
||||
.get(WP_CORRELATION_ID)
|
||||
.and_then(|header_value| header_value.to_str().ok())
|
||||
.map(|id| id.to_string())
|
||||
});
|
||||
@ -994,17 +1105,45 @@ impl IncomingWebhook for Worldpay {
|
||||
&self,
|
||||
request: &IncomingWebhookRequestDetails<'_>,
|
||||
_merchant_id: &common_utils::id_type::MerchantId,
|
||||
connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
|
||||
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
|
||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||
let secret_str = std::str::from_utf8(&connector_webhook_secrets.secret)
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
let to_sign = format!(
|
||||
"{}{}",
|
||||
secret_str,
|
||||
std::str::from_utf8(request.body)
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?
|
||||
);
|
||||
Ok(to_sign.into_bytes())
|
||||
Ok(request.body.to_vec())
|
||||
}
|
||||
|
||||
async fn verify_webhook_source(
|
||||
&self,
|
||||
request: &IncomingWebhookRequestDetails<'_>,
|
||||
merchant_id: &common_utils::id_type::MerchantId,
|
||||
connector_webhook_details: Option<common_utils::pii::SecretSerdeValue>,
|
||||
_connector_account_details: crypto::Encryptable<masking::Secret<serde_json::Value>>,
|
||||
connector_label: &str,
|
||||
) -> CustomResult<bool, errors::ConnectorError> {
|
||||
let connector_webhook_secrets = self
|
||||
.get_webhook_source_verification_merchant_secret(
|
||||
merchant_id,
|
||||
connector_label,
|
||||
connector_webhook_details,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||
let signature = self
|
||||
.get_webhook_source_verification_signature(request, &connector_webhook_secrets)
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||
let message = self
|
||||
.get_webhook_source_verification_message(
|
||||
request,
|
||||
merchant_id,
|
||||
&connector_webhook_secrets,
|
||||
)
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||
let secret_key = hex::decode(connector_webhook_secrets.secret)
|
||||
.change_context(errors::ConnectorError::WebhookVerificationSecretInvalid)?;
|
||||
|
||||
let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &secret_key);
|
||||
let signed_message = hmac::sign(&signing_key, &message);
|
||||
let computed_signature = hex::encode(signed_message.as_ref());
|
||||
|
||||
Ok(computed_signature.as_bytes() == hex::encode(signature).as_bytes())
|
||||
}
|
||||
|
||||
fn get_webhook_object_reference_id(
|
||||
|
||||
@ -24,6 +24,7 @@ pub struct Merchant {
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Instruction {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub settlement: Option<AutoSettlement>,
|
||||
pub method: PaymentMethod,
|
||||
pub payment_instrument: PaymentInstrument,
|
||||
@ -33,6 +34,43 @@ pub struct Instruction {
|
||||
pub debt_repayment: Option<bool>,
|
||||
#[serde(rename = "threeDS")]
|
||||
pub three_ds: Option<ThreeDSRequest>,
|
||||
/// For setting up mandates
|
||||
pub token_creation: Option<TokenCreation>,
|
||||
/// For specifying CIT vs MIT
|
||||
pub customer_agreement: Option<CustomerAgreement>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub struct TokenCreation {
|
||||
#[serde(rename = "type")]
|
||||
pub token_type: TokenCreationType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TokenCreationType {
|
||||
Worldpay,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomerAgreement {
|
||||
#[serde(rename = "type")]
|
||||
pub agreement_type: CustomerAgreementType,
|
||||
pub stored_card_usage: StoredCardUsageType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CustomerAgreementType {
|
||||
Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum StoredCardUsageType {
|
||||
First,
|
||||
Subsequent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@ -225,6 +263,14 @@ pub enum ThreeDSRequestChannel {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ThreeDSRequestChallenge {
|
||||
pub return_url: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preference: Option<ThreeDsPreference>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ThreeDsPreference {
|
||||
ChallengeMandated,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
@ -284,3 +330,6 @@ pub struct WorldpayCompleteAuthorizationRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub collection_reference: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) const THREE_DS_MODE: &str = "always";
|
||||
pub(super) const THREE_DS_TYPE: &str = "integrated";
|
||||
|
||||
@ -41,6 +41,16 @@ pub struct AuthorizedResponse {
|
||||
pub description: Option<String>,
|
||||
pub risk_factors: Option<Vec<RiskFactorsInner>>,
|
||||
pub fraud: Option<Fraud>,
|
||||
/// Mandate's token
|
||||
pub token: Option<MandateToken>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MandateToken {
|
||||
pub href: Secret<String>,
|
||||
pub token_id: String,
|
||||
pub token_expiry_date_time: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@ -445,3 +455,6 @@ pub enum WorldpayWebhookStatus {
|
||||
SentForRefund,
|
||||
RefundFailed,
|
||||
}
|
||||
|
||||
/// Worldpay's unique reference ID for a request
|
||||
pub(super) const WP_CORRELATION_ID: &str = "WP-CorrelationId";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use api_models::payments::Address;
|
||||
use api_models::payments::{Address, MandateIds, MandateReferenceId};
|
||||
use base64::Engine;
|
||||
use common_enums::enums;
|
||||
use common_utils::{
|
||||
@ -10,9 +10,11 @@ use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::{
|
||||
payment_method_data::{PaymentMethodData, WalletData},
|
||||
router_data::{ConnectorAuthType, ErrorResponse, RouterData},
|
||||
router_flow_types::Authorize,
|
||||
router_request_types::{PaymentsAuthorizeData, ResponseId},
|
||||
router_response_types::{PaymentsResponseData, RedirectForm},
|
||||
router_flow_types::{Authorize, SetupMandate},
|
||||
router_request_types::{
|
||||
BrowserInformation, PaymentsAuthorizeData, ResponseId, SetupMandateRequestData,
|
||||
},
|
||||
router_response_types::{MandateReference, PaymentsResponseData, RedirectForm},
|
||||
types,
|
||||
};
|
||||
use hyperswitch_interfaces::{api, errors};
|
||||
@ -22,7 +24,10 @@ use serde::{Deserialize, Serialize};
|
||||
use super::{requests::*, response::*};
|
||||
use crate::{
|
||||
types::ResponseRouterData,
|
||||
utils::{self, AddressData, ForeignTryFrom, PaymentsAuthorizeRequestData, RouterData as _},
|
||||
utils::{
|
||||
self, AddressData, ForeignTryFrom, PaymentsAuthorizeRequestData,
|
||||
PaymentsSetupMandateRequestData, RouterData as RouterDataTrait,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@ -47,18 +52,15 @@ impl<T> TryFrom<(&api::CurrencyUnit, enums::Currency, MinorUnit, T)> for Worldpa
|
||||
}
|
||||
}
|
||||
|
||||
/// Worldpay's unique reference ID for a request
|
||||
pub const WP_CORRELATION_ID: &str = "WP-CorrelationId";
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct WorldpayConnectorMetadataObject {
|
||||
pub merchant_name: Option<Secret<String>>,
|
||||
}
|
||||
|
||||
impl TryFrom<&Option<pii::SecretSerdeValue>> for WorldpayConnectorMetadataObject {
|
||||
impl TryFrom<Option<&pii::SecretSerdeValue>> for WorldpayConnectorMetadataObject {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(meta_data: &Option<pii::SecretSerdeValue>) -> Result<Self, Self::Error> {
|
||||
let metadata: Self = utils::to_connector_meta_from_secret::<Self>(meta_data.clone())
|
||||
fn try_from(meta_data: Option<&pii::SecretSerdeValue>) -> Result<Self, Self::Error> {
|
||||
let metadata: Self = utils::to_connector_meta_from_secret::<Self>(meta_data.cloned())
|
||||
.change_context(errors::ConnectorError::InvalidConnectorConfig {
|
||||
config: "metadata",
|
||||
})?;
|
||||
@ -69,6 +71,7 @@ impl TryFrom<&Option<pii::SecretSerdeValue>> for WorldpayConnectorMetadataObject
|
||||
fn fetch_payment_instrument(
|
||||
payment_method: PaymentMethodData,
|
||||
billing_address: Option<&Address>,
|
||||
mandate_ids: Option<MandateIds>,
|
||||
) -> CustomResult<PaymentInstrument, errors::ConnectorError> {
|
||||
match payment_method {
|
||||
PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment {
|
||||
@ -103,6 +106,29 @@ fn fetch_payment_instrument(
|
||||
None
|
||||
},
|
||||
})),
|
||||
PaymentMethodData::MandatePayment => mandate_ids
|
||||
.and_then(|mandate_ids| {
|
||||
mandate_ids
|
||||
.mandate_reference_id
|
||||
.and_then(|mandate_id| match mandate_id {
|
||||
MandateReferenceId::ConnectorMandateId(connector_mandate_id) => {
|
||||
connector_mandate_id.get_connector_mandate_id().map(|href| {
|
||||
PaymentInstrument::CardToken(CardToken {
|
||||
payment_type: PaymentType::Token,
|
||||
href,
|
||||
cvc: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.ok_or(
|
||||
errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "connector_mandate_id",
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
PaymentMethodData::Wallet(wallet) => match wallet {
|
||||
WalletData::GooglePay(data) => Ok(PaymentInstrument::Googlepay(WalletPayment {
|
||||
payment_type: PaymentType::Encrypted,
|
||||
@ -149,7 +175,6 @@ fn fetch_payment_instrument(
|
||||
| PaymentMethodData::BankDebit(_)
|
||||
| PaymentMethodData::BankTransfer(_)
|
||||
| PaymentMethodData::Crypto(_)
|
||||
| PaymentMethodData::MandatePayment
|
||||
| PaymentMethodData::Reward
|
||||
| PaymentMethodData::RealTimePayment(_)
|
||||
| PaymentMethodData::Upi(_)
|
||||
@ -196,109 +221,300 @@ impl TryFrom<(enums::PaymentMethod, Option<enums::PaymentMethodType>)> for Payme
|
||||
}
|
||||
}
|
||||
|
||||
impl
|
||||
TryFrom<(
|
||||
&WorldpayRouterData<&RouterData<Authorize, PaymentsAuthorizeData, PaymentsResponseData>>,
|
||||
&Secret<String>,
|
||||
)> for WorldpayPaymentsRequest
|
||||
// Trait to abstract common functionality between Authorize and SetupMandate
|
||||
trait WorldpayPaymentsRequestData {
|
||||
fn get_return_url(&self) -> Result<String, error_stack::Report<errors::ConnectorError>>;
|
||||
fn get_auth_type(&self) -> &enums::AuthenticationType;
|
||||
fn get_browser_info(&self) -> Option<&BrowserInformation>;
|
||||
fn get_payment_method_data(&self) -> &PaymentMethodData;
|
||||
fn get_setup_future_usage(&self) -> Option<enums::FutureUsage>;
|
||||
fn get_off_session(&self) -> Option<bool>;
|
||||
fn get_mandate_id(&self) -> Option<MandateIds>;
|
||||
fn get_currency(&self) -> enums::Currency;
|
||||
fn get_optional_billing_address(&self) -> Option<&Address>;
|
||||
fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue>;
|
||||
fn get_payment_method(&self) -> enums::PaymentMethod;
|
||||
fn get_payment_method_type(&self) -> Option<enums::PaymentMethodType>;
|
||||
fn get_connector_request_reference_id(&self) -> String;
|
||||
fn get_is_mandate_payment(&self) -> bool;
|
||||
fn get_settlement_info(&self, _amount: i64) -> Option<AutoSettlement> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldpayPaymentsRequestData
|
||||
for RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>
|
||||
{
|
||||
fn get_return_url(&self) -> Result<String, error_stack::Report<errors::ConnectorError>> {
|
||||
self.request.get_router_return_url()
|
||||
}
|
||||
|
||||
fn get_auth_type(&self) -> &enums::AuthenticationType {
|
||||
&self.auth_type
|
||||
}
|
||||
|
||||
fn get_browser_info(&self) -> Option<&BrowserInformation> {
|
||||
self.request.browser_info.as_ref()
|
||||
}
|
||||
|
||||
fn get_payment_method_data(&self) -> &PaymentMethodData {
|
||||
&self.request.payment_method_data
|
||||
}
|
||||
|
||||
fn get_setup_future_usage(&self) -> Option<enums::FutureUsage> {
|
||||
self.request.setup_future_usage
|
||||
}
|
||||
|
||||
fn get_off_session(&self) -> Option<bool> {
|
||||
self.request.off_session
|
||||
}
|
||||
|
||||
fn get_mandate_id(&self) -> Option<MandateIds> {
|
||||
self.request.mandate_id.clone()
|
||||
}
|
||||
|
||||
fn get_currency(&self) -> enums::Currency {
|
||||
self.request.currency
|
||||
}
|
||||
|
||||
fn get_optional_billing_address(&self) -> Option<&Address> {
|
||||
self.get_optional_billing()
|
||||
}
|
||||
|
||||
fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue> {
|
||||
self.connector_meta_data.as_ref()
|
||||
}
|
||||
|
||||
fn get_payment_method(&self) -> enums::PaymentMethod {
|
||||
self.payment_method
|
||||
}
|
||||
|
||||
fn get_payment_method_type(&self) -> Option<enums::PaymentMethodType> {
|
||||
self.request.payment_method_type
|
||||
}
|
||||
|
||||
fn get_connector_request_reference_id(&self) -> String {
|
||||
self.connector_request_reference_id.clone()
|
||||
}
|
||||
|
||||
fn get_is_mandate_payment(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldpayPaymentsRequestData
|
||||
for RouterData<Authorize, PaymentsAuthorizeData, PaymentsResponseData>
|
||||
{
|
||||
fn get_return_url(&self) -> Result<String, error_stack::Report<errors::ConnectorError>> {
|
||||
self.request.get_complete_authorize_url()
|
||||
}
|
||||
|
||||
fn get_auth_type(&self) -> &enums::AuthenticationType {
|
||||
&self.auth_type
|
||||
}
|
||||
|
||||
fn get_browser_info(&self) -> Option<&BrowserInformation> {
|
||||
self.request.browser_info.as_ref()
|
||||
}
|
||||
|
||||
fn get_payment_method_data(&self) -> &PaymentMethodData {
|
||||
&self.request.payment_method_data
|
||||
}
|
||||
|
||||
fn get_setup_future_usage(&self) -> Option<enums::FutureUsage> {
|
||||
self.request.setup_future_usage
|
||||
}
|
||||
|
||||
fn get_off_session(&self) -> Option<bool> {
|
||||
self.request.off_session
|
||||
}
|
||||
|
||||
fn get_mandate_id(&self) -> Option<MandateIds> {
|
||||
self.request.mandate_id.clone()
|
||||
}
|
||||
|
||||
fn get_currency(&self) -> enums::Currency {
|
||||
self.request.currency
|
||||
}
|
||||
|
||||
fn get_optional_billing_address(&self) -> Option<&Address> {
|
||||
self.get_optional_billing()
|
||||
}
|
||||
|
||||
fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue> {
|
||||
self.connector_meta_data.as_ref()
|
||||
}
|
||||
|
||||
fn get_payment_method(&self) -> enums::PaymentMethod {
|
||||
self.payment_method
|
||||
}
|
||||
|
||||
fn get_payment_method_type(&self) -> Option<enums::PaymentMethodType> {
|
||||
self.request.payment_method_type
|
||||
}
|
||||
|
||||
fn get_connector_request_reference_id(&self) -> String {
|
||||
self.connector_request_reference_id.clone()
|
||||
}
|
||||
|
||||
fn get_is_mandate_payment(&self) -> bool {
|
||||
self.request.is_mandate_payment()
|
||||
}
|
||||
|
||||
fn get_settlement_info(&self, amount: i64) -> Option<AutoSettlement> {
|
||||
match (self.request.capture_method.unwrap_or_default(), amount) {
|
||||
(_, 0) => None,
|
||||
(enums::CaptureMethod::Automatic, _) => Some(AutoSettlement { auto: true }),
|
||||
(enums::CaptureMethod::Manual, _) | (enums::CaptureMethod::ManualMultiple, _) => {
|
||||
Some(AutoSettlement { auto: false })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dangling helper function to create ThreeDS request
|
||||
fn create_three_ds_request<T: WorldpayPaymentsRequestData>(
|
||||
router_data: &T,
|
||||
is_mandate_payment: bool,
|
||||
) -> Result<Option<ThreeDSRequest>, error_stack::Report<errors::ConnectorError>> {
|
||||
match router_data.get_auth_type() {
|
||||
enums::AuthenticationType::ThreeDs => {
|
||||
let browser_info = router_data.get_browser_info().ok_or(
|
||||
errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "browser_info",
|
||||
},
|
||||
)?;
|
||||
|
||||
let accept_header = browser_info
|
||||
.accept_header
|
||||
.clone()
|
||||
.get_required_value("accept_header")
|
||||
.change_context(errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "accept_header",
|
||||
})?;
|
||||
|
||||
let user_agent_header = browser_info
|
||||
.user_agent
|
||||
.clone()
|
||||
.get_required_value("user_agent")
|
||||
.change_context(errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "user_agent",
|
||||
})?;
|
||||
|
||||
Ok(Some(ThreeDSRequest {
|
||||
three_ds_type: THREE_DS_TYPE.to_string(),
|
||||
mode: THREE_DS_MODE.to_string(),
|
||||
device_data: ThreeDSRequestDeviceData {
|
||||
accept_header,
|
||||
user_agent_header,
|
||||
browser_language: browser_info.language.clone(),
|
||||
browser_screen_width: browser_info.screen_width,
|
||||
browser_screen_height: browser_info.screen_height,
|
||||
browser_color_depth: browser_info.color_depth.map(|depth| depth.to_string()),
|
||||
time_zone: browser_info.time_zone.map(|tz| tz.to_string()),
|
||||
browser_java_enabled: browser_info.java_enabled,
|
||||
browser_javascript_enabled: browser_info.java_script_enabled,
|
||||
channel: Some(ThreeDSRequestChannel::Browser),
|
||||
},
|
||||
challenge: ThreeDSRequestChallenge {
|
||||
return_url: router_data.get_return_url()?,
|
||||
preference: if is_mandate_payment {
|
||||
Some(ThreeDsPreference::ChallengeMandated)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
// Dangling helper function to determine token and agreement settings
|
||||
fn get_token_and_agreement(
|
||||
payment_method_data: &PaymentMethodData,
|
||||
setup_future_usage: Option<enums::FutureUsage>,
|
||||
off_session: Option<bool>,
|
||||
) -> (Option<TokenCreation>, Option<CustomerAgreement>) {
|
||||
match (payment_method_data, setup_future_usage, off_session) {
|
||||
// CIT
|
||||
(PaymentMethodData::Card(_), Some(enums::FutureUsage::OffSession), _) => (
|
||||
Some(TokenCreation {
|
||||
token_type: TokenCreationType::Worldpay,
|
||||
}),
|
||||
Some(CustomerAgreement {
|
||||
agreement_type: CustomerAgreementType::Subscription,
|
||||
stored_card_usage: StoredCardUsageType::First,
|
||||
}),
|
||||
),
|
||||
// MIT
|
||||
(PaymentMethodData::Card(_), _, Some(true)) => (
|
||||
None,
|
||||
Some(CustomerAgreement {
|
||||
agreement_type: CustomerAgreementType::Subscription,
|
||||
stored_card_usage: StoredCardUsageType::Subsequent,
|
||||
}),
|
||||
),
|
||||
_ => (None, None),
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation for WorldpayPaymentsRequest using abstracted request
|
||||
impl<T: WorldpayPaymentsRequestData> TryFrom<(&WorldpayRouterData<&T>, &Secret<String>)>
|
||||
for WorldpayPaymentsRequest
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
|
||||
fn try_from(
|
||||
req: (
|
||||
&WorldpayRouterData<
|
||||
&RouterData<Authorize, PaymentsAuthorizeData, PaymentsResponseData>,
|
||||
>,
|
||||
&Secret<String>,
|
||||
),
|
||||
) -> Result<Self, Self::Error> {
|
||||
fn try_from(req: (&WorldpayRouterData<&T>, &Secret<String>)) -> Result<Self, Self::Error> {
|
||||
let (item, entity_id) = req;
|
||||
let worldpay_connector_metadata_object: WorldpayConnectorMetadataObject =
|
||||
WorldpayConnectorMetadataObject::try_from(&item.router_data.connector_meta_data)?;
|
||||
WorldpayConnectorMetadataObject::try_from(item.router_data.get_connector_meta_data())?;
|
||||
|
||||
let merchant_name = worldpay_connector_metadata_object.merchant_name.ok_or(
|
||||
errors::ConnectorError::InvalidConnectorConfig {
|
||||
config: "metadata.merchant_name",
|
||||
},
|
||||
)?;
|
||||
let three_ds = match item.router_data.auth_type {
|
||||
enums::AuthenticationType::ThreeDs => {
|
||||
let browser_info = item
|
||||
.router_data
|
||||
.request
|
||||
.browser_info
|
||||
.clone()
|
||||
.get_required_value("browser_info")
|
||||
.change_context(errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "browser_info",
|
||||
})?;
|
||||
let accept_header = browser_info
|
||||
.accept_header
|
||||
.get_required_value("accept_header")
|
||||
.change_context(errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "accept_header",
|
||||
})?;
|
||||
let user_agent_header = browser_info
|
||||
.user_agent
|
||||
.get_required_value("user_agent")
|
||||
.change_context(errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "user_agent",
|
||||
})?;
|
||||
Some(ThreeDSRequest {
|
||||
three_ds_type: "integrated".to_string(),
|
||||
mode: "always".to_string(),
|
||||
device_data: ThreeDSRequestDeviceData {
|
||||
accept_header,
|
||||
user_agent_header,
|
||||
browser_language: browser_info.language.clone(),
|
||||
browser_screen_width: browser_info.screen_width,
|
||||
browser_screen_height: browser_info.screen_height,
|
||||
browser_color_depth: browser_info
|
||||
.color_depth
|
||||
.map(|depth| depth.to_string()),
|
||||
time_zone: browser_info.time_zone.map(|tz| tz.to_string()),
|
||||
browser_java_enabled: browser_info.java_enabled,
|
||||
browser_javascript_enabled: browser_info.java_script_enabled,
|
||||
channel: Some(ThreeDSRequestChannel::Browser),
|
||||
},
|
||||
challenge: ThreeDSRequestChallenge {
|
||||
return_url: item.router_data.request.get_complete_authorize_url()?,
|
||||
},
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let is_mandate_payment = item.router_data.get_is_mandate_payment();
|
||||
let three_ds = create_three_ds_request(item.router_data, is_mandate_payment)?;
|
||||
|
||||
let (token_creation, customer_agreement) = get_token_and_agreement(
|
||||
item.router_data.get_payment_method_data(),
|
||||
item.router_data.get_setup_future_usage(),
|
||||
item.router_data.get_off_session(),
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
instruction: Instruction {
|
||||
settlement: item
|
||||
.router_data
|
||||
.request
|
||||
.capture_method
|
||||
.map(|capture_method| AutoSettlement {
|
||||
auto: capture_method == enums::CaptureMethod::Automatic,
|
||||
}),
|
||||
settlement: item.router_data.get_settlement_info(item.amount),
|
||||
method: PaymentMethod::try_from((
|
||||
item.router_data.payment_method,
|
||||
item.router_data.request.payment_method_type,
|
||||
item.router_data.get_payment_method(),
|
||||
item.router_data.get_payment_method_type(),
|
||||
))?,
|
||||
payment_instrument: fetch_payment_instrument(
|
||||
item.router_data.request.payment_method_data.clone(),
|
||||
item.router_data.get_optional_billing(),
|
||||
item.router_data.get_payment_method_data().clone(),
|
||||
item.router_data.get_optional_billing_address(),
|
||||
item.router_data.get_mandate_id(),
|
||||
)?,
|
||||
narrative: InstructionNarrative {
|
||||
line1: merchant_name.expose(),
|
||||
},
|
||||
value: PaymentValue {
|
||||
amount: item.amount,
|
||||
currency: item.router_data.request.currency,
|
||||
currency: item.router_data.get_currency(),
|
||||
},
|
||||
debt_repayment: None,
|
||||
three_ds,
|
||||
token_creation,
|
||||
customer_agreement,
|
||||
},
|
||||
merchant: Merchant {
|
||||
entity: entity_id.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
transaction_reference: item.router_data.connector_request_reference_id.clone(),
|
||||
transaction_reference: item.router_data.get_connector_request_reference_id(),
|
||||
customer: None,
|
||||
})
|
||||
}
|
||||
@ -409,14 +625,22 @@ impl<F, T>
|
||||
),
|
||||
) -> Result<Self, Self::Error> {
|
||||
let (router_data, optional_correlation_id) = item;
|
||||
let (description, redirection_data, error) = router_data
|
||||
let (description, redirection_data, mandate_reference, error) = router_data
|
||||
.response
|
||||
.other_fields
|
||||
.as_ref()
|
||||
.map(|other_fields| match other_fields {
|
||||
WorldpayPaymentResponseFields::AuthorizedResponse(res) => {
|
||||
(res.description.clone(), None, None)
|
||||
}
|
||||
WorldpayPaymentResponseFields::AuthorizedResponse(res) => (
|
||||
res.description.clone(),
|
||||
None,
|
||||
res.token.as_ref().map(|mandate_token| MandateReference {
|
||||
connector_mandate_id: Some(mandate_token.href.clone().expose()),
|
||||
payment_method_id: Some(mandate_token.token_id.clone()),
|
||||
mandate_metadata: None,
|
||||
connector_mandate_request_reference_id: None,
|
||||
}),
|
||||
None,
|
||||
),
|
||||
WorldpayPaymentResponseFields::DDCResponse(res) => (
|
||||
None,
|
||||
Some(RedirectForm::WorldpayDDCForm {
|
||||
@ -435,6 +659,7 @@ impl<F, T>
|
||||
]),
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
WorldpayPaymentResponseFields::ThreeDsChallenged(res) => (
|
||||
None,
|
||||
@ -447,15 +672,17 @@ impl<F, T>
|
||||
)]),
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
WorldpayPaymentResponseFields::RefusedResponse(res) => (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some((res.refusal_code.clone(), res.refusal_description.clone())),
|
||||
),
|
||||
WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None),
|
||||
WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None, None),
|
||||
})
|
||||
.unwrap_or((None, None, None));
|
||||
.unwrap_or((None, None, None, None));
|
||||
let worldpay_status = router_data.response.outcome.clone();
|
||||
let optional_error_message = match worldpay_status {
|
||||
PaymentOutcome::ThreeDsAuthenticationFailed => {
|
||||
@ -475,7 +702,7 @@ impl<F, T>
|
||||
optional_correlation_id.clone(),
|
||||
))?,
|
||||
redirection_data: Box::new(redirection_data),
|
||||
mandate_reference: Box::new(None),
|
||||
mandate_reference: Box::new(mandate_reference),
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: optional_correlation_id.clone(),
|
||||
|
||||
@ -1434,6 +1434,7 @@ impl RefundsRequestData for RefundsData {
|
||||
pub trait PaymentsSetupMandateRequestData {
|
||||
fn get_browser_info(&self) -> Result<BrowserInformation, Error>;
|
||||
fn get_email(&self) -> Result<Email, Error>;
|
||||
fn get_router_return_url(&self) -> Result<String, Error>;
|
||||
fn is_card(&self) -> bool;
|
||||
}
|
||||
|
||||
@ -1446,6 +1447,11 @@ impl PaymentsSetupMandateRequestData for SetupMandateRequestData {
|
||||
fn get_email(&self) -> Result<Email, Error> {
|
||||
self.email.clone().ok_or_else(missing_field_err("email"))
|
||||
}
|
||||
fn get_router_return_url(&self) -> Result<String, Error> {
|
||||
self.router_return_url
|
||||
.clone()
|
||||
.ok_or_else(missing_field_err("router_return_url"))
|
||||
}
|
||||
fn is_card(&self) -> bool {
|
||||
matches!(self.payment_method_data, PaymentMethodData::Card(_))
|
||||
}
|
||||
|
||||
@ -644,7 +644,7 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> for ApiErrorRespon
|
||||
AER::Unprocessable(ApiError::new("WE", 5, "There was an issue processing the webhook body", None))
|
||||
},
|
||||
Self::WebhookInvalidMerchantSecret => {
|
||||
AER::BadRequest(ApiError::new("WE", 6, "Merchant Secret set for webhook source verificartion is invalid", None))
|
||||
AER::BadRequest(ApiError::new("WE", 6, "Merchant Secret set for webhook source verification is invalid", None))
|
||||
}
|
||||
Self::IntegrityCheckFailed {
|
||||
reason,
|
||||
|
||||
@ -180,6 +180,3 @@ pub const VAULT_DELETE_FLOW_TYPE: &str = "delete_from_vault";
|
||||
/// Vault Fingerprint fetch flow type
|
||||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||
pub const VAULT_GET_FINGERPRINT_FLOW_TYPE: &str = "get_fingerprint_vault";
|
||||
|
||||
/// Worldpay's unique reference ID for a request TODO: Move to hyperswitch_connectors/constants once Worldpay is moved to connectors crate
|
||||
pub const WP_CORRELATION_ID: &str = "WP-CorrelationId";
|
||||
|
||||
@ -2396,7 +2396,7 @@ fn update_connector_mandate_details_for_the_flow<F: Clone>(
|
||||
))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
original_connector_mandate_reference_id
|
||||
};
|
||||
|
||||
payment_data.payment_attempt.connector_mandate_detail = connector_mandate_reference_id
|
||||
|
||||
@ -89,7 +89,6 @@ pub mod headers {
|
||||
pub const X_REDIRECT_URI: &str = "x-redirect-uri";
|
||||
pub const X_TENANT_ID: &str = "x-tenant-id";
|
||||
pub const X_CLIENT_SECRET: &str = "X-Client-Secret";
|
||||
pub const X_WP_API_VERSION: &str = "WP-Api-Version";
|
||||
}
|
||||
|
||||
pub mod pii {
|
||||
|
||||
Reference in New Issue
Block a user