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:
Kashif
2024-11-08 18:00:39 +05:30
committed by GitHub
parent 1be2654b4f
commit 378ec44db9
9 changed files with 549 additions and 119 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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