Skip to content

Commit

Permalink
feat(connector): [worldpay] add support for mandates (#6479)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
2 people authored and Sayak Bhattacharya committed Nov 26, 2024
1 parent eaf8dd7 commit dad2718
Show file tree
Hide file tree
Showing 9 changed files with 549 additions and 119 deletions.
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
49 changes: 49 additions & 0 deletions crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs
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(super) const THREE_DS_MODE: &str = "always";
pub(super) const THREE_DS_TYPE: &str = "integrated";
13 changes: 13 additions & 0 deletions crates/hyperswitch_connectors/src/connectors/worldpay/response.rs
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(super) const WP_CORRELATION_ID: &str = "WP-CorrelationId";
Loading

0 comments on commit dad2718

Please sign in to comment.