Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(connector): [worldpay] add support for mandates #6479

Merged
merged 16 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 159 additions & 20 deletions crates/hyperswitch_connectors/src/connectors/worldpay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,7 +30,7 @@ use hyperswitch_domain_models::{
types::{
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
PaymentsCompleteAuthorizeRouterData, PaymentsSyncRouterData, RefundExecuteRouterData,
RefundSyncRouterData, RefundsRouterData,
RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData,
},
};
use hyperswitch_interfaces::{
Expand All @@ -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,
},
};

Expand Down Expand Up @@ -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 {}
Expand All @@ -179,15 +195,108 @@ impl api::MandateSetup for Worldpay {}
impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsResponseData>
for Worldpay
{
fn build_request(
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: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
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: &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)
}
}

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

Expand Down Expand Up @@ -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())
});
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -284,3 +330,6 @@ pub struct WorldpayCompleteAuthorizationRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub collection_reference: Option<String>,
}

pub const THREE_DS_MODE: &str = "always";
kashif-m marked this conversation as resolved.
Show resolved Hide resolved
pub const THREE_DS_TYPE: &str = "integrated";
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -445,3 +455,6 @@ pub enum WorldpayWebhookStatus {
SentForRefund,
RefundFailed,
}

/// Worldpay's unique reference ID for a request
pub const WP_CORRELATION_ID: &str = "WP-CorrelationId";
Loading
Loading