Skip to content

Commit 378ec44

Browse files
feat(connector): [worldpay] add support for mandates (#6479)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 1be2654 commit 378ec44

File tree

9 files changed

+549
-119
lines changed

9 files changed

+549
-119
lines changed

crates/hyperswitch_connectors/src/connectors/worldpay.rs

Lines changed: 159 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use common_utils::{
1313
};
1414
use error_stack::ResultExt;
1515
use hyperswitch_domain_models::{
16+
payment_method_data::PaymentMethodData,
1617
router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData},
1718
router_flow_types::{
1819
access_token_auth::AccessTokenAuth,
@@ -29,7 +30,7 @@ use hyperswitch_domain_models::{
2930
types::{
3031
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
3132
PaymentsCompleteAuthorizeRouterData, PaymentsSyncRouterData, RefundExecuteRouterData,
32-
RefundSyncRouterData, RefundsRouterData,
33+
RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData,
3334
},
3435
};
3536
use hyperswitch_interfaces::{
@@ -50,15 +51,17 @@ use requests::{
5051
use response::{
5152
EventType, ResponseIdStr, WorldpayErrorResponse, WorldpayEventResponse,
5253
WorldpayPaymentsResponse, WorldpayWebhookEventType, WorldpayWebhookTransactionId,
54+
WP_CORRELATION_ID,
5355
};
54-
use transformers::{self as worldpay, WP_CORRELATION_ID};
56+
use ring::hmac;
57+
use transformers::{self as worldpay};
5558

5659
use crate::{
5760
constants::headers,
5861
types::ResponseRouterData,
5962
utils::{
6063
construct_not_implemented_error_report, convert_amount, get_header_key_value,
61-
ForeignTryFrom, RefundsRequestData,
64+
is_mandate_supported, ForeignTryFrom, PaymentMethodDataType, RefundsRequestData,
6265
},
6366
};
6467

@@ -171,6 +174,19 @@ impl ConnectorValidation for Worldpay {
171174
),
172175
}
173176
}
177+
178+
fn validate_mandate_payment(
179+
&self,
180+
pm_type: Option<enums::PaymentMethodType>,
181+
pm_data: PaymentMethodData,
182+
) -> CustomResult<(), errors::ConnectorError> {
183+
let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]);
184+
is_mandate_supported(pm_data.clone(), pm_type, mandate_supported_pmd, self.id())
185+
}
186+
187+
fn is_webhook_source_verification_mandatory(&self) -> bool {
188+
true
189+
}
174190
}
175191

176192
impl api::Payment for Worldpay {}
@@ -179,15 +195,108 @@ impl api::MandateSetup for Worldpay {}
179195
impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsResponseData>
180196
for Worldpay
181197
{
182-
fn build_request(
198+
fn get_headers(
199+
&self,
200+
req: &SetupMandateRouterData,
201+
connectors: &Connectors,
202+
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
203+
self.build_headers(req, connectors)
204+
}
205+
206+
fn get_content_type(&self) -> &'static str {
207+
self.common_get_content_type()
208+
}
209+
210+
fn get_url(
211+
&self,
212+
_req: &SetupMandateRouterData,
213+
connectors: &Connectors,
214+
) -> CustomResult<String, errors::ConnectorError> {
215+
Ok(format!("{}api/payments", self.base_url(connectors)))
216+
}
217+
218+
fn get_request_body(
183219
&self,
184-
_req: &RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
220+
req: &SetupMandateRouterData,
185221
_connectors: &Connectors,
222+
) -> CustomResult<RequestContent, errors::ConnectorError> {
223+
let auth = worldpay::WorldpayAuthType::try_from(&req.connector_auth_type)
224+
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
225+
let connector_router_data = worldpay::WorldpayRouterData::try_from((
226+
&self.get_currency_unit(),
227+
req.request.currency,
228+
req.request.minor_amount.unwrap_or_default(),
229+
req,
230+
))?;
231+
let connector_req =
232+
WorldpayPaymentsRequest::try_from((&connector_router_data, &auth.entity_id))?;
233+
234+
Ok(RequestContent::Json(Box::new(connector_req)))
235+
}
236+
237+
fn build_request(
238+
&self,
239+
req: &SetupMandateRouterData,
240+
connectors: &Connectors,
186241
) -> CustomResult<Option<Request>, errors::ConnectorError> {
187-
Err(
188-
errors::ConnectorError::NotImplemented("Setup Mandate flow for Worldpay".to_string())
189-
.into(),
190-
)
242+
Ok(Some(
243+
RequestBuilder::new()
244+
.method(Method::Post)
245+
.url(&types::SetupMandateType::get_url(self, req, connectors)?)
246+
.attach_default_headers()
247+
.headers(types::SetupMandateType::get_headers(self, req, connectors)?)
248+
.set_body(types::SetupMandateType::get_request_body(
249+
self, req, connectors,
250+
)?)
251+
.build(),
252+
))
253+
}
254+
255+
fn handle_response(
256+
&self,
257+
data: &SetupMandateRouterData,
258+
event_builder: Option<&mut ConnectorEvent>,
259+
res: Response,
260+
) -> CustomResult<SetupMandateRouterData, errors::ConnectorError> {
261+
let response: WorldpayPaymentsResponse = res
262+
.response
263+
.parse_struct("Worldpay PaymentsResponse")
264+
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
265+
266+
event_builder.map(|i| i.set_response_body(&response));
267+
router_env::logger::info!(connector_response=?response);
268+
let optional_correlation_id = res.headers.and_then(|headers| {
269+
headers
270+
.get(WP_CORRELATION_ID)
271+
.and_then(|header_value| header_value.to_str().ok())
272+
.map(|id| id.to_string())
273+
});
274+
275+
RouterData::foreign_try_from((
276+
ResponseRouterData {
277+
response,
278+
data: data.clone(),
279+
http_code: res.status_code,
280+
},
281+
optional_correlation_id,
282+
))
283+
.change_context(errors::ConnectorError::ResponseHandlingFailed)
284+
}
285+
286+
fn get_error_response(
287+
&self,
288+
res: Response,
289+
event_builder: Option<&mut ConnectorEvent>,
290+
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
291+
self.build_error_response(res, event_builder)
292+
}
293+
294+
fn get_5xx_error_response(
295+
&self,
296+
res: Response,
297+
event_builder: Option<&mut ConnectorEvent>,
298+
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
299+
self.build_error_response(res, event_builder)
191300
}
192301
}
193302

@@ -401,6 +510,7 @@ impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Wor
401510
enums::AttemptStatus::Authorizing
402511
| enums::AttemptStatus::Authorized
403512
| enums::AttemptStatus::CaptureInitiated
513+
| enums::AttemptStatus::Charged
404514
| enums::AttemptStatus::Pending
405515
| enums::AttemptStatus::VoidInitiated,
406516
EventType::Authorized,
@@ -587,6 +697,7 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
587697
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
588698
let connector_req =
589699
WorldpayPaymentsRequest::try_from((&connector_router_data, &auth.entity_id))?;
700+
590701
Ok(RequestContent::Json(Box::new(connector_req)))
591702
}
592703

@@ -739,7 +850,7 @@ impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResp
739850
router_env::logger::info!(connector_response=?response);
740851
let optional_correlation_id = res.headers.and_then(|headers| {
741852
headers
742-
.get("WP-CorrelationId")
853+
.get(WP_CORRELATION_ID)
743854
.and_then(|header_value| header_value.to_str().ok())
744855
.map(|id| id.to_string())
745856
});
@@ -994,17 +1105,45 @@ impl IncomingWebhook for Worldpay {
9941105
&self,
9951106
request: &IncomingWebhookRequestDetails<'_>,
9961107
_merchant_id: &common_utils::id_type::MerchantId,
997-
connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
1108+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
9981109
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
999-
let secret_str = std::str::from_utf8(&connector_webhook_secrets.secret)
1000-
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1001-
let to_sign = format!(
1002-
"{}{}",
1003-
secret_str,
1004-
std::str::from_utf8(request.body)
1005-
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?
1006-
);
1007-
Ok(to_sign.into_bytes())
1110+
Ok(request.body.to_vec())
1111+
}
1112+
1113+
async fn verify_webhook_source(
1114+
&self,
1115+
request: &IncomingWebhookRequestDetails<'_>,
1116+
merchant_id: &common_utils::id_type::MerchantId,
1117+
connector_webhook_details: Option<common_utils::pii::SecretSerdeValue>,
1118+
_connector_account_details: crypto::Encryptable<masking::Secret<serde_json::Value>>,
1119+
connector_label: &str,
1120+
) -> CustomResult<bool, errors::ConnectorError> {
1121+
let connector_webhook_secrets = self
1122+
.get_webhook_source_verification_merchant_secret(
1123+
merchant_id,
1124+
connector_label,
1125+
connector_webhook_details,
1126+
)
1127+
.await
1128+
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1129+
let signature = self
1130+
.get_webhook_source_verification_signature(request, &connector_webhook_secrets)
1131+
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1132+
let message = self
1133+
.get_webhook_source_verification_message(
1134+
request,
1135+
merchant_id,
1136+
&connector_webhook_secrets,
1137+
)
1138+
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1139+
let secret_key = hex::decode(connector_webhook_secrets.secret)
1140+
.change_context(errors::ConnectorError::WebhookVerificationSecretInvalid)?;
1141+
1142+
let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &secret_key);
1143+
let signed_message = hmac::sign(&signing_key, &message);
1144+
let computed_signature = hex::encode(signed_message.as_ref());
1145+
1146+
Ok(computed_signature.as_bytes() == hex::encode(signature).as_bytes())
10081147
}
10091148

10101149
fn get_webhook_object_reference_id(

crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub struct Merchant {
2424
#[derive(Clone, Debug, PartialEq, Serialize)]
2525
#[serde(rename_all = "camelCase")]
2626
pub struct Instruction {
27+
#[serde(skip_serializing_if = "Option::is_none")]
2728
pub settlement: Option<AutoSettlement>,
2829
pub method: PaymentMethod,
2930
pub payment_instrument: PaymentInstrument,
@@ -33,6 +34,43 @@ pub struct Instruction {
3334
pub debt_repayment: Option<bool>,
3435
#[serde(rename = "threeDS")]
3536
pub three_ds: Option<ThreeDSRequest>,
37+
/// For setting up mandates
38+
pub token_creation: Option<TokenCreation>,
39+
/// For specifying CIT vs MIT
40+
pub customer_agreement: Option<CustomerAgreement>,
41+
}
42+
43+
#[derive(Clone, Debug, PartialEq, Serialize)]
44+
pub struct TokenCreation {
45+
#[serde(rename = "type")]
46+
pub token_type: TokenCreationType,
47+
}
48+
49+
#[derive(Clone, Debug, PartialEq, Serialize)]
50+
#[serde(rename_all = "lowercase")]
51+
pub enum TokenCreationType {
52+
Worldpay,
53+
}
54+
55+
#[derive(Clone, Debug, PartialEq, Serialize)]
56+
#[serde(rename_all = "camelCase")]
57+
pub struct CustomerAgreement {
58+
#[serde(rename = "type")]
59+
pub agreement_type: CustomerAgreementType,
60+
pub stored_card_usage: StoredCardUsageType,
61+
}
62+
63+
#[derive(Clone, Debug, PartialEq, Serialize)]
64+
#[serde(rename_all = "lowercase")]
65+
pub enum CustomerAgreementType {
66+
Subscription,
67+
}
68+
69+
#[derive(Clone, Debug, PartialEq, Serialize)]
70+
#[serde(rename_all = "lowercase")]
71+
pub enum StoredCardUsageType {
72+
First,
73+
Subsequent,
3674
}
3775

3876
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -225,6 +263,14 @@ pub enum ThreeDSRequestChannel {
225263
#[serde(rename_all = "camelCase")]
226264
pub struct ThreeDSRequestChallenge {
227265
pub return_url: String,
266+
#[serde(skip_serializing_if = "Option::is_none")]
267+
pub preference: Option<ThreeDsPreference>,
268+
}
269+
270+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
271+
#[serde(rename_all = "camelCase")]
272+
pub enum ThreeDsPreference {
273+
ChallengeMandated,
228274
}
229275

230276
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
@@ -284,3 +330,6 @@ pub struct WorldpayCompleteAuthorizationRequest {
284330
#[serde(skip_serializing_if = "Option::is_none")]
285331
pub collection_reference: Option<String>,
286332
}
333+
334+
pub(super) const THREE_DS_MODE: &str = "always";
335+
pub(super) const THREE_DS_TYPE: &str = "integrated";

crates/hyperswitch_connectors/src/connectors/worldpay/response.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ pub struct AuthorizedResponse {
4141
pub description: Option<String>,
4242
pub risk_factors: Option<Vec<RiskFactorsInner>>,
4343
pub fraud: Option<Fraud>,
44+
/// Mandate's token
45+
pub token: Option<MandateToken>,
46+
}
47+
48+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49+
#[serde(rename_all = "camelCase")]
50+
pub struct MandateToken {
51+
pub href: Secret<String>,
52+
pub token_id: String,
53+
pub token_expiry_date_time: String,
4454
}
4555

4656
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -445,3 +455,6 @@ pub enum WorldpayWebhookStatus {
445455
SentForRefund,
446456
RefundFailed,
447457
}
458+
459+
/// Worldpay's unique reference ID for a request
460+
pub(super) const WP_CORRELATION_ID: &str = "WP-CorrelationId";

0 commit comments

Comments
 (0)